Skip to content

Commit 488004b

Browse files
authored
feat: useTransient (#73)
1 parent f21a368 commit 488004b

File tree

9 files changed

+80
-24
lines changed

9 files changed

+80
-24
lines changed

src/core/api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ export function useEvent<EVENT extends {}>(expectEventType?: QEvent | string): E
329329
// @public (undocumented)
330330
export function useHostElement(): Element;
331331

332+
// @public (undocumented)
333+
export function useTransient<OBJ, ARGS extends any[], RET>(obj: OBJ, factory: (this: OBJ, ...args: ARGS) => RET, ...args: ARGS): RET;
334+
332335
// @public (undocumented)
333336
export function useURL(): URL;
334337

src/core/component/q-component-ctx.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ComponentRenderQueue, visitJsxNode } from '../render/q-render';
55
import { AttributeMarker } from '../util/markers';
66
import { flattenPromiseTree } from '../util/promises';
77
import { QrlStyles, styleContent, styleHost } from './qrl-styles';
8-
import { _qObject } from '../object/q-object';
8+
import { _stateQObject } from '../object/q-object';
99
import { qProps } from '../props/q-props.public';
1010

1111
// TODO(misko): Can we get rid of this whole file, and instead teach qProps to know how to render
@@ -41,7 +41,7 @@ export class QComponentCtx {
4141
if (hook) {
4242
const values: OnHookReturn[] = await hook('qMount');
4343
values.forEach((v) => {
44-
props['state:' + v.state] = _qObject(v.value, v.state);
44+
props['state:' + v.state] = _stateQObject(v.value, v.state);
4545
});
4646
}
4747
} catch (e) {

src/core/component/qrl-hook.public.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,16 @@ export function qHook<COMP extends QComponent, ARGS extends {} | unknown = unkno
2828
/**
2929
* @public
3030
*/
31-
export function qHook<COMP extends QComponent, ARGS extends {} | undefined = any, RET = unknown>(
32-
hook: (props: PropsOf<COMP>, state: StateOf<COMP>, args: ARGS) => ValueOrPromise<RET>
33-
): QHook<PropsOf<COMP>, StateOf<COMP>, ARGS, RET> {
31+
export function qHook(hook: any, symbol?: string): any {
32+
if (typeof hook === 'string') return hook;
33+
if (typeof symbol === 'string') {
34+
const match = String(hook).match(EXTRACT_IMPORT_PATH);
35+
if (match && match[2]) {
36+
return (match[2] + '#' + symbol) as any;
37+
} else {
38+
throw new Error('dynamic import not found: ' + String(hook));
39+
}
40+
}
3441
const qrlFn = async (element: HTMLElement, event: Event, url: URL) => {
3542
const isQwikInternalHook = typeof event == 'string';
3643
// isQwikInternalHook && console.log('HOOK', event, element, url);
@@ -50,9 +57,9 @@ export function qHook<COMP extends QComponent, ARGS extends {} | undefined = any
5057
);
5158
};
5259
if (qTest) {
53-
return toDevModeQRL(qrlFn, new Error()) as any;
60+
return toDevModeQRL(qrlFn, new Error());
5461
}
55-
return qrlFn as any;
62+
return qrlFn;
5663
}
5764

5865
/**
@@ -67,3 +74,6 @@ export interface QHook<
6774
__brand__: 'QHook';
6875
with(args: ARGS): QHook<PROPS, STATE, ARGS, RET>;
6976
}
77+
78+
// https://regexr.com/68v72
79+
const EXTRACT_IMPORT_PATH = /import\(\s*(['"])([^\1]+)\1\s*\)/;

src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export { QwikDOMAttributes, QwikJSX } from './render/jsx/types/jsx-qwik';
5454
export type { QwikIntrinsicElements } from './render/jsx/types/jsx-qwik-elements';
5555
export { qRender } from './render/q-render.public';
5656
export { useEvent, useHostElement, useURL } from './use/use-core.public';
57+
export { useTransient } from './use/use-transient.public';
5758
//////////////////////////////////////////////////////////////////////////////////////////
5859
// Developer Low-Level API
5960
//////////////////////////////////////////////////////////////////////////////////////////

src/core/object/q-object.ts

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,27 @@ import { safeQSubscribe } from '../use/use-core.public';
44
import type { QObject as IQObject } from './q-object.public';
55
export const Q_OBJECT_PREFIX_SEP = ':';
66

7-
export function _qObject<T>(obj: T, prefix?: string, isId: boolean = false): T {
7+
export function _qObject<T>(obj: T): T {
88
assertEqual(unwrapProxy(obj), obj, 'Unexpected proxy at this location');
9-
const id = isId
10-
? (prefix as string)
11-
: (prefix == null ? '' : prefix + Q_OBJECT_PREFIX_SEP) + generateId();
12-
const proxy = readWriteProxy(obj as any as IQObject<T>, id);
9+
const proxy = readWriteProxy(obj as any as IQObject<T>, generateId());
1310
Object.assign(proxy, obj);
1411
return proxy;
1512
}
1613

14+
export function _stateQObject<T>(obj: T, prefix: string): T {
15+
const id = getQObjectId(obj);
16+
if (id) {
17+
(obj as any)[QObjectIdSymbol] = prefix + Q_OBJECT_PREFIX_SEP + id;
18+
return obj;
19+
} else {
20+
return readWriteProxy(obj as any as IQObject<T>, prefix + Q_OBJECT_PREFIX_SEP + generateId());
21+
}
22+
}
23+
24+
export function _restoreQObject<T>(obj: T, id: string): T {
25+
return readWriteProxy(obj as any as IQObject<T>, id);
26+
}
27+
1728
function QObject_notifyWrite(id: string, doc: Document | null) {
1829
if (doc) {
1930
doc.querySelectorAll(idToComponentSelector(id)).forEach(qNotifyRender);
@@ -35,6 +46,17 @@ export function getQObjectId(obj: any): string | null {
3546
return (obj && typeof obj === 'object' && obj[QObjectIdSymbol]) || null;
3647
}
3748

49+
export function getTransient<T>(obj: any, key: any): T | null {
50+
assertDefined(getQObjectId(obj));
51+
return obj[QOjectTransientsSymbol].get(key);
52+
}
53+
54+
export function setTransient<T>(obj: any, key: any, value: T): T {
55+
assertDefined(getQObjectId(obj));
56+
obj[QOjectTransientsSymbol].set(key, value);
57+
return value;
58+
}
59+
3860
function idToComponentSelector(id: string): any {
3961
id = id.replace(/([^\w\d])/g, (_, v) => '\\' + v);
4062
return '[q\\:obj*=' + (isStateObj(id) ? '' : '\\!') + id + ']';
@@ -61,6 +83,7 @@ export function readWriteProxy<T extends object>(target: T, id: string): T {
6183
}
6284

6385
const QOjectTargetSymbol = ':target:';
86+
const QOjectTransientsSymbol = ':transients:';
6487
const QObjectIdSymbol = ':id:';
6588
const QObjectDocumentSymbol = ':doc:';
6689

@@ -90,13 +113,17 @@ export function wrap<T>(value: T): T {
90113
class ReadWriteProxyHandler<T extends object> implements ProxyHandler<T> {
91114
private id: string;
92115
private doc: Document | null = null;
116+
private transients: WeakMap<any, any> | null = null;
93117
constructor(id: string) {
94118
this.id = id;
95119
}
96120

97121
get(target: T, prop: string): any {
98122
if (prop === QOjectTargetSymbol) return target;
99123
if (prop === QObjectIdSymbol) return this.id;
124+
if (prop === QOjectTransientsSymbol) {
125+
return this.transients || (this.transients = new WeakMap());
126+
}
100127
const value = (target as any)[prop];
101128
QObject_notifyRead(target);
102129
return wrap(value);
@@ -105,6 +132,8 @@ class ReadWriteProxyHandler<T extends object> implements ProxyHandler<T> {
105132
set(target: T, prop: string, newValue: any): boolean {
106133
if (prop === QObjectDocumentSymbol) {
107134
this.doc = newValue;
135+
} else if (prop == QObjectIdSymbol) {
136+
this.id = newValue;
108137
} else {
109138
const unwrappedNewValue = unwrapProxy(newValue);
110139
const oldValue = (target as any)[prop];

src/core/object/q-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { assertDefined } from '../assert/assert';
22
import { JSON_OBJ_PREFIX } from '../json/q-json';
33
import { qDev } from '../util/qdev';
44
import { clearQProps, clearQPropsMap, QPropsContext } from '../props/q-props';
5-
import { getQObjectId, _qObject } from './q-object';
5+
import { getQObjectId, _restoreQObject } from './q-object';
66
import { qProps } from '../props/q-props.public';
77

88
export interface Store {
@@ -55,7 +55,7 @@ function reviveQObjects(map: Record<string, object> | null) {
5555
for (const key in map) {
5656
if (Object.prototype.hasOwnProperty.call(map, key)) {
5757
const value = map[key];
58-
map[key] = _qObject(value, key, true);
58+
map[key] = _restoreQObject(value, key);
5959
}
6060
}
6161
}

src/core/object/q-store.unit.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createDocument } from '../../testing/document';
22
import { qProps, QProps } from '../props/q-props.public';
33
import { qObject, qDehydrate } from '@builder.io/qwik';
4-
import { _qObject } from './q-object';
4+
import { _stateQObject } from './q-object';
55

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

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

2121
qDehydrate(document);
2222

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

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

@@ -36,8 +36,8 @@ describe('q-element', () => {
3636
expect(qDiv.foo).toEqual(foo);
3737
});
3838
it('should serialize cyclic graphs', () => {
39-
const foo = _qObject({ mark: 'foo', bar: {} }, 'foo');
40-
const bar = _qObject({ mark: 'bar', foo: foo }, 'bar');
39+
const foo = _stateQObject({ mark: 'foo', bar: {} }, 'foo');
40+
const bar = _stateQObject({ mark: 'bar', foo: foo }, 'bar');
4141
foo.bar = bar;
4242
qDiv.foo = foo;
4343

src/core/props/q-props.unit.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { ParsedQRL } from '../import/qrl';
44
import { diff, test_clearqPropsCache as test_clearQPropsCache } from './q-props';
55
import type { QComponent } from '../component/q-component.public';
66
import { qObject } from '../object/q-object.public';
7-
import { getQObjectId, _qObject } from '../object/q-object';
7+
import { getQObjectId, _stateQObject } from '../object/q-object';
88
import { qDehydrate } from '../object/q-store.public';
99
import { qProps, QProps } from './q-props.public';
1010

@@ -120,8 +120,8 @@ describe('q-element', () => {
120120

121121
describe('state', () => {
122122
it('should retrieve state by name', () => {
123-
const state1 = _qObject({ mark: 1 }, '');
124-
const state2 = _qObject({ mark: 2 }, 'foo');
123+
const state1 = _stateQObject({ mark: 1 }, '');
124+
const state2 = _stateQObject({ mark: 2 }, 'foo');
125125
qDiv['state:'] = state1;
126126
qDiv['state:foo'] = state2;
127127

@@ -167,8 +167,8 @@ describe('q-element', () => {
167167

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

174174
const child = document.createElement('child');
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { getTransient, setTransient } from '../object/q-object';
2+
3+
/**
4+
* @public
5+
*/
6+
export function useTransient<OBJ, ARGS extends any[], RET>(
7+
obj: OBJ,
8+
factory: (this: OBJ, ...args: ARGS) => RET,
9+
...args: ARGS
10+
): RET {
11+
const existing = getTransient<RET>(obj, factory);
12+
return existing || setTransient(obj, factory, factory.apply(obj, args));
13+
}

0 commit comments

Comments
 (0)