mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
up
This commit is contained in:
Generated
+6738
-3858
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -22,7 +22,7 @@
|
||||
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
||||
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
||||
"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",
|
||||
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
|
||||
"playground": "npm run build && npm run playground:serve",
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
export type ExecutionContext = {
|
||||
unsubcribe?: (scheduledContexts: Set<ExecutionContext>) => void;
|
||||
update: Function;
|
||||
signals: Set<Signal>;
|
||||
getParent: () => ExecutionContext | undefined;
|
||||
getChildren: () => ExecutionContext[];
|
||||
meta: any;
|
||||
// schedule: () => void;
|
||||
};
|
||||
|
||||
export type customDirectives = Record<
|
||||
string,
|
||||
(node: Element, value: string, modifier: string[]) => void
|
||||
>;
|
||||
|
||||
export type Signal = {
|
||||
executionContexts: Set<ExecutionContext>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { ExecutionContext } from "../common/types";
|
||||
import type { App, Env } from "./app";
|
||||
import { BDom, VNode } from "./blockdom";
|
||||
import { makeTaskContext, TaskContext } from "./cancellableContext";
|
||||
import { Component, ComponentConstructor, Props } from "./component";
|
||||
import { fibersInError } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { makeExecutionContext } from "./executionContext";
|
||||
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
|
||||
import { reactive, targets } from "./reactivity";
|
||||
import { STATUS } from "./status";
|
||||
import { batched, Callback } from "./utils";
|
||||
|
||||
let currentNode: ComponentNode | null = null;
|
||||
|
||||
@@ -42,7 +44,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
|
||||
// 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.
|
||||
* Reading data from the returned object (eg during rendering) will cause the
|
||||
@@ -54,15 +56,19 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||
* @see reactive
|
||||
*/
|
||||
export function useState<T extends object>(state: T): T {
|
||||
const node = getCurrent();
|
||||
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);
|
||||
// const node = getCurrent();
|
||||
// let render = batchedRenderFunctions.get(node)!;
|
||||
// if (!render) {
|
||||
// render = batched(() => {
|
||||
// debugger;
|
||||
// const r = node.render(false);
|
||||
// return r;
|
||||
// });
|
||||
// batchedRenderFunctions.set(node, render);
|
||||
// // manual implementation of onWillDestroy to break cyclic dependency
|
||||
// node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
||||
// }
|
||||
return reactive(state);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -96,6 +102,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
willPatch: LifecycleHook[] = [];
|
||||
patched: LifecycleHook[] = [];
|
||||
willDestroy: LifecycleHook[] = [];
|
||||
taskContext: TaskContext;
|
||||
executionContext: ExecutionContext;
|
||||
|
||||
constructor(
|
||||
C: ComponentConstructor<P, E>,
|
||||
@@ -109,6 +117,17 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
this.parent = parent;
|
||||
this.props = props;
|
||||
this.parentKey = parentKey;
|
||||
this.taskContext = makeTaskContext();
|
||||
this.executionContext = makeExecutionContext({
|
||||
update: () => {
|
||||
this.render(false);
|
||||
},
|
||||
getParent: () => this.parent?.executionContext,
|
||||
getChildren: () => {
|
||||
return Object.values(this.children).map((c) => c.executionContext);
|
||||
},
|
||||
meta: this,
|
||||
});
|
||||
const defaultProps = C.defaultProps;
|
||||
props = Object.assign({}, props);
|
||||
if (defaultProps) {
|
||||
@@ -384,8 +403,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
||||
return this.component.constructor.name;
|
||||
}
|
||||
|
||||
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||
const render = batchedRenderFunctions.get(this);
|
||||
return render ? getSubscriptions(render) : [];
|
||||
}
|
||||
// get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||
// const render = batchedRenderFunctions.get(this);
|
||||
// return render ? getSubscriptions(render) : [];
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ExecutionContext } from "../common/types";
|
||||
|
||||
export const executionContext: ExecutionContext[] = [];
|
||||
// export const scheduledContexts: Set<ExecutionContext> = new Set();
|
||||
|
||||
export function getExecutionContext() {
|
||||
return executionContext[executionContext.length - 1];
|
||||
}
|
||||
|
||||
export function makeExecutionContext({
|
||||
update,
|
||||
getParent,
|
||||
getChildren,
|
||||
meta,
|
||||
}: {
|
||||
update: () => void;
|
||||
getParent?: () => ExecutionContext | undefined;
|
||||
getChildren?: () => ExecutionContext[];
|
||||
meta?: any;
|
||||
}) {
|
||||
const executionContext: ExecutionContext = {
|
||||
update,
|
||||
getParent: getParent!,
|
||||
getChildren: getChildren!,
|
||||
signals: new Set(),
|
||||
meta: meta || {},
|
||||
};
|
||||
return executionContext;
|
||||
}
|
||||
|
||||
export function pushExecutionContext(context: ExecutionContext) {
|
||||
executionContext.push(context);
|
||||
}
|
||||
|
||||
export function popExecutionContext() {
|
||||
executionContext.pop();
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import type { ComponentNode } from "./component_node";
|
||||
import { fibersInError } from "./error_handling";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { STATUS } from "./status";
|
||||
import { popTaskContext, pushTaskContext } from "./cancellableContext";
|
||||
import { popExecutionContext, pushExecutionContext } from "./executionContext";
|
||||
|
||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||
let current = node.fiber;
|
||||
@@ -133,12 +135,16 @@ export class Fiber {
|
||||
const node = this.node;
|
||||
const root = this.root;
|
||||
if (root) {
|
||||
pushTaskContext(node.taskContext);
|
||||
pushExecutionContext(node.executionContext);
|
||||
try {
|
||||
(this.bdom as any) = true;
|
||||
this.bdom = node.renderFn();
|
||||
} catch (e) {
|
||||
node.app.handleError({ node, error: e });
|
||||
}
|
||||
popExecutionContext();
|
||||
popTaskContext();
|
||||
root.setCounter(root.counter - 1);
|
||||
}
|
||||
}
|
||||
|
||||
+203
-143
@@ -1,13 +1,9 @@
|
||||
import type { Callback } from "./utils";
|
||||
import { OwlError } from "../common/owl_error";
|
||||
import { ExecutionContext, Signal } from "../common/types";
|
||||
import { getExecutionContext, popExecutionContext, pushExecutionContext } from "./executionContext";
|
||||
|
||||
// Special key to subscribe to, to be notified of key creation/deletion
|
||||
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
|
||||
// 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
|
||||
* @returns a reactive for the given object when possible, the original otherwise
|
||||
*/
|
||||
function possiblyReactive(val: any, cb: Callback) {
|
||||
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
||||
function possiblyReactive(val: any) {
|
||||
return canBeMadeReactive(val) ? reactive(val) : val;
|
||||
}
|
||||
|
||||
const skipped = new WeakSet<Target>();
|
||||
@@ -81,7 +77,16 @@ export function toRaw<T extends Target, U extends Reactive<T>>(value: U | T): T
|
||||
return targets.has(value) ? (targets.get(value) as T) : value;
|
||||
}
|
||||
|
||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
|
||||
const targetToKeysToSignalItem = new WeakMap<Target, Map<PropertyKey, Signal>>();
|
||||
const scheduledSignals = new Set<Signal>();
|
||||
|
||||
function makeSignal() {
|
||||
const signal: Signal = {
|
||||
executionContexts: new Set<ExecutionContext>(),
|
||||
};
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a given key on a target with an callback. The callback will be
|
||||
* called when the given key changes on the target.
|
||||
@@ -91,23 +96,69 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
|
||||
* or deletion)
|
||||
* @param callback the function to call when the key changes
|
||||
*/
|
||||
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
||||
if (callback === NO_CALLBACK) {
|
||||
return;
|
||||
function onReadTargetKey(target: Target, key: PropertyKey): void {
|
||||
const executionContext = getExecutionContext();
|
||||
if (!executionContext) return;
|
||||
|
||||
let keyToSignalItem: Map<PropertyKey, Signal> = targetToKeysToSignalItem.get(target)!;
|
||||
if (!keyToSignalItem) {
|
||||
keyToSignalItem = new Map();
|
||||
targetToKeysToSignalItem.set(target, keyToSignalItem);
|
||||
}
|
||||
if (!targetToKeysToCallbacks.get(target)) {
|
||||
targetToKeysToCallbacks.set(target, new Map());
|
||||
let signal = keyToSignalItem.get(key)!;
|
||||
if (!signal) {
|
||||
signal = makeSignal();
|
||||
keyToSignalItem.set(key, signal);
|
||||
}
|
||||
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);
|
||||
// observerSignals.add(signal);
|
||||
executionContext.signals.add(signal);
|
||||
signal.executionContexts.add(executionContext);
|
||||
}
|
||||
|
||||
let scheduled = false;
|
||||
function scheduleSignal(signal: Signal) {
|
||||
scheduledSignals.add(signal);
|
||||
if (scheduled) return;
|
||||
scheduled = true;
|
||||
Promise.resolve().then(() => {
|
||||
scheduled = false;
|
||||
processSignals();
|
||||
});
|
||||
}
|
||||
|
||||
function processSignals() {
|
||||
const scheduledContexts = new Set(
|
||||
[...scheduledSignals.values()].map((s) => [...s.executionContexts]).flat()
|
||||
);
|
||||
|
||||
for (const ctx of [...scheduledContexts]) {
|
||||
removeSignalsFromContext(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();
|
||||
}
|
||||
}
|
||||
scheduledSignals.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the target.
|
||||
@@ -117,66 +168,21 @@ function observeTargetKey(target: Target, key: PropertyKey, callback: Callback):
|
||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||
* or deleted)
|
||||
*/
|
||||
function notifyReactives(target: Target, key: PropertyKey): void {
|
||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
||||
if (!keyToCallbacks) {
|
||||
function onWriteTargetKey(target: Target, key: PropertyKey): void {
|
||||
const keyToSignalItem = targetToKeysToSignalItem.get(target)!;
|
||||
if (!keyToSignalItem) {
|
||||
return;
|
||||
}
|
||||
const callbacks = keyToCallbacks.get(key);
|
||||
if (!callbacks) {
|
||||
const signal = keyToSignalItem.get(key);
|
||||
if (!signal) {
|
||||
return;
|
||||
}
|
||||
// Loop on copy because clearReactivesForCallback will modify the set in place
|
||||
for (const callback of [...callbacks]) {
|
||||
clearReactivesForCallback(callback);
|
||||
callback();
|
||||
}
|
||||
scheduleSignal(signal);
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
* subscribes to changes to the data. Writing data on the object will cause the
|
||||
@@ -204,7 +210,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
|
||||
* reactive has changed
|
||||
* @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)) {
|
||||
throw new OwlError(`Cannot make the given value reactive`);
|
||||
}
|
||||
@@ -213,30 +219,86 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
||||
}
|
||||
if (targets.has(target)) {
|
||||
// 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)) {
|
||||
reactiveCache.set(target, new WeakMap());
|
||||
}
|
||||
const reactivesForTarget = reactiveCache.get(target)!;
|
||||
if (!reactivesForTarget.has(callback)) {
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
||||
: basicProxyHandler<T>(callback);
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
reactivesForTarget.set(callback, proxy);
|
||||
targets.set(proxy, target);
|
||||
}
|
||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
||||
const reactive = reactiveCache.get(target)!;
|
||||
if (reactive) return reactive as T;
|
||||
|
||||
const targetRawType = rawType(target);
|
||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||
? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType)
|
||||
: basicProxyHandler<T>();
|
||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||
|
||||
reactiveCache.set(target, proxy);
|
||||
targets.set(proxy, target);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
function removeSignalsFromContext(executionContext: ExecutionContext) {
|
||||
for (const sig of executionContext.signals) {
|
||||
sig.executionContexts.delete(executionContext);
|
||||
}
|
||||
executionContext.signals.clear();
|
||||
}
|
||||
/**
|
||||
* Unsubscribe an execution context and all its children from all signals
|
||||
* they are subscribed to.
|
||||
*
|
||||
* @param executionContext the context to unsubscribe
|
||||
*/
|
||||
function unsubscribeChildEffect(
|
||||
executionContext: ExecutionContext,
|
||||
scheduledContexts: Set<ExecutionContext>
|
||||
) {
|
||||
// executionContext.update = () => {};
|
||||
|
||||
for (const children of executionContext.meta.children) {
|
||||
children.meta.parent = undefined;
|
||||
removeSignalsFromContext(children);
|
||||
scheduledContexts.delete(children);
|
||||
unsubscribeChildEffect(children, scheduledContexts);
|
||||
}
|
||||
executionContext.meta.children.length = 0;
|
||||
}
|
||||
export function effect(fn: Function) {
|
||||
const parent = getExecutionContext();
|
||||
const executionContext: ExecutionContext = {
|
||||
unsubcribe: (scheduledContexts: Set<ExecutionContext>) => {
|
||||
unsubscribeChildEffect(executionContext, scheduledContexts);
|
||||
},
|
||||
update: fn,
|
||||
getParent: () => {
|
||||
return executionContext.meta.parent;
|
||||
},
|
||||
getChildren: () => {
|
||||
return executionContext.meta.children || [];
|
||||
},
|
||||
signals: new Set(),
|
||||
meta: {
|
||||
parent: getExecutionContext(),
|
||||
children: [],
|
||||
},
|
||||
};
|
||||
if (parent) {
|
||||
parent.meta.children.push(executionContext);
|
||||
}
|
||||
pushExecutionContext(executionContext);
|
||||
try {
|
||||
fn();
|
||||
} finally {
|
||||
popExecutionContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a basic proxy handler for regular objects and arrays.
|
||||
*
|
||||
* @param callback @see reactive
|
||||
* @returns a proxy handler object
|
||||
*/
|
||||
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
||||
function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
|
||||
return {
|
||||
get(target, key, receiver) {
|
||||
// non-writable non-configurable properties cannot be made reactive
|
||||
@@ -244,15 +306,15 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
||||
if (desc && !desc.writable && !desc.configurable) {
|
||||
return Reflect.get(target, key, receiver);
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(Reflect.get(target, key, receiver), callback);
|
||||
onReadTargetKey(target, key);
|
||||
return possiblyReactive(Reflect.get(target, key, receiver));
|
||||
},
|
||||
set(target, key, value, receiver) {
|
||||
const hadKey = objectHasOwnProperty.call(target, key);
|
||||
const originalValue = Reflect.get(target, key, receiver);
|
||||
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||||
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
|
||||
// method but is updated behind the scenes, and the trap is not called with the
|
||||
@@ -261,26 +323,26 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
||||
originalValue !== Reflect.get(target, key, receiver) ||
|
||||
(key === "length" && Array.isArray(target))
|
||||
) {
|
||||
notifyReactives(target, key);
|
||||
onWriteTargetKey(target, key);
|
||||
}
|
||||
return ret;
|
||||
},
|
||||
deleteProperty(target, key) {
|
||||
const ret = Reflect.deleteProperty(target, key);
|
||||
// TODO: only notify when something was actually deleted
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
notifyReactives(target, key);
|
||||
onWriteTargetKey(target, KEYCHANGES);
|
||||
onWriteTargetKey(target, key);
|
||||
return ret;
|
||||
},
|
||||
ownKeys(target) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
onReadTargetKey(target, KEYCHANGES);
|
||||
return Reflect.ownKeys(target);
|
||||
},
|
||||
has(target, 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
|
||||
// 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);
|
||||
},
|
||||
} as ProxyHandler<T>;
|
||||
@@ -293,11 +355,11 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
||||
* @param target @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) => {
|
||||
key = toRaw(key);
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[methodName](key), callback);
|
||||
onReadTargetKey(target, key);
|
||||
return possiblyReactive(target[methodName](key));
|
||||
};
|
||||
}
|
||||
/**
|
||||
@@ -310,16 +372,15 @@ function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callb
|
||||
*/
|
||||
function makeIteratorObserver(
|
||||
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
||||
target: any,
|
||||
callback: Callback
|
||||
target: any
|
||||
) {
|
||||
return function* () {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
onReadTargetKey(target, KEYCHANGES);
|
||||
const keys = target.keys();
|
||||
for (const item of target[methodName]()) {
|
||||
const key = keys.next().value;
|
||||
observeTargetKey(target, key, callback);
|
||||
yield possiblyReactive(item, callback);
|
||||
onReadTargetKey(target, key);
|
||||
yield possiblyReactive(item);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -331,16 +392,16 @@ function makeIteratorObserver(
|
||||
* @param target @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) {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
onReadTargetKey(target, KEYCHANGES);
|
||||
target.forEach(function (val: any, key: any, targetObj: any) {
|
||||
observeTargetKey(target, key, callback);
|
||||
onReadTargetKey(target, key);
|
||||
forEachCb.call(
|
||||
thisArg,
|
||||
possiblyReactive(val, callback),
|
||||
possiblyReactive(key, callback),
|
||||
possiblyReactive(targetObj, callback)
|
||||
possiblyReactive(val),
|
||||
possiblyReactive(key),
|
||||
possiblyReactive(targetObj)
|
||||
);
|
||||
}, thisArg);
|
||||
};
|
||||
@@ -367,10 +428,10 @@ function delegateAndNotify(
|
||||
const ret = target[setterName](key, value);
|
||||
const hasKey = target.has(key);
|
||||
if (hadKey !== hasKey) {
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
onWriteTargetKey(target, KEYCHANGES);
|
||||
}
|
||||
if (originalValue !== target[getterName](key)) {
|
||||
notifyReactives(target, key);
|
||||
onWriteTargetKey(target, key);
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
@@ -385,9 +446,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
||||
return () => {
|
||||
const allKeys = [...target.keys()];
|
||||
target.clear();
|
||||
notifyReactives(target, KEYCHANGES);
|
||||
onWriteTargetKey(target, KEYCHANGES);
|
||||
for (const key of allKeys) {
|
||||
notifyReactives(target, key);
|
||||
onWriteTargetKey(target, key);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -399,40 +460,40 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
||||
* reactives that the key which is being added or deleted has been modified.
|
||||
*/
|
||||
const rawTypeToFuncHandlers = {
|
||||
Set: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
Set: (target: any) => ({
|
||||
has: makeKeyObserver("has", target),
|
||||
add: delegateAndNotify("add", "has", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
forEach: makeForEachObserver(target, callback),
|
||||
keys: makeIteratorObserver("keys", target),
|
||||
values: makeIteratorObserver("values", target),
|
||||
entries: makeIteratorObserver("entries", target),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
|
||||
forEach: makeForEachObserver(target),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
onReadTargetKey(target, KEYCHANGES);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
Map: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
Map: (target: any) => ({
|
||||
has: makeKeyObserver("has", target),
|
||||
get: makeKeyObserver("get", target),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
keys: makeIteratorObserver("keys", target, callback),
|
||||
values: makeIteratorObserver("values", target, callback),
|
||||
entries: makeIteratorObserver("entries", target, callback),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
||||
forEach: makeForEachObserver(target, callback),
|
||||
keys: makeIteratorObserver("keys", target),
|
||||
values: makeIteratorObserver("values", target),
|
||||
entries: makeIteratorObserver("entries", target),
|
||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
|
||||
forEach: makeForEachObserver(target),
|
||||
clear: makeClearNotifier(target),
|
||||
get size() {
|
||||
observeTargetKey(target, KEYCHANGES, callback);
|
||||
onReadTargetKey(target, KEYCHANGES);
|
||||
return target.size;
|
||||
},
|
||||
}),
|
||||
WeakMap: (target: any, callback: Callback) => ({
|
||||
has: makeKeyObserver("has", target, callback),
|
||||
get: makeKeyObserver("get", target, callback),
|
||||
WeakMap: (target: any) => ({
|
||||
has: makeKeyObserver("has", target),
|
||||
get: makeKeyObserver("get", target),
|
||||
set: delegateAndNotify("set", "get", target),
|
||||
delete: delegateAndNotify("delete", "has", target),
|
||||
}),
|
||||
@@ -446,20 +507,19 @@ const rawTypeToFuncHandlers = {
|
||||
*/
|
||||
function collectionsProxyHandler<T extends Collection>(
|
||||
target: T,
|
||||
callback: Callback,
|
||||
targetRawType: CollectionRawType
|
||||
): ProxyHandler<T> {
|
||||
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||||
// property is read.
|
||||
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
||||
return Object.assign(basicProxyHandler(callback), {
|
||||
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target);
|
||||
return Object.assign(basicProxyHandler(), {
|
||||
// FIXME: probably broken when part of prototype chain since we ignore the receiver
|
||||
get(target: any, key: PropertyKey) {
|
||||
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||||
return (specialHandlers as any)[key];
|
||||
}
|
||||
observeTargetKey(target, key, callback);
|
||||
return possiblyReactive(target[key], callback);
|
||||
onReadTargetKey(target, key);
|
||||
return possiblyReactive(target[key]);
|
||||
},
|
||||
}) as ProxyHandler<T>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
// 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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component is inactive 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2;
|
||||
if (ctx['state'].flag) {
|
||||
b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState destroyed component is inactive 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState one components can subscribe twice to same context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/><block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj1'].a;
|
||||
let txt2 = ctx['contextObj2'].b;
|
||||
return block1([txt1, txt2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState parent and children subscribed to same context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
let txt1 = ctx['contextObj'].b;
|
||||
return block1([txt1], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState parent and children subscribed to same context 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].a;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components are updated in parallel 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components can subscribe to same context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two components can subscribe to same context 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`Parent\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
const b3 = comp2({}, key + \`__2\`, node, this, null);
|
||||
return block1([], [b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState two independent components on different levels are updated in parallel 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useContext=useState hook is reactive, for one component 1`] = `
|
||||
"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['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useless atoms should be deleted 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { prepareList, withKey } = helpers;
|
||||
const comp1 = app.createComponent(\`Quantity\`, true, false, false, [\\"id\\"]);
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/> Total: <block-text-0/> Count: <block-text-1/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
ctx = Object.create(ctx);
|
||||
const [k_block2, v_block2, l_block2, c_block2] = prepareList(Object.keys(ctx['state']));;
|
||||
for (let i1 = 0; i1 < l_block2; i1++) {
|
||||
ctx[\`id\`] = k_block2[i1];
|
||||
const key1 = ctx['id'];
|
||||
c_block2[i1] = withKey(comp1({id: ctx['id']}, key + \`__1__\${key1}\`, node, this, null), key1);
|
||||
}
|
||||
ctx = ctx.__proto__;
|
||||
const b2 = list(c_block2);
|
||||
let txt1 = ctx['total'];
|
||||
let txt2 = Object.keys(ctx['state']).length;
|
||||
return block1([txt1, txt2], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState useless atoms should be deleted 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['state'].quantity;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`Reactivity: useState very simple use, with initial value 1`] = `
|
||||
"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['contextObj'].value;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
@@ -702,7 +702,7 @@ describe("props validation", () => {
|
||||
const app = new App(Parent, { test: true });
|
||||
await app.mount(fixture);
|
||||
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 () => {
|
||||
|
||||
@@ -2,12 +2,11 @@ import {
|
||||
Component,
|
||||
mount,
|
||||
onPatched,
|
||||
onWillRender,
|
||||
onWillPatch,
|
||||
onWillRender,
|
||||
onWillUnmount,
|
||||
useState,
|
||||
reactive,
|
||||
xml,
|
||||
toRaw,
|
||||
} from "../../src";
|
||||
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
||||
|
||||
@@ -20,10 +19,36 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
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 () => {
|
||||
class Counter extends Component {
|
||||
static template = xml`<div><t t-esc="counter.value"/></div>`;
|
||||
counter = useState({ value: 42 });
|
||||
counter = reactive({ value: 42 });
|
||||
}
|
||||
const counter = await mount(Counter, fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||
@@ -36,7 +61,7 @@ describe("reactivity in lifecycle", () => {
|
||||
let n = 0;
|
||||
class Comp extends Component {
|
||||
static template = xml`<div><t t-esc="state.a"/></div>`;
|
||||
state = useState({ a: 5, b: 7 });
|
||||
state = reactive({ a: 5, b: 7 });
|
||||
setup() {
|
||||
onWillRender(() => n++);
|
||||
}
|
||||
@@ -57,7 +82,7 @@ describe("reactivity in lifecycle", () => {
|
||||
test("can use a state hook on Map", async () => {
|
||||
class Counter extends Component {
|
||||
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);
|
||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||
@@ -72,7 +97,7 @@ describe("reactivity in lifecycle", () => {
|
||||
static template = xml`
|
||||
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
||||
`;
|
||||
state = useState({ n: 2 });
|
||||
state = reactive({ n: 2 });
|
||||
setup() {
|
||||
onWillRender(() => {
|
||||
steps.push("render");
|
||||
@@ -96,7 +121,7 @@ describe("reactivity in lifecycle", () => {
|
||||
</div>
|
||||
`;
|
||||
static components = { Child };
|
||||
state = useState({ val: 1, flag: true });
|
||||
state = reactive({ val: 1, flag: true });
|
||||
}
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect(steps).toEqual(["render"]);
|
||||
@@ -142,7 +167,7 @@ describe("reactivity in lifecycle", () => {
|
||||
static template = xml`
|
||||
<div><t t-esc="state.val"/></div>
|
||||
`;
|
||||
state = useState({ val: 1 });
|
||||
state = reactive({ val: 1 });
|
||||
setup() {
|
||||
STATE = this.state;
|
||||
onWillRender(() => {
|
||||
@@ -167,7 +192,7 @@ describe("reactivity in lifecycle", () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<Child t-if="state.renderChild" state="state"/>`;
|
||||
static components = { Child };
|
||||
state: any = useState({ renderChild: true, content: { a: 2 } });
|
||||
state: any = reactive({ renderChild: true, content: { a: 2 } });
|
||||
setup() {
|
||||
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 parentRenderCount = 0;
|
||||
class Child extends Component {
|
||||
@@ -218,7 +244,7 @@ describe("reactivity in lifecycle", () => {
|
||||
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
|
||||
static components = { Child };
|
||||
obj = { a: 1 };
|
||||
reactiveObj = useState({ b: 2 });
|
||||
reactiveObj = reactive({ b: 2 });
|
||||
setup() {
|
||||
onWillRender(() => parentRenderCount++);
|
||||
}
|
||||
@@ -237,34 +263,3 @@ describe("reactivity in lifecycle", () => {
|
||||
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) }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
});
|
||||
+918
-968
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user