Compare commits

..

14 Commits

Author SHA1 Message Date
Nicolas Bayet d0cdc489b1 up 2025-10-02 15:22:13 +02:00
Nicolas Bayet a309ec498b up 2025-10-01 13:57:58 +02:00
Nicolas Bayet 1ddf2ceff9 up 2025-10-01 13:55:40 +02:00
Nicolas Bayet 38574a1f3a no reactivity in setup 2025-09-29 18:50:26 +02:00
Nicolas Bayet 6f2600ba5f md 2025-09-29 18:50:16 +02:00
Nicolas Bayet ed9db6ea2a withoutReactivity 2025-09-29 13:34:58 +02:00
Nicolas Bayet 0c1f9f3ffa fix for dropdown 2025-09-28 14:57:49 +02:00
Nicolas Bayet 4385969e2e up 2025-09-26 18:58:15 +02:00
Nicolas Bayet e31d195bb7 up 2025-09-26 18:38:40 +02:00
Nicolas Bayet 836fdada7f up 2025-09-26 18:31:03 +02:00
Nicolas Bayet 22580661c4 up 2025-09-26 18:27:22 +02:00
Nicolas Bayet adb9d405bb up 2025-09-26 18:08:15 +02:00
Nicolas Bayet 8d12bf17dc comment 2025-09-26 16:06:50 +02:00
Nicolas Bayet c74c66ab3b up 2025-09-26 15:37:51 +02:00
22 changed files with 8385 additions and 5221 deletions
+6079 -3199
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -22,7 +22,7 @@
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production", "build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production", "build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
"test": "jest", "test": "jest",
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000", "test:debug": "node node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py", "playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
"playground": "npm run build && npm run playground:serve", "playground": "npm run build && npm run playground:serve",
+18
View File
@@ -0,0 +1,18 @@
# encountered issues
## dropdown issue
- there was a problem that writing in a state while the effect was updated.
- the tracking of signal being written were dropped because we cleared it
after re-running the effect that made a write.
- solution: clear the tracked signal before re-executing the effects
- reading signal A while also writing signal A makes an infinite loop
- current solution: use toRaw in order to not track the read
- possible better solution to explore: do not track read if there is a write in a effect.
## website issue
- a rpc request was made on onWillStart, onWillStart was tracking reads. (see WebsiteBuilderClientAction)
- The read subsequently made a write, that re-triggered the onWillStart.
- A similar situation happened with onWillUpdateProps (see Transition)
- solution: prevent tracking reads in onWillStart and onWillUpdateProps
# future
- worker for computation?
- cap'n web
+24
View File
@@ -1,4 +1,28 @@
export type ExecutionContext = {
onReadAtom: (atom: Atom) => void;
unsubcribe?: (scheduledContexts: Set<ExecutionContext>) => void;
update?: Function;
atoms?: Set<Atom>;
meta?: any;
// getParent: () => ExecutionContext | undefined;
// getChildren: () => ExecutionContext[];
// schedule: () => void;
};
export type customDirectives = Record< export type customDirectives = Record<
string, string,
(node: Element, value: string, modifier: string[]) => void (node: Element, value: string, modifier: string[]) => void
>; >;
export type Atom = {
executionContexts: Set<ExecutionContext>;
dependents: Set<DerivedAtom>;
getValue: () => any;
};
export type OldValue = any;
export type DerivedAtom = Atom & {
dependencies: Map<Atom, OldValue>;
computed: boolean;
};
+46
View File
@@ -0,0 +1,46 @@
export type TaskContext = { isCancelled: boolean; cancel: () => void; meta: Record<string, any> };
export const taskContextStack: TaskContext[] = [];
export function getTaskContext() {
return taskContextStack[taskContextStack.length - 1];
}
export function makeTaskContext(): TaskContext {
let isCancelled = false;
return {
get isCancelled() {
return isCancelled;
},
cancel() {
isCancelled = true;
},
meta: {},
};
}
export function useTaskContext(ctx?: TaskContext) {
ctx ??= makeTaskContext();
taskContextStack.push(ctx);
return {
ctx,
cleanup: () => {
taskContextStack.pop();
},
};
}
export function pushTaskContext(context: TaskContext) {
taskContextStack.push(context);
}
export function popTaskContext() {
taskContextStack.pop();
}
export function taskEffect(fn: Function) {
const { ctx, cleanup } = useTaskContext();
fn();
cleanup();
return ctx;
}
+45 -32
View File
@@ -1,12 +1,13 @@
import { OwlError } from "../common/owl_error";
import { Atom, ExecutionContext } from "../common/types";
import type { App, Env } from "./app"; import type { App, Env } from "./app";
import { BDom, VNode } from "./blockdom"; import { BDom, VNode } from "./blockdom";
import { makeTaskContext, TaskContext } from "./cancellableContext";
import { Component, ComponentConstructor, Props } from "./component"; import { Component, ComponentConstructor, Props } from "./component";
import { fibersInError } from "./error_handling"; import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error";
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers"; import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity"; import { addAtomToContext, reactive, targets, withoutReactivity } from "./reactivity";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { batched, Callback } from "./utils";
let currentNode: ComponentNode | null = null; let currentNode: ComponentNode | null = null;
@@ -42,7 +43,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
// Integration with reactivity system (useState) // Integration with reactivity system (useState)
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>(); // const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
/** /**
* Creates a reactive object that will be observed by the current component. * Creates a reactive object that will be observed by the current component.
* Reading data from the returned object (eg during rendering) will cause the * Reading data from the returned object (eg during rendering) will cause the
@@ -54,15 +55,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
* @see reactive * @see reactive
*/ */
export function useState<T extends object>(state: T): T { export function useState<T extends object>(state: T): T {
const node = getCurrent(); return reactive(state);
let render = batchedRenderFunctions.get(node)!;
if (!render) {
render = batched(node.render.bind(node, false));
batchedRenderFunctions.set(node, render);
// manual implementation of onWillDestroy to break cyclic dependency
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
}
return reactive(state, render);
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
@@ -96,6 +89,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
willPatch: LifecycleHook[] = []; willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = []; patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = []; willDestroy: LifecycleHook[] = [];
taskContext: TaskContext;
executionContext: ExecutionContext;
constructor( constructor(
C: ComponentConstructor<P, E>, C: ComponentConstructor<P, E>,
@@ -109,6 +104,15 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
this.parent = parent; this.parent = parent;
this.props = props; this.props = props;
this.parentKey = parentKey; this.parentKey = parentKey;
this.taskContext = makeTaskContext();
this.executionContext = {
meta: this,
update: () => {
this.render(false);
},
onReadAtom: (atom: Atom) => addAtomToContext(atom, this.executionContext),
atoms: new Set<Atom>(),
};
const defaultProps = C.defaultProps; const defaultProps = C.defaultProps;
props = Object.assign({}, props); props = Object.assign({}, props);
if (defaultProps) { if (defaultProps) {
@@ -116,16 +120,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
const env = (parent && parent.childEnv) || app.env; const env = (parent && parent.childEnv) || app.env;
this.childEnv = env; this.childEnv = env;
for (const key in props) { // for (const key in props) {
const prop = props[key]; // const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) { // if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop); // props[key] = useState(prop);
} // }
} // }
this.component = new C(props, env, this); this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component }); const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this); this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
withoutReactivity(() => {
this.component.setup(); this.component.setup();
});
currentNode = null; currentNode = null;
} }
@@ -142,7 +148,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
const component = this.component; const component = this.component;
try { try {
await Promise.all(this.willStart.map((f) => f.call(component))); let prom: Promise<any[]>;
withoutReactivity(() => {
prom = Promise.all(this.willStart.map((f) => f.call(component)));
});
await prom!;
} catch (e) { } catch (e) {
this.app.handleError({ node: this, error: e }); this.app.handleError({ node: this, error: e });
return; return;
@@ -258,15 +268,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
} }
currentNode = this; currentNode = this;
for (const key in props) { // for (const key in props) {
const prop = props[key]; // const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) { // if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop); // props[key] = useState(prop);
} // }
} // }
currentNode = null; currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props))); let prom: Promise<any[]>;
await prom; withoutReactivity(() => {
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
});
await prom!;
if (fiber !== this.fiber) { if (fiber !== this.fiber) {
return; return;
} }
@@ -384,8 +397,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
return this.component.constructor.name; return this.component.constructor.name;
} }
get subscriptions(): ReturnType<typeof getSubscriptions> { // get subscriptions(): ReturnType<typeof getSubscriptions> {
const render = batchedRenderFunctions.get(this); // const render = batchedRenderFunctions.get(this);
return render ? getSubscriptions(render) : []; // return render ? getSubscriptions(render) : [];
} // }
} }
+26
View File
@@ -0,0 +1,26 @@
import { ExecutionContext } from "../common/types";
export const executionContexts: ExecutionContext[] = [];
(window as any).executionContexts = executionContexts;
// export const scheduledContexts: Set<ExecutionContext> = new Set();
export function getExecutionContext() {
return executionContexts[executionContexts.length - 1];
}
export function pushExecutionContext(context: ExecutionContext) {
executionContexts.push(context);
}
export function popExecutionContext() {
executionContexts.pop();
}
// export function makeExecutionContext({ update, meta }: { update: () => void; meta?: any }) {
// const executionContext: ExecutionContext = {
// update,
// atoms: new Set(),
// meta: meta || {},
// };
// return executionContext;
// }
+6
View File
@@ -3,6 +3,8 @@ import type { ComponentNode } from "./component_node";
import { fibersInError } from "./error_handling"; import { fibersInError } from "./error_handling";
import { OwlError } from "../common/owl_error"; import { OwlError } from "../common/owl_error";
import { STATUS } from "./status"; import { STATUS } from "./status";
import { popTaskContext, pushTaskContext } from "./cancellableContext";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber { export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber; let current = node.fiber;
@@ -133,12 +135,16 @@ export class Fiber {
const node = this.node; const node = this.node;
const root = this.root; const root = this.root;
if (root) { if (root) {
pushTaskContext(node.taskContext);
pushExecutionContext(node.executionContext);
try { try {
(this.bdom as any) = true; (this.bdom as any) = true;
this.bdom = node.renderFn(); this.bdom = node.renderFn();
} catch (e) { } catch (e) {
node.app.handleError({ node, error: e }); node.app.handleError({ node, error: e });
} }
popExecutionContext();
popTaskContext();
root.setCounter(root.counter - 1); root.setCounter(root.counter - 1);
} }
} }
+27 -5
View File
@@ -1,5 +1,6 @@
import type { Env } from "./app"; import type { Env } from "./app";
import { getCurrent } from "./component_node"; import { getCurrent } from "./component_node";
import { popExecutionContext, pushExecutionContext } from "./executionContext";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks"; import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { inOwnerDocument } from "./utils"; import { inOwnerDocument } from "./utils";
@@ -86,22 +87,43 @@ export function useEffect<T extends unknown[]>(
effect: Effect<T>, effect: Effect<T>,
computeDependencies: () => [...T] = () => [NaN] as never computeDependencies: () => [...T] = () => [NaN] as never
) { ) {
const context = getCurrent().component.__owl__.executionContext;
let cleanup: (() => void) | void; let cleanup: (() => void) | void;
let dependencies: T; let dependencies: T;
onMounted(() => {
dependencies = computeDependencies(); const runEffect = () => {
pushExecutionContext(context);
try {
cleanup = effect(...dependencies); cleanup = effect(...dependencies);
} finally {
popExecutionContext();
}
};
const computeDependenciesWithContext = () => {
pushExecutionContext(context);
let r: any;
try {
r = computeDependencies();
} finally {
popExecutionContext();
}
return r;
};
onMounted(() => {
dependencies = computeDependenciesWithContext();
runEffect();
}); });
onPatched(() => { onPatched(() => {
const newDeps = computeDependencies(); const newDeps = computeDependenciesWithContext();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]); const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
if (shouldReapply) { if (shouldReapply) {
dependencies = newDeps; dependencies = newDeps;
if (cleanup) { if (cleanup) {
cleanup(); cleanup();
} }
cleanup = effect(...dependencies); runEffect();
} }
}); });
+1 -1
View File
@@ -39,7 +39,7 @@ export { Component } from "./component";
export type { ComponentConstructor } from "./component"; export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node"; export { useComponent, useState } from "./component_node";
export { status } from "./status"; export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity"; export { reactive, markRaw, toRaw, effect, withoutReactivity } from "./reactivity";
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks"; export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils"; export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
export { export {
+270 -141
View File
@@ -1,13 +1,9 @@
import type { Callback } from "./utils";
import { OwlError } from "../common/owl_error"; import { OwlError } from "../common/owl_error";
import { ExecutionContext, Atom, DerivedAtom, OldValue } from "../common/types";
import { getExecutionContext, popExecutionContext, pushExecutionContext } from "./executionContext";
// Special key to subscribe to, to be notified of key creation/deletion // Special key to subscribe to, to be notified of key creation/deletion
const KEYCHANGES = Symbol("Key changes"); const KEYCHANGES = Symbol("Key changes");
// Used to specify the absence of a callback, can be used as WeakMap key but
// should only be used as a sentinel value and never called.
const NO_CALLBACK = () => {
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
};
// The following types only exist to signify places where objects are expected // The following types only exist to signify places where objects are expected
// to be reactive or not, they provide no type checking benefit over "object" // to be reactive or not, they provide no type checking benefit over "object"
@@ -55,8 +51,8 @@ function canBeMadeReactive(value: any): boolean {
* @param value the value make reactive * @param value the value make reactive
* @returns a reactive for the given object when possible, the original otherwise * @returns a reactive for the given object when possible, the original otherwise
*/ */
function possiblyReactive(val: any, cb: Callback) { function possiblyReactive(val: any) {
return canBeMadeReactive(val) ? reactive(val, cb) : val; return canBeMadeReactive(val) ? reactive(val) : val;
} }
const skipped = new WeakSet<Target>(); const skipped = new WeakSet<Target>();
@@ -81,7 +77,37 @@ export function toRaw<T extends Target, U extends Reactive<T>>(value: U | T): T
return targets.has(value) ? (targets.get(value) as T) : value; return targets.has(value) ? (targets.get(value) as T) : value;
} }
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>(); const targetToKeysToAtomItem = new WeakMap<Target, Map<PropertyKey, Atom>>();
const scheduledAtoms = new Set<Atom>();
function makeAtom(getValue: () => any): Atom {
const atom: Atom = {
executionContexts: new Set<ExecutionContext>(),
dependents: new Set<Atom>(),
// getValue,
};
return atom;
}
function getTargetKeyAtom(target: Target, key: PropertyKey): Atom {
let keyToAtomItem: Map<PropertyKey, Atom> = targetToKeysToAtomItem.get(target)!;
if (!keyToAtomItem) {
keyToAtomItem = new Map();
targetToKeysToAtomItem.set(target, keyToAtomItem);
}
let atom = keyToAtomItem.get(key)!;
if (!atom) {
atom = makeAtom(() => Reflect.get(target, key));
keyToAtomItem.set(key, atom);
}
return atom;
}
export function addAtomToContext(atom: Atom, executionContext: ExecutionContext) {
executionContext.atoms.add(atom);
atom.executionContexts.add(executionContext);
}
/** /**
* Observes a given key on a target with an callback. The callback will be * Observes a given key on a target with an callback. The callback will be
* called when the given key changes on the target. * called when the given key changes on the target.
@@ -91,23 +117,73 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
* or deletion) * or deletion)
* @param callback the function to call when the key changes * @param callback the function to call when the key changes
*/ */
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void { function onReadTargetKey(target: Target, key: PropertyKey, receiver: any): void {
if (callback === NO_CALLBACK) { const executionContext = getExecutionContext();
return; executionContext?.onReadAtom(getTargetKeyAtom(target, key));
}
if (!targetToKeysToCallbacks.get(target)) {
targetToKeysToCallbacks.set(target, new Map());
}
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
if (!keyToCallbacks.get(key)) {
keyToCallbacks.set(key, new Set());
}
keyToCallbacks.get(key)!.add(callback);
if (!callbacksToTargets.has(callback)) {
callbacksToTargets.set(callback, new Set());
}
callbacksToTargets.get(callback)!.add(target);
} }
let scheduled = false;
function scheduleAtom(atom: Atom) {
scheduledAtoms.add(atom);
// batched(processAtoms)();
if (scheduled) return;
scheduled = true;
Promise.resolve().then(() => {
scheduled = false;
processAtoms();
});
}
function processDerivedAtoms() {
const processedAtoms = new Set<Atom>();
for (const atom of scheduledAtoms) {
for (const dep of atom.dependents) {
if (processedAtoms.has(dep)) continue;
dep.computed = false;
processedAtoms.add(dep);
}
}
}
function processAtoms() {
processDerivedAtoms();
const scheduledContexts = new Set(
[...scheduledAtoms.values()].map((s) => [...s.executionContexts]).flat()
);
// schedule before context.update in case there is write operations during update
// todo: add a test in case there is write operations during update the test
// will break is scheduledAtoms.clear(); is called after context.update();
// that writes
scheduledAtoms.clear();
for (const ctx of [...scheduledContexts]) {
removeAtomsFromContext(ctx);
// custom unsubscribe depending on the context.
// scheduledContexts might be updated while we're iterating over it.
ctx.unsubcribe?.(scheduledContexts);
}
for (const context of scheduledContexts) {
pushExecutionContext(context);
try {
context.update?.();
} finally {
popExecutionContext();
}
}
}
/**
* Notify Reactives that are observing a given target that a key has changed on
}
});
};
for (const context of executionContexts) {
context.update();
}
}
/** /**
* Notify Reactives that are observing a given target that a key has changed on * Notify Reactives that are observing a given target that a key has changed on
* the target. * the target.
@@ -117,66 +193,21 @@ function observeTargetKey(target: Target, key: PropertyKey, callback: Callback):
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created * @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
* or deleted) * or deleted)
*/ */
function notifyReactives(target: Target, key: PropertyKey): void { function onWriteTargetKey(target: Target, key: PropertyKey): void {
const keyToCallbacks = targetToKeysToCallbacks.get(target); const keyToAtomItem = targetToKeysToAtomItem.get(target)!;
if (!keyToCallbacks) { if (!keyToAtomItem) {
return; return;
} }
const callbacks = keyToCallbacks.get(key); const atom = keyToAtomItem.get(key);
if (!callbacks) { if (!atom) {
return; return;
} }
// Loop on copy because clearReactivesForCallback will modify the set in place scheduleAtom(atom);
for (const callback of [...callbacks]) {
clearReactivesForCallback(callback);
callback();
}
} }
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
/**
* Clears all subscriptions of the Reactives associated with a given callback.
*
* @param callback the callback for which the reactives need to be cleared
*/
export function clearReactivesForCallback(callback: Callback): void {
const targetsToClear = callbacksToTargets.get(callback);
if (!targetsToClear) {
return;
}
for (const target of targetsToClear) {
const observedKeys = targetToKeysToCallbacks.get(target);
if (!observedKeys) {
continue;
}
for (const [key, callbacks] of observedKeys.entries()) {
callbacks.delete(callback);
if (!callbacks.size) {
observedKeys.delete(key);
}
}
}
targetsToClear.clear();
}
export function getSubscriptions(callback: Callback) {
const targets = callbacksToTargets.get(callback) || [];
return [...targets].map((target) => {
const keysToCallbacks = targetToKeysToCallbacks.get(target);
let keys = [];
if (keysToCallbacks) {
for (const [key, cbs] of keysToCallbacks) {
if (cbs.has(callback)) {
keys.push(key);
}
}
}
return { target, keys };
});
}
// Maps reactive objects to the underlying target // Maps reactive objects to the underlying target
export const targets = new WeakMap<Reactive<Target>, Target>(); export const targets = new WeakMap<Reactive<Target>, Target>();
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>(); const reactiveCache = new WeakMap<Target, Reactive<Target>>();
/** /**
* Creates a reactive proxy for an object. Reading data on the reactive object * Creates a reactive proxy for an object. Reading data on the reactive object
* subscribes to changes to the data. Writing data on the object will cause the * subscribes to changes to the data. Writing data on the object will cause the
@@ -204,7 +235,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
* reactive has changed * reactive has changed
* @returns a proxy that tracks changes to it * @returns a proxy that tracks changes to it
*/ */
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T { export function reactive<T extends Target>(target: T): T {
if (!canBeMadeReactive(target)) { if (!canBeMadeReactive(target)) {
throw new OwlError(`Cannot make the given value reactive`); throw new OwlError(`Cannot make the given value reactive`);
} }
@@ -213,30 +244,130 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
} }
if (targets.has(target)) { if (targets.has(target)) {
// target is reactive, create a reactive on the underlying object instead // target is reactive, create a reactive on the underlying object instead
return reactive(targets.get(target) as T, callback); // return reactive(targets.get(target) as T);
return target;
} }
if (!reactiveCache.has(target)) { const reactive = reactiveCache.get(target)!;
reactiveCache.set(target, new WeakMap()); if (reactive) return reactive as T;
}
const reactivesForTarget = reactiveCache.get(target)!;
if (!reactivesForTarget.has(callback)) {
const targetRawType = rawType(target); const targetRawType = rawType(target);
const handler = COLLECTION_RAW_TYPES.includes(targetRawType) const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType) ? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType)
: basicProxyHandler<T>(callback); : basicProxyHandler<T>();
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>; const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
reactivesForTarget.set(callback, proxy);
reactiveCache.set(target, proxy);
targets.set(proxy, target); targets.set(proxy, target);
}
return reactivesForTarget.get(callback) as Reactive<T>; return proxy;
} }
function removeAtomsFromContext(executionContext: ExecutionContext) {
for (const sig of executionContext.atoms) {
sig.executionContexts.delete(executionContext);
}
executionContext.atoms.clear();
}
/**
* Unsubscribe an execution context and all its children from all atoms
* they are subscribed to.
*
* @param parentExecutionContext the context to unsubscribe
*/
function unsubscribeChildEffect(
parentExecutionContext: ExecutionContext,
scheduledContexts: Set<ExecutionContext>
) {
// executionContext.update = () => {};
for (const children of parentExecutionContext.meta.children) {
children.meta.parent = undefined;
removeAtomsFromContext(children);
scheduledContexts.delete(children);
unsubscribeChildEffect(children, scheduledContexts);
}
parentExecutionContext.meta.children.length = 0;
}
export function withoutReactivity<T extends (...args: any[]) => any>(fn: T): ReturnType<T> {
pushExecutionContext(undefined!);
let r: ReturnType<T>;
try {
r = fn();
} finally {
popExecutionContext();
}
return r;
}
export function effect(fn: Function) {
let parent = getExecutionContext();
// todo: is it useful?
if (parent && !parent?.meta.children) {
parent = undefined!;
}
const executionContext: ExecutionContext = {
unsubcribe: (scheduledContexts: Set<ExecutionContext>) => {
unsubscribeChildEffect(executionContext, scheduledContexts);
},
update: fn,
onReadAtom: (atom: Atom) => addAtomToContext(atom, executionContext),
atoms: new Set(),
meta: {
parent: parent,
children: [],
},
};
if (parent) {
// todo: is it useful?
parent.meta.children?.push?.(executionContext);
}
pushExecutionContext(executionContext);
try {
fn();
} finally {
popExecutionContext();
}
}
export function derived(fn: Function) {
let lastValue: any;
const derivedAtom: DerivedAtom = {
executionContexts: new Set<ExecutionContext>(),
dependents: new Set<Atom>(),
dependencies: new Map<Atom, OldValue>(),
getValue: () => lastValue,
computed: false,
};
return () => {
const executionContext = getExecutionContext();
executionContext?.onReadAtom(derivedAtom);
if (derivedAtom.computed) return lastValue;
const derivedExecutionContext: ExecutionContext = {
onReadAtom: (atom: Atom) => {
atom.dependents.add(derivedAtom);
// derivedAtom.executionContexts.add(executionContext);
},
};
pushExecutionContext(derivedExecutionContext);
try {
lastValue = fn();
} finally {
popExecutionContext();
}
derivedAtom.computed = true;
return lastValue;
};
}
/** /**
* Creates a basic proxy handler for regular objects and arrays. * Creates a basic proxy handler for regular objects and arrays.
* *
* @param callback @see reactive * @param callback @see reactive
* @returns a proxy handler object * @returns a proxy handler object
*/ */
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> { function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
return { return {
get(target, key, receiver) { get(target, key, receiver) {
// non-writable non-configurable properties cannot be made reactive // non-writable non-configurable properties cannot be made reactive
@@ -244,15 +375,15 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
if (desc && !desc.writable && !desc.configurable) { if (desc && !desc.writable && !desc.configurable) {
return Reflect.get(target, key, receiver); return Reflect.get(target, key, receiver);
} }
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(Reflect.get(target, key, receiver), callback); return possiblyReactive(Reflect.get(target, key, receiver));
}, },
set(target, key, value, receiver) { set(target, key, value, receiver) {
const hadKey = objectHasOwnProperty.call(target, key); const hadKey = objectHasOwnProperty.call(target, key);
const originalValue = Reflect.get(target, key, receiver); const originalValue = Reflect.get(target, key, receiver);
const ret = Reflect.set(target, key, toRaw(value), receiver); const ret = Reflect.set(target, key, toRaw(value), receiver);
if (!hadKey && objectHasOwnProperty.call(target, key)) { if (!hadKey && objectHasOwnProperty.call(target, key)) {
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
} }
// While Array length may trigger the set trap, it's not actually set by this // While Array length may trigger the set trap, it's not actually set by this
// method but is updated behind the scenes, and the trap is not called with the // method but is updated behind the scenes, and the trap is not called with the
@@ -261,26 +392,26 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
originalValue !== Reflect.get(target, key, receiver) || originalValue !== Reflect.get(target, key, receiver) ||
(key === "length" && Array.isArray(target)) (key === "length" && Array.isArray(target))
) { ) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
return ret; return ret;
}, },
deleteProperty(target, key) { deleteProperty(target, key) {
const ret = Reflect.deleteProperty(target, key); const ret = Reflect.deleteProperty(target, key);
// TODO: only notify when something was actually deleted // TODO: only notify when something was actually deleted
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
notifyReactives(target, key); onWriteTargetKey(target, key);
return ret; return ret;
}, },
ownKeys(target) { ownKeys(target) {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return Reflect.ownKeys(target); return Reflect.ownKeys(target);
}, },
has(target, key) { has(target, key) {
// TODO: this observes all key changes instead of only the presence of the argument key // TODO: this observes all key changes instead of only the presence of the argument key
// observing the key itself would observe value changes instead of presence changes // observing the key itself would observe value changes instead of presence changes
// so we may need a finer grained system to distinguish observing value vs presence. // so we may need a finer grained system to distinguish observing value vs presence.
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return Reflect.has(target, key); return Reflect.has(target, key);
}, },
} as ProxyHandler<T>; } as ProxyHandler<T>;
@@ -293,11 +424,11 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
* @param target @see reactive * @param target @see reactive
* @param callback @see reactive * @param callback @see reactive
*/ */
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) { function makeKeyObserver(methodName: "has" | "get", target: any) {
return (key: any) => { return (key: any) => {
key = toRaw(key); key = toRaw(key);
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(target[methodName](key), callback); return possiblyReactive(target[methodName](key));
}; };
} }
/** /**
@@ -310,16 +441,15 @@ function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callb
*/ */
function makeIteratorObserver( function makeIteratorObserver(
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator, methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
target: any, target: any
callback: Callback
) { ) {
return function* () { return function* () {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
const keys = target.keys(); const keys = target.keys();
for (const item of target[methodName]()) { for (const item of target[methodName]()) {
const key = keys.next().value; const key = keys.next().value;
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
yield possiblyReactive(item, callback); yield possiblyReactive(item);
} }
}; };
} }
@@ -331,16 +461,16 @@ function makeIteratorObserver(
* @param target @see reactive * @param target @see reactive
* @param callback @see reactive * @param callback @see reactive
*/ */
function makeForEachObserver(target: any, callback: Callback) { function makeForEachObserver(target: any) {
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) { return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
target.forEach(function (val: any, key: any, targetObj: any) { target.forEach(function (val: any, key: any, targetObj: any) {
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
forEachCb.call( forEachCb.call(
thisArg, thisArg,
possiblyReactive(val, callback), possiblyReactive(val),
possiblyReactive(key, callback), possiblyReactive(key),
possiblyReactive(targetObj, callback) possiblyReactive(targetObj)
); );
}, thisArg); }, thisArg);
}; };
@@ -367,10 +497,10 @@ function delegateAndNotify(
const ret = target[setterName](key, value); const ret = target[setterName](key, value);
const hasKey = target.has(key); const hasKey = target.has(key);
if (hadKey !== hasKey) { if (hadKey !== hasKey) {
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
} }
if (originalValue !== target[getterName](key)) { if (originalValue !== target[getterName](key)) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
return ret; return ret;
}; };
@@ -385,9 +515,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
return () => { return () => {
const allKeys = [...target.keys()]; const allKeys = [...target.keys()];
target.clear(); target.clear();
notifyReactives(target, KEYCHANGES); onWriteTargetKey(target, KEYCHANGES);
for (const key of allKeys) { for (const key of allKeys) {
notifyReactives(target, key); onWriteTargetKey(target, key);
} }
}; };
} }
@@ -399,40 +529,40 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
* reactives that the key which is being added or deleted has been modified. * reactives that the key which is being added or deleted has been modified.
*/ */
const rawTypeToFuncHandlers = { const rawTypeToFuncHandlers = {
Set: (target: any, callback: Callback) => ({ Set: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
add: delegateAndNotify("add", "has", target), add: delegateAndNotify("add", "has", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback), keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target, callback), values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target, callback), entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target, callback), forEach: makeForEachObserver(target),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return target.size; return target.size;
}, },
}), }),
Map: (target: any, callback: Callback) => ({ Map: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target, callback), get: makeKeyObserver("get", target),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
keys: makeIteratorObserver("keys", target, callback), keys: makeIteratorObserver("keys", target),
values: makeIteratorObserver("values", target, callback), values: makeIteratorObserver("values", target),
entries: makeIteratorObserver("entries", target, callback), entries: makeIteratorObserver("entries", target),
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback), [Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
forEach: makeForEachObserver(target, callback), forEach: makeForEachObserver(target),
clear: makeClearNotifier(target), clear: makeClearNotifier(target),
get size() { get size() {
observeTargetKey(target, KEYCHANGES, callback); onReadTargetKey(target, KEYCHANGES);
return target.size; return target.size;
}, },
}), }),
WeakMap: (target: any, callback: Callback) => ({ WeakMap: (target: any) => ({
has: makeKeyObserver("has", target, callback), has: makeKeyObserver("has", target),
get: makeKeyObserver("get", target, callback), get: makeKeyObserver("get", target),
set: delegateAndNotify("set", "get", target), set: delegateAndNotify("set", "get", target),
delete: delegateAndNotify("delete", "has", target), delete: delegateAndNotify("delete", "has", target),
}), }),
@@ -446,20 +576,19 @@ const rawTypeToFuncHandlers = {
*/ */
function collectionsProxyHandler<T extends Collection>( function collectionsProxyHandler<T extends Collection>(
target: T, target: T,
callback: Callback,
targetRawType: CollectionRawType targetRawType: CollectionRawType
): ProxyHandler<T> { ): ProxyHandler<T> {
// TODO: if performance is an issue we can create the special handlers lazily when each // TODO: if performance is an issue we can create the special handlers lazily when each
// property is read. // property is read.
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback); const specialHandlers = rawTypeToFuncHandlers[targetRawType](target);
return Object.assign(basicProxyHandler(callback), { return Object.assign(basicProxyHandler(), {
// FIXME: probably broken when part of prototype chain since we ignore the receiver // FIXME: probably broken when part of prototype chain since we ignore the receiver
get(target: any, key: PropertyKey) { get(target: any, key: PropertyKey) {
if (objectHasOwnProperty.call(specialHandlers, key)) { if (objectHasOwnProperty.call(specialHandlers, key)) {
return (specialHandlers as any)[key]; return (specialHandlers as any)[key];
} }
observeTargetKey(target, key, callback); onReadTargetKey(target, key);
return possiblyReactive(target[key], callback); return possiblyReactive(target[key]);
}, },
}) as ProxyHandler<T>; }) as ProxyHandler<T>;
} }
+72
View File
@@ -0,0 +1,72 @@
import { getTaskContext, TaskContext, useTaskContext } from "./cancellableContext";
export class Task<T = any> {
_promise: Promise<T>;
_ctx?: TaskContext = getTaskContext();
constructor(
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: any) => void) => void,
public _onCancelled?: Function
) {
if (!this._ctx) {
this._promise = new Promise(executor);
return;
}
this._promise = new Promise((resolve, reject) => {
try {
executor(
(value: T | PromiseLike<T>) => {
if (!this._ctx?.isCancelled) resolve(value);
},
(error: any) => {
if (!this._ctx?.isCancelled) reject(error);
}
);
} catch (err) {
if (!this._ctx?.isCancelled) reject(err);
}
});
}
then(onFulfilled: (value: any) => any, onRejected: (error: any) => any) {
if (!this._ctx) return this._promise.then(onFulfilled, onRejected);
return this._promise.then((v) => {
if (this._ctx!.isCancelled) return;
let cleanup: Function;
Promise.resolve().then(() => {
const ctx = useTaskContext(this._ctx);
cleanup = ctx.cleanup;
});
const result = onFulfilled(v);
Promise.resolve().then(() => {
cleanup();
});
return result;
}, onRejected);
}
catch(onRejected: (error: any) => any) {
return this._promise.catch(onRejected);
}
finally(onFinally: () => any) {
return this._promise.finally(onFinally);
}
cancel() {
if (this._onCancelled) {
this._onCancelled();
}
}
get [Symbol.toStringTag]() {
return "Promise";
}
// static all(tasks) {
// return new Task((resolve, reject) => {
// Promise.all(tasks.map((t) => (t instanceof Task ? t._promise : t))).then(resolve, reject);
// });
// }
}
-109
View File
@@ -1,51 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Reactivity: useState concurrent renderings 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['context'][ctx['props'].key].n;
let d2 = ctx['state'].x;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<p><block-child-0/></p>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState concurrent renderings 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
return block1([], [b2]);
}
}"
`;
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = ` exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -155,69 +109,6 @@ exports[`Reactivity: useState parent and children subscribed to same context 2`]
}" }"
`; `;
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let d2 = ctx['contextObj'].b;
return block1([d1, d2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].b;
return block1([d1]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let d1 = ctx['contextObj'].a;
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
return block1([d1], [b2]);
}
}"
`;
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
"function anonymous(bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, component } = bdom;
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
return function template(ctx, node, key = \\"\\") {
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
return block1([], [b2, b3]);
}
}"
`;
exports[`Reactivity: useState two components are updated in parallel 1`] = ` exports[`Reactivity: useState two components are updated in parallel 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -52,6 +52,36 @@ exports[`reactivity in lifecycle Component is automatically subscribed to reacti
}" }"
`; `;
exports[`reactivity in lifecycle an external reactive object should be tracked 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`TestSubComponent\`, true, false, false, []);
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['obj1'].value;
const b2 = comp1({}, key + \`__1\`, node, this, null);
return block1([txt1], [b2]);
}
}"
`;
exports[`reactivity in lifecycle an external reactive object should be tracked 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
let block1 = createBlock(\`<div><block-text-0/></div>\`);
return function template(ctx, node, key = \\"\\") {
let txt1 = ctx['obj2'].value;
return block1([txt1]);
}
}"
`;
exports[`reactivity in lifecycle can use a state hook 1`] = ` exports[`reactivity in lifecycle can use a state hook 1`] = `
"function anonymous(app, bdom, helpers "function anonymous(app, bdom, helpers
) { ) {
@@ -140,39 +170,3 @@ exports[`reactivity in lifecycle state changes in willUnmount do not trigger rer
} }
}" }"
`; `;
exports[`subscriptions subscriptions returns the keys and targets observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['state'].a);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 1`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"state\\"]);
return function template(ctx, node, key = \\"\\") {
const b2 = text(ctx['state'].a);
const b3 = comp1({state: ctx['state']}, key + \`__1\`, node, this, null);
return multi([b2, b3]);
}
}"
`;
exports[`subscriptions subscriptions returns the keys observed by the component 2`] = `
"function anonymous(app, bdom, helpers
) {
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
return function template(ctx, node, key = \\"\\") {
return text(ctx['props'].state.b);
}
}"
`;
+10 -9
View File
@@ -1,27 +1,28 @@
import { App, Component, mount, onWillDestroy } from "../../src"; import { App, Component, mount, onWillDestroy } from "../../src";
import { OwlError } from "../../src/common/owl_error";
import { import {
onError, onError,
onMounted, onMounted,
onPatched, onPatched,
onWillPatch,
onWillStart,
onWillRender,
onRendered, onRendered,
onWillPatch,
onWillRender,
onWillStart,
onWillUnmount, onWillUnmount,
useState, useState,
xml, xml,
} from "../../src/index"; } from "../../src/index";
import { getCurrent } from "../../src/runtime/component_node";
import { import {
logStep, logStep,
makeTestFixture, makeTestFixture,
nextTick,
nextMicroTick,
snapshotEverything,
useLogLifecycle,
nextAppError, nextAppError,
nextMicroTick,
nextTick,
snapshotEverything,
steps, steps,
useLogLifecycle,
} from "../helpers"; } from "../helpers";
import { OwlError } from "../../src/common/owl_error";
let fixture: HTMLElement; let fixture: HTMLElement;
@@ -647,7 +648,7 @@ describe("can catch errors", () => {
setup() { setup() {
onWillStart(() => { onWillStart(() => {
this.state = useState({ value: 2 }); getCurrent();
}); });
} }
} }
-4
View File
@@ -450,14 +450,10 @@ test(".alike suffix in a list", async () => {
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>"); expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
expect(steps.splice(0)).toMatchInlineSnapshot(` expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [ Array [
"Parent:willRender",
"Parent:rendered",
"Todo:willRender", "Todo:willRender",
"Todo:rendered", "Todo:rendered",
"Todo:willPatch", "Todo:willPatch",
"Todo:patched", "Todo:patched",
"Parent:willPatch",
"Parent:patched",
] ]
`); `);
}); });
+1 -1
View File
@@ -702,7 +702,7 @@ describe("props validation", () => {
const app = new App(Parent, { test: true }); const app = new App(Parent, { test: true });
await app.mount(fixture); await app.mount(fixture);
expect(fixture.innerHTML).toBe("12"); expect(fixture.innerHTML).toBe("12");
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]); // expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
}); });
test("props are validated whenever component is updated", async () => { test("props are validated whenever component is updated", async () => {
+38 -43
View File
@@ -2,12 +2,11 @@ import {
Component, Component,
mount, mount,
onPatched, onPatched,
onWillRender,
onWillPatch, onWillPatch,
onWillRender,
onWillUnmount, onWillUnmount,
useState, reactive,
xml, xml,
toRaw,
} from "../../src"; } from "../../src";
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers"; import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
@@ -20,10 +19,36 @@ beforeEach(() => {
}); });
describe("reactivity in lifecycle", () => { describe("reactivity in lifecycle", () => {
test("an external reactive object should be tracked", async () => {
const obj1 = reactive({ value: 1 });
const obj2 = reactive({ value: 100 });
class TestSubComponent extends Component {
obj2 = obj2;
static template = xml`<div>
<t t-esc="obj2.value"/>
</div>`;
}
class TestComponent extends Component {
obj1 = obj1;
static template = xml`<div>
<t t-esc="obj1.value"/>
<TestSubComponent/>
</div>`;
static components = { TestSubComponent };
}
await mount(TestComponent, fixture);
expect(fixture.innerHTML).toBe("<div>1<div>100</div></div>");
obj1.value = 2;
obj2.value = 200;
await nextTick();
expect(fixture.innerHTML).toBe("<div>2<div>200</div></div>");
});
test("can use a state hook", async () => { test("can use a state hook", async () => {
class Counter extends Component { class Counter extends Component {
static template = xml`<div><t t-esc="counter.value"/></div>`; static template = xml`<div><t t-esc="counter.value"/></div>`;
counter = useState({ value: 42 }); counter = reactive({ value: 42 });
} }
const counter = await mount(Counter, fixture); const counter = await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>"); expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -36,7 +61,7 @@ describe("reactivity in lifecycle", () => {
let n = 0; let n = 0;
class Comp extends Component { class Comp extends Component {
static template = xml`<div><t t-esc="state.a"/></div>`; static template = xml`<div><t t-esc="state.a"/></div>`;
state = useState({ a: 5, b: 7 }); state = reactive({ a: 5, b: 7 });
setup() { setup() {
onWillRender(() => n++); onWillRender(() => n++);
} }
@@ -57,7 +82,7 @@ describe("reactivity in lifecycle", () => {
test("can use a state hook on Map", async () => { test("can use a state hook on Map", async () => {
class Counter extends Component { class Counter extends Component {
static template = xml`<div><t t-esc="counter.get('value')"/></div>`; static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
counter = useState(new Map([["value", 42]])); counter = reactive(new Map([["value", 42]]));
} }
const counter = await mount(Counter, fixture); const counter = await mount(Counter, fixture);
expect(fixture.innerHTML).toBe("<div>42</div>"); expect(fixture.innerHTML).toBe("<div>42</div>");
@@ -72,7 +97,7 @@ describe("reactivity in lifecycle", () => {
static template = xml` static template = xml`
<span><t t-esc="props.val"/><t t-esc="state.n"/></span> <span><t t-esc="props.val"/><t t-esc="state.n"/></span>
`; `;
state = useState({ n: 2 }); state = reactive({ n: 2 });
setup() { setup() {
onWillRender(() => { onWillRender(() => {
steps.push("render"); steps.push("render");
@@ -96,7 +121,7 @@ describe("reactivity in lifecycle", () => {
</div> </div>
`; `;
static components = { Child }; static components = { Child };
state = useState({ val: 1, flag: true }); state = reactive({ val: 1, flag: true });
} }
const parent = await mount(Parent, fixture); const parent = await mount(Parent, fixture);
expect(steps).toEqual(["render"]); expect(steps).toEqual(["render"]);
@@ -142,7 +167,7 @@ describe("reactivity in lifecycle", () => {
static template = xml` static template = xml`
<div><t t-esc="state.val"/></div> <div><t t-esc="state.val"/></div>
`; `;
state = useState({ val: 1 }); state = reactive({ val: 1 });
setup() { setup() {
STATE = this.state; STATE = this.state;
onWillRender(() => { onWillRender(() => {
@@ -167,7 +192,7 @@ describe("reactivity in lifecycle", () => {
class Parent extends Component { class Parent extends Component {
static template = xml`<Child t-if="state.renderChild" state="state"/>`; static template = xml`<Child t-if="state.renderChild" state="state"/>`;
static components = { Child }; static components = { Child };
state: any = useState({ renderChild: true, content: { a: 2 } }); state: any = reactive({ renderChild: true, content: { a: 2 } });
setup() { setup() {
useLogLifecycle(); useLogLifecycle();
} }
@@ -205,7 +230,8 @@ describe("reactivity in lifecycle", () => {
`); `);
}); });
test("Component is automatically subscribed to reactive object received as prop", async () => { // todo: unskip it
test.skip("Component is automatically subscribed to reactive object received as prop", async () => {
let childRenderCount = 0; let childRenderCount = 0;
let parentRenderCount = 0; let parentRenderCount = 0;
class Child extends Component { class Child extends Component {
@@ -218,7 +244,7 @@ describe("reactivity in lifecycle", () => {
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`; static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
static components = { Child }; static components = { Child };
obj = { a: 1 }; obj = { a: 1 };
reactiveObj = useState({ b: 2 }); reactiveObj = reactive({ b: 2 });
setup() { setup() {
onWillRender(() => parentRenderCount++); onWillRender(() => parentRenderCount++);
} }
@@ -237,34 +263,3 @@ describe("reactivity in lifecycle", () => {
expect(fixture.innerHTML).toBe("34"); expect(fixture.innerHTML).toBe("34");
}); });
}); });
describe("subscriptions", () => {
test("subscriptions returns the keys and targets observed by the component", async () => {
class Comp extends Component {
static template = xml`<t t-esc="state.a"/>`;
state = useState({ a: 1, b: 2 });
}
const comp = await mount(Comp, fixture);
expect(fixture.innerHTML).toBe("1");
expect(comp.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(comp.state) }]);
});
test("subscriptions returns the keys observed by the component", async () => {
class Child extends Component {
static template = xml`<t t-esc="props.state.b"/>`;
setup() {
child = this;
}
}
let child: Child;
class Parent extends Component {
static template = xml`<t t-esc="state.a"/><Child state="state"/>`;
static components = { Child };
state = useState({ a: 1, b: 2 });
}
const parent = await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("12");
expect(parent.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(parent.state) }]);
expect(child!.__owl__.subscriptions).toEqual([{ keys: ["b"], target: toRaw(parent.state) }]);
});
});
-4
View File
@@ -330,12 +330,8 @@ describe("rendering semantics", () => {
expect(fixture.innerHTML).toBe("444"); expect(fixture.innerHTML).toBe("444");
expect(steps.splice(0)).toMatchInlineSnapshot(` expect(steps.splice(0)).toMatchInlineSnapshot(`
Array [ Array [
"Parent:willRender",
"Parent:rendered",
"Child:willRender", "Child:willRender",
"Child:rendered", "Child:rendered",
"Parent:willPatch",
"Parent:patched",
"Child:willPatch", "Child:willPatch",
"Child:patched", "Child:patched",
] ]
+109
View File
@@ -0,0 +1,109 @@
import { taskEffect } from "../../src/runtime/cancellableContext";
import { Task } from "../../src/runtime/task";
export type Deffered = Promise<any> & {
resolve: (value: any) => void;
reject: (reason: any) => void;
};
interface TaskWithResolvers<T> {
promise: Promise<T>;
resolve: (value: T | PromiseLike<T>) => void;
reject: (reason?: any) => void;
}
let resolvers: Record<string, TaskWithResolvers<string>> = {};
function getTask(id: string) {
const resolver: {
task?: Task<string>;
resolve?: (value: string | PromiseLike<string>) => void;
reject?: (reason?: any) => void;
} = {};
const promise = new Task<string>((res, rej) => {
resolver.resolve = res;
resolver.reject = rej;
});
resolver.task = promise;
resolvers[id] = resolver as TaskWithResolvers<string>;
return promise;
}
function tick() {
return new Promise((r) => setTimeout(r, 0));
}
// const timeoutTask = (ms: number) => new Task((resolve) => setTimeout(() => resolve(ms), ms));
const steps: string[] = [];
function step(msg: string) {
steps.push(msg);
}
function verifySteps(expected: string[]) {
expect(steps).toEqual(expected);
steps.length = 0;
}
afterEach(() => {
resolvers = {};
});
describe("task", () => {
test("should run a task properly", async () => {
taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
resolvers["b"].resolve("b");
await tick();
verifySteps(["b:b"]);
});
test.only("should cancel a task properly", async () => {
const ctx = taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
ctx.cancel();
resolvers["b"].resolve("b");
await tick();
verifySteps([]);
});
test("should run a task with subtasks properly", async () => {
taskEffect(async () => {
let result;
step(`a:begin`);
result = await getTask("a");
step(`a:${result}`);
result = await getTask("b");
step(`b:${result}`);
});
verifySteps(["a:begin"]);
resolvers["a"].resolve("a");
await tick();
verifySteps(["a:a"]);
resolvers["b"].resolve("b");
await tick();
verifySteps(["b:b"]);
});
});
-4
View File
@@ -458,10 +458,8 @@ describe("Portal", () => {
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched", "parent:patched",
"parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched",
]); ]);
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>'); expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
@@ -472,10 +470,8 @@ describe("Portal", () => {
"parent:willPatch", "parent:willPatch",
"child:mounted", "child:mounted",
"parent:patched", "parent:patched",
"parent:willPatch",
"child:willPatch", "child:willPatch",
"child:patched", "child:patched",
"parent:patched",
"parent:willPatch", "parent:willPatch",
"child:willUnmount", "child:willUnmount",
"parent:patched", "parent:patched",
+760 -810
View File
File diff suppressed because it is too large Load Diff