Skip to content
Open

Signal #1700

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10,342 changes: 6,611 additions & 3,731 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
38 changes: 38 additions & 0 deletions signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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


# questions
to batch write in next tick or directly?

# owl component
## todo
- test proper unsubscription

# derived
## todo
- unsubscribe from derived when there is no need to read from them
- improve test
- more assertion within one test
- less test to compress the noise?

# optimization
- fragmented memory
- Entity-Component-System

# future
- worker for computation?
- cap'n web

24 changes: 24 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
export enum ComputationState {
EXECUTED = 0,
STALE = 1,
PENDING = 2,
}

export type Computation<T = any> = {
compute?: () => T;
state: ComputationState;
sources: Set<Atom | Derived<any, any>>;
isDerived?: boolean;
value: T; // for effects, this is the cleanup function
childrenEffect?: Computation[]; // only for effects
};

export type customDirectives = Record<
string,
(node: Element, value: string, modifier: string[]) => void
>;

export type Atom<T = any> = {
value: T;
observers: Set<Computation>;
};

export interface Derived<Prev, Next = Prev> extends Atom<Next>, Computation<Next> {}

export type OldValue = any;
46 changes: 46 additions & 0 deletions src/runtime/cancellableContext.ts
Original file line number Diff line number Diff line change
@@ -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;
}
76 changes: 42 additions & 34 deletions src/runtime/component_node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { OwlError } from "../common/owl_error";
import { Atom, Computation, ComputationState } 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 { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
import { reactive } from "./reactivity";
import { getCurrentComputation, setComputation, withoutReactivity } from "./signals";
import { STATUS } from "./status";
import { batched, Callback } from "./utils";

let currentNode: ComponentNode | null = null;

Expand Down Expand Up @@ -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
Expand All @@ -54,15 +56,7 @@ 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);
return reactive(state);
}

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -96,6 +90,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
willPatch: LifecycleHook[] = [];
patched: LifecycleHook[] = [];
willDestroy: LifecycleHook[] = [];
taskContext: TaskContext;
signalComputation: Computation;

constructor(
C: ComponentConstructor<P, E>,
Expand All @@ -109,23 +105,36 @@ 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.signalComputation = {
// data: this,
value: undefined,
compute: () => {
this.render(false);
},
sources: new Set<Atom>(),
state: ComputationState.EXECUTED,
};
const defaultProps = C.defaultProps;
props = Object.assign({}, props);
if (defaultProps) {
applyDefaultProps(props, defaultProps);
}
const env = (parent && parent.childEnv) || app.env;
this.childEnv = env;
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop);
}
}
// for (const key in props) {
// const prop = props[key];
// if (prop && typeof prop === "object" && targets.has(prop)) {
// props[key] = useState(prop);
// }
// }
const currentContext = getCurrentComputation();
setComputation(this.signalComputation);
this.component = new C(props, env, this);
const ctx = Object.assign(Object.create(this.component), { this: this.component });
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
this.component.setup();
setComputation(currentContext);
currentNode = null;
}

Expand All @@ -142,7 +151,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
}
const component = this.component;
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) {
this.app.handleError({ node: this, error: e });
return;
Expand Down Expand Up @@ -257,16 +270,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
applyDefaultProps(props, defaultProps);
}

currentNode = this;
for (const key in props) {
const prop = props[key];
if (prop && typeof prop === "object" && targets.has(prop)) {
props[key] = useState(prop);
}
}
currentNode = null;
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
await prom;
let prom: Promise<any[]>;
withoutReactivity(() => {
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
});
await prom!;
if (fiber !== this.fiber) {
return;
}
Expand Down Expand Up @@ -384,8 +392,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) : [];
// }
}
26 changes: 26 additions & 0 deletions src/runtime/executionContext.ts
Original file line number Diff line number Diff line change
@@ -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;
// }
18 changes: 12 additions & 6 deletions src/runtime/fibers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { runWithComputation } from "./signals";

export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
let current = node.fiber;
Expand Down Expand Up @@ -133,12 +135,16 @@ export class Fiber {
const node = this.node;
const root = this.root;
if (root) {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
node.app.handleError({ node, error: e });
}
pushTaskContext(node.taskContext);
runWithComputation(node.signalComputation, () => {
try {
(this.bdom as any) = true;
this.bdom = node.renderFn();
} catch (e) {
node.app.handleError({ node, error: e });
}
});
popTaskContext();
root.setCounter(root.counter - 1);
}
}
Expand Down
22 changes: 16 additions & 6 deletions src/runtime/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Env } from "./app";
import { getCurrent } from "./component_node";
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
import { runWithComputation } from "./signals";
import { inOwnerDocument } from "./utils";

// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -86,22 +87,31 @@ export function useEffect<T extends unknown[]>(
effect: Effect<T>,
computeDependencies: () => [...T] = () => [NaN] as never
) {
const context = getCurrent().component.__owl__.signalComputation;

let cleanup: (() => void) | void;
let dependencies: T;

let dependencies: any;
const runEffect = () =>
runWithComputation(context, () => {
cleanup = effect(...dependencies);
});
const computeDependenciesWithContext = () => runWithComputation(context, computeDependencies);

onMounted(() => {
dependencies = computeDependencies();
cleanup = effect(...dependencies);
dependencies = computeDependenciesWithContext();
runEffect();
});

onPatched(() => {
const newDeps = computeDependencies();
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
const newDeps = computeDependenciesWithContext();
const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
if (shouldReapply) {
dependencies = newDeps;
if (cleanup) {
cleanup();
}
cleanup = effect(...dependencies);
runEffect();
}
});

Expand Down
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type { ComponentConstructor } from "./component";
export { useComponent, useState } from "./component_node";
export { status } from "./status";
export { reactive, markRaw, toRaw } from "./reactivity";
export { effect, withoutReactivity } from "./signals";
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
export {
Expand Down
Loading
Loading