Skip to content
Open

Signal #1700

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c74c66a
up
Goaman Sep 26, 2025
8d12bf1
comment
Goaman Sep 26, 2025
adb9d40
up
Goaman Sep 26, 2025
2258066
up
Goaman Sep 26, 2025
836fdad
up
Goaman Sep 26, 2025
e31d195
up
Goaman Sep 26, 2025
4385969
up
Goaman Sep 26, 2025
0c1f9f3
fix for dropdown
Goaman Sep 28, 2025
ed9db6e
withoutReactivity
Goaman Sep 29, 2025
6f2600b
md
Goaman Sep 29, 2025
38574a1
no reactivity in setup
Goaman Sep 29, 2025
1ddf2ce
up
Goaman Oct 1, 2025
a309ec4
up
Goaman Oct 1, 2025
d0cdc48
up
Goaman Oct 2, 2025
00f8b21
up
Goaman Oct 6, 2025
5e1adf0
up
Goaman Oct 6, 2025
9d78c0b
up
Goaman Oct 6, 2025
e32dd50
up
Goaman Oct 6, 2025
704630b
up
Goaman Oct 6, 2025
3d080ff
up
Goaman Oct 7, 2025
a4bd6cf
reorganise
Goaman Oct 7, 2025
69cdf18
up
Goaman Oct 7, 2025
f505173
up
Goaman Oct 7, 2025
76884c6
up
Goaman Oct 7, 2025
cf018d3
up
Goaman Oct 7, 2025
f0d9a98
up
Goaman Oct 7, 2025
0066523
up
Goaman Oct 7, 2025
458817d
up
Goaman Oct 8, 2025
d2d3ac5
up
Goaman Oct 8, 2025
dde51e0
up
Goaman Oct 8, 2025
a507d62
up
Goaman Oct 8, 2025
83e438d
up
Goaman Oct 8, 2025
af7c21b
up
Goaman Oct 8, 2025
b731fef
up
Goaman Oct 9, 2025
8fd890c
up
Goaman Oct 9, 2025
c1c6310
up
Goaman Oct 9, 2025
ac0c8eb
up
Goaman Oct 13, 2025
75a08c7
up
Goaman Oct 14, 2025
2cd6ac9
up
Goaman Oct 20, 2025
3f505f6
up
Goaman Oct 20, 2025
bf09247
up
Goaman Oct 20, 2025
5d18332
add ability to draft
Goaman Oct 20, 2025
868639d
up
Goaman Oct 21, 2025
db2d8ed
up
Goaman Oct 21, 2025
a177f87
up
Goaman Oct 21, 2025
0beba35
up
Goaman Oct 21, 2025
c471ea3
up
Goaman Oct 21, 2025
e1a5da0
remove debugger
Goaman Oct 21, 2025
742b59c
up
Goaman Oct 21, 2025
84ef98f
up
Goaman Oct 21, 2025
7579558
up
Goaman Oct 21, 2025
83a2322
up
Goaman Oct 21, 2025
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.

4 changes: 2 additions & 2 deletions 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 Expand Up @@ -88,7 +88,7 @@
"^.+\\.ts?$": "ts-jest"
},
"verbose": false,
"testRegex": "(/tests/.*(test|spec))\\.ts?$",
"testRegex": "(/tests/model.(test|spec))\\.ts?$",
"moduleFileExtensions": [
"ts",
"tsx",
Expand Down
119 changes: 119 additions & 0 deletions signal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@



# todo
## make doc
### content
#### signals
- useState is useless
``` js

class Parent extends Component {
setup() {
const todos = reactive([]);
useEnv({
getFirstTodo: () => todos[0],
});
}
}

class Child extends Component {
static template = xml`<t t-out="this.firstTodo"/>`;
setup() {
this.firstTodo = useState(this.env.getFirstTodo());
}
}

class Child extends Component {
static template = xml`<t t-out="this.env.getFoo()"/>`;
}


function getSomething(key) {
return registry.category('someReactiveValues').value();
}

class MyComp extends Component {
static template = xml`
<t t-out="this.env.getSomething(this.state.value)"/>
`;
}

```
#### derived
- before: effect to synchronise data
- recompute whenever one of the dependency changes
- currently needed in pos for the relational model
- currently needed in mail for the relational model
- future: async derived
- cancel if recomputed
- cancel if component destroyed

### todo
- find imperative example that could be derived
- build everything from reactive principle vs build from imperative principle


## derived in odoo
- check in odoo where derived could have been used:
- useEffect, effect, willUpdateProps, services

## derived
- 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?

## Models
- relations one2many, many2many
- delete
- automatic models
- partial lists
- draft record
- indexes

## Optimisation
- map/filter/reducte/... with delta data structure

## owl component
- test proper unsubscription





# pos
- great:
- automatically fetch model definition
- offline
- missing:
- choice between online/offline fetching (it only get data offline)
- multi-level draft


# questions
to batch write in next tick or directly?

# optimization
- fragmented memory
- Entity-Component-System

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


# encountered issues in odoo
## 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
31 changes: 31 additions & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,35 @@
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
} & Opts;
export type Opts = {
name?: string;
};
export type customDirectives = Record<
string,
(node: Element, value: string, modifier: string[]) => void
>;

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

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

export type OldValue = any;

export type Getter<V> = () => V | null;
export type Setter<T, V> = (this: T, value: V) => void;
export type MakeGetSetReturn<T, V> = readonly [Getter<V>] | readonly [Getter<V>, Setter<T, V>];
export type MakeGetSet<T, V> = (this: T) => MakeGetSetReturn<T, V>;
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;
}
77 changes: 43 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,37 @@ 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.STALE,
name: `ComponentNode(${C.name})`,
};
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 +152,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 +271,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 +393,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;
// }
Loading
Loading