@zoijs/core 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -93
- package/README.md +154 -154
- package/package.json +67 -63
- package/src/core/each.js +24 -24
- package/src/core/renderer.js +473 -442
- package/src/devtools.d.ts +56 -0
- package/src/index.d.ts +185 -185
- package/src/reactivity/core.js +7 -0
- package/src/reactivity/devtools.js +77 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Type surface for `@zoijs/core/devtools` — the read-only reactive-graph
|
|
2
|
+
// inspection hook (RFC 0005). This is dev-tooling, NOT part of the stable
|
|
3
|
+
// nine-function API; it lives behind its own subpath so the learnable surface
|
|
4
|
+
// stays frozen. See `src/reactivity/devtools.js`.
|
|
5
|
+
|
|
6
|
+
/** What a node is. */
|
|
7
|
+
export type NodeKind = "state" | "computed" | "effect";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A reactive graph node. Treat it as an opaque identity with a few read-only
|
|
11
|
+
* fields — never mutate it. `sources` / `observers` let an inspector walk the
|
|
12
|
+
* graph on demand (so reads stay un-instrumented). `fn` is present on
|
|
13
|
+
* computeds/effects, absent on states.
|
|
14
|
+
*/
|
|
15
|
+
export interface ReactiveNode {
|
|
16
|
+
readonly value?: unknown;
|
|
17
|
+
readonly fn?: Function | null;
|
|
18
|
+
readonly sources?: ReadonlySet<ReactiveNode>;
|
|
19
|
+
readonly observers: ReadonlySet<ReactiveNode>;
|
|
20
|
+
readonly disposed?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A tag the renderer attaches to a binding node, naming the DOM it updates:
|
|
25
|
+
* a text slot, an attribute, or a keyed list. Lets an inspector show *which DOM
|
|
26
|
+
* node each signal updates*.
|
|
27
|
+
*/
|
|
28
|
+
export interface NodeLabel {
|
|
29
|
+
kind: "text" | "attr" | "list";
|
|
30
|
+
el: Node;
|
|
31
|
+
name?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Implement this to observe the reactive graph. Every callback is read-only and
|
|
36
|
+
* fires only in dev mode while attached. `onCreate` is called once per node;
|
|
37
|
+
* `onRun` each time a computed/effect recomputes; `onWrite` each time a state
|
|
38
|
+
* actually changes; `onDispose` when a computed/effect leaves the graph.
|
|
39
|
+
*/
|
|
40
|
+
export interface Inspector {
|
|
41
|
+
onAttach?(): void;
|
|
42
|
+
onCreate(node: ReactiveNode, kind: NodeKind, label?: NodeLabel): void;
|
|
43
|
+
onRun(node: ReactiveNode): void;
|
|
44
|
+
onWrite(node: ReactiveNode): void;
|
|
45
|
+
onDispose(node: ReactiveNode): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Attach a read-only inspector; returns a detach function. A no-op (returns a
|
|
50
|
+
* no-op) under `configure({ dev: false })`, so an inspector can never run
|
|
51
|
+
* against an app shipped in production mode.
|
|
52
|
+
*/
|
|
53
|
+
export function attachInspector(inspector: Inspector): () => void;
|
|
54
|
+
|
|
55
|
+
/** True while an inspector is attached. */
|
|
56
|
+
export function inspecting(): boolean;
|
package/src/index.d.ts
CHANGED
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
// Type definitions for Zoijs.
|
|
2
|
-
//
|
|
3
|
-
// Zoijs is authored in plain JavaScript; these declarations add editor
|
|
4
|
-
// autocomplete and optional type-checking WITHOUT converting the project to
|
|
5
|
-
// TypeScript. Using them is entirely opt-in — JS users are unaffected.
|
|
6
|
-
|
|
7
|
-
/** A reactive value created by {@link createState}. */
|
|
8
|
-
export interface State<T> {
|
|
9
|
-
/** Read the current value. Inside a binding/effect this subscribes to it. */
|
|
10
|
-
get(): T;
|
|
11
|
-
/** Write a new value. Dependents update only if the value actually changed. */
|
|
12
|
-
set(next: T): void;
|
|
13
|
-
/** Read the current value WITHOUT subscribing. */
|
|
14
|
-
peek(): T;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** A lazy, cached, value-gated derived value created by {@link computed}. */
|
|
18
|
-
export interface Computed<T> {
|
|
19
|
-
/** Read the current value. Recomputes only if a dependency changed. */
|
|
20
|
-
get(): T;
|
|
21
|
-
/** Read without subscribing. */
|
|
22
|
-
peek(): T;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
declare const templateBrand: unique symbol;
|
|
26
|
-
/**
|
|
27
|
-
* The result of an `html` template. Pass it to {@link mount}, return it from a
|
|
28
|
-
* component, or place it in another template. Treat it as opaque.
|
|
29
|
-
*/
|
|
30
|
-
export interface TemplateResult {
|
|
31
|
-
readonly [templateBrand]: true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
declare const eachBrand: unique symbol;
|
|
35
|
-
/** The result of {@link each}. Place it in a template's child position. */
|
|
36
|
-
export interface EachResult {
|
|
37
|
-
readonly [eachBrand]: true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** A component is a function that returns an `html` template. */
|
|
41
|
-
export type Component = () => TemplateResult;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* A callback ref. Place it on an element as `ref=${fn}`; it receives the real DOM
|
|
45
|
-
* element once the surrounding render has been inserted (deferred one microtask,
|
|
46
|
-
* so `focus()` / `scrollIntoView()` / `getBoundingClientRect()` work). It may
|
|
47
|
-
* return a cleanup function, which runs on unmount or list-item removal. It runs
|
|
48
|
-
* once and is not reactive.
|
|
49
|
-
*
|
|
50
|
-
* ```ts
|
|
51
|
-
* html`<input ref=${(el: HTMLInputElement) => el.focus()} />`
|
|
52
|
-
* html`<div ref=${(el) => { const c = chart(el); return () => c.destroy(); }}></div>`
|
|
53
|
-
* ```
|
|
54
|
-
*/
|
|
55
|
-
export type Ref<E extends Element = Element> = (element: E) => void | (() => void);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Tagged-template function — write your markup as HTML.
|
|
59
|
-
*
|
|
60
|
-
* ```js
|
|
61
|
-
* html`<button onclick=${() => count.set(count.get() + 1)}>${() => count.get()}</button>`
|
|
62
|
-
* ```
|
|
63
|
-
*
|
|
64
|
-
* To reach the rendered DOM element, add a callback `ref` (see {@link Ref}):
|
|
65
|
-
* `html\`<input ref=${(el) => el.focus()} />\``.
|
|
66
|
-
*/
|
|
67
|
-
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Render a component (or a template) into a DOM element or CSS selector.
|
|
71
|
-
* Returns an `unmount()` that detaches the DOM and disposes all reactivity.
|
|
72
|
-
*/
|
|
73
|
-
export function mount(component: Component | TemplateResult, target: Element | string): () => void;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Create a reactive value.
|
|
77
|
-
*
|
|
78
|
-
* ```ts
|
|
79
|
-
* const count = createState(0); // State<number>
|
|
80
|
-
* const name = createState<string>(""); // explicit
|
|
81
|
-
* ```
|
|
82
|
-
*/
|
|
83
|
-
export function createState<T>(initial: T, equals?: (a: T, b: T) => boolean): State<T>;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Create a lazy, cached derived value.
|
|
87
|
-
*
|
|
88
|
-
* ```ts
|
|
89
|
-
* const fullName = computed(() => `${first.get()} ${last.get()}`); // Computed<string>
|
|
90
|
-
* ```
|
|
91
|
-
*/
|
|
92
|
-
export function computed<T>(fn: () => T, equals?: (a: T, b: T) => boolean): Computed<T>;
|
|
93
|
-
|
|
94
|
-
/** A disposable handle returned by {@link effect}. */
|
|
95
|
-
export interface EffectHandle {
|
|
96
|
-
/** Dispose the effect now. It also auto-disposes with its owner (component/list item). */
|
|
97
|
-
dispose(): void;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Run a side effect that re-runs whenever a reactive value it reads changes.
|
|
102
|
-
* Runs once immediately, then again (batched on a microtask) on any change.
|
|
103
|
-
* Dependencies are tracked automatically — there is no dependency array.
|
|
104
|
-
*
|
|
105
|
-
* The function may return a cleanup function (same convention as a `ref`); it
|
|
106
|
-
* runs **before the next run** and **on dispose**. Use the return value for
|
|
107
|
-
* per-run teardown — `onCleanup` is component-scoped (fires on unmount), not
|
|
108
|
-
* per-run.
|
|
109
|
-
*
|
|
110
|
-
* Created inside a component or list item, it auto-disposes with that scope;
|
|
111
|
-
* created at module top level, it lives until you call `dispose()`.
|
|
112
|
-
*
|
|
113
|
-
* ```ts
|
|
114
|
-
* // persist on change
|
|
115
|
-
* effect(() => localStorage.setItem("theme", theme.get()));
|
|
116
|
-
*
|
|
117
|
-
* // per-run cleanup
|
|
118
|
-
* effect(() => {
|
|
119
|
-
* const id = setInterval(() => poll(query.get()), 1000);
|
|
120
|
-
* return () => clearInterval(id);
|
|
121
|
-
* });
|
|
122
|
-
* ```
|
|
123
|
-
*
|
|
124
|
-
* For reactive *content on screen*, use a binding (`${() => …}`), not an effect.
|
|
125
|
-
*/
|
|
126
|
-
export function effect(fn: () => void | (() => void)): EffectHandle;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Keyed list rendering. `items` may be a reactive function or a plain array;
|
|
130
|
-
* `keyFn` returns a stable unique key; `renderFn` returns the template for one item.
|
|
131
|
-
*
|
|
132
|
-
* ```ts
|
|
133
|
-
* each(() => todos.get(), (t) => t.id, (t) => html`<li>${() => t.text}</li>`)
|
|
134
|
-
* ```
|
|
135
|
-
*/
|
|
136
|
-
export function each<T>(
|
|
137
|
-
items: () => readonly T[],
|
|
138
|
-
keyFn: (item: T) => unknown,
|
|
139
|
-
renderFn: (item: T) => TemplateResult
|
|
140
|
-
): EachResult;
|
|
141
|
-
export function each<T>(
|
|
142
|
-
items: readonly T[],
|
|
143
|
-
keyFn: (item: T) => unknown,
|
|
144
|
-
renderFn: (item: T) => TemplateResult
|
|
145
|
-
): EachResult;
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Render `child`; if it **throws synchronously while building its markup** (a
|
|
149
|
-
* setup/render error that would otherwise break the whole `mount`), tear down the
|
|
150
|
-
* partial work and render `fallback` instead. Place the call in a template slot.
|
|
151
|
-
*
|
|
152
|
-
* Catches synchronous setup/render throws only. Errors in reactive *updates* are
|
|
153
|
-
* already contained per binding by the core; *async* errors belong to
|
|
154
|
-
* `@zoijs/resource` / `@zoijs/action`'s `error()` state. It renders once (no reset
|
|
155
|
-
* — re-mount the subtree to retry), logs in dev, and is silent in production.
|
|
156
|
-
*
|
|
157
|
-
* ```ts
|
|
158
|
-
* html`<section>
|
|
159
|
-
* ${boundary(() => RiskyWidget(), (err) => html`<p>Couldn't load this.</p>`)}
|
|
160
|
-
* </section>`
|
|
161
|
-
* ```
|
|
162
|
-
*/
|
|
163
|
-
export function boundary<C, F>(
|
|
164
|
-
child: (() => C) | C,
|
|
165
|
-
fallback: ((error: unknown) => F) | F
|
|
166
|
-
): C | F;
|
|
167
|
-
|
|
168
|
-
/** Toggle development warnings (default: `dev` is `true`). */
|
|
169
|
-
export function configure(options: { dev?: boolean }): void;
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Register a teardown function for the current component or list item. It runs
|
|
173
|
-
* when that component is unmounted or that list item is removed. Use it for
|
|
174
|
-
* timers, subscriptions, or third-party widgets created during setup.
|
|
175
|
-
*
|
|
176
|
-
* ```ts
|
|
177
|
-
* function Clock() {
|
|
178
|
-
* const now = createState(Date.now());
|
|
179
|
-
* const id = setInterval(() => now.set(Date.now()), 1000);
|
|
180
|
-
* onCleanup(() => clearInterval(id));
|
|
181
|
-
* return html`<time>${() => now.get()}</time>`;
|
|
182
|
-
* }
|
|
183
|
-
* ```
|
|
184
|
-
*/
|
|
185
|
-
export function onCleanup(fn: () => void): void;
|
|
1
|
+
// Type definitions for Zoijs.
|
|
2
|
+
//
|
|
3
|
+
// Zoijs is authored in plain JavaScript; these declarations add editor
|
|
4
|
+
// autocomplete and optional type-checking WITHOUT converting the project to
|
|
5
|
+
// TypeScript. Using them is entirely opt-in — JS users are unaffected.
|
|
6
|
+
|
|
7
|
+
/** A reactive value created by {@link createState}. */
|
|
8
|
+
export interface State<T> {
|
|
9
|
+
/** Read the current value. Inside a binding/effect this subscribes to it. */
|
|
10
|
+
get(): T;
|
|
11
|
+
/** Write a new value. Dependents update only if the value actually changed. */
|
|
12
|
+
set(next: T): void;
|
|
13
|
+
/** Read the current value WITHOUT subscribing. */
|
|
14
|
+
peek(): T;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A lazy, cached, value-gated derived value created by {@link computed}. */
|
|
18
|
+
export interface Computed<T> {
|
|
19
|
+
/** Read the current value. Recomputes only if a dependency changed. */
|
|
20
|
+
get(): T;
|
|
21
|
+
/** Read without subscribing. */
|
|
22
|
+
peek(): T;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare const templateBrand: unique symbol;
|
|
26
|
+
/**
|
|
27
|
+
* The result of an `html` template. Pass it to {@link mount}, return it from a
|
|
28
|
+
* component, or place it in another template. Treat it as opaque.
|
|
29
|
+
*/
|
|
30
|
+
export interface TemplateResult {
|
|
31
|
+
readonly [templateBrand]: true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare const eachBrand: unique symbol;
|
|
35
|
+
/** The result of {@link each}. Place it in a template's child position. */
|
|
36
|
+
export interface EachResult {
|
|
37
|
+
readonly [eachBrand]: true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A component is a function that returns an `html` template. */
|
|
41
|
+
export type Component = () => TemplateResult;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A callback ref. Place it on an element as `ref=${fn}`; it receives the real DOM
|
|
45
|
+
* element once the surrounding render has been inserted (deferred one microtask,
|
|
46
|
+
* so `focus()` / `scrollIntoView()` / `getBoundingClientRect()` work). It may
|
|
47
|
+
* return a cleanup function, which runs on unmount or list-item removal. It runs
|
|
48
|
+
* once and is not reactive.
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* html`<input ref=${(el: HTMLInputElement) => el.focus()} />`
|
|
52
|
+
* html`<div ref=${(el) => { const c = chart(el); return () => c.destroy(); }}></div>`
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export type Ref<E extends Element = Element> = (element: E) => void | (() => void);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Tagged-template function — write your markup as HTML.
|
|
59
|
+
*
|
|
60
|
+
* ```js
|
|
61
|
+
* html`<button onclick=${() => count.set(count.get() + 1)}>${() => count.get()}</button>`
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* To reach the rendered DOM element, add a callback `ref` (see {@link Ref}):
|
|
65
|
+
* `html\`<input ref=${(el) => el.focus()} />\``.
|
|
66
|
+
*/
|
|
67
|
+
export function html(strings: TemplateStringsArray, ...values: unknown[]): TemplateResult;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render a component (or a template) into a DOM element or CSS selector.
|
|
71
|
+
* Returns an `unmount()` that detaches the DOM and disposes all reactivity.
|
|
72
|
+
*/
|
|
73
|
+
export function mount(component: Component | TemplateResult, target: Element | string): () => void;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a reactive value.
|
|
77
|
+
*
|
|
78
|
+
* ```ts
|
|
79
|
+
* const count = createState(0); // State<number>
|
|
80
|
+
* const name = createState<string>(""); // explicit
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function createState<T>(initial: T, equals?: (a: T, b: T) => boolean): State<T>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a lazy, cached derived value.
|
|
87
|
+
*
|
|
88
|
+
* ```ts
|
|
89
|
+
* const fullName = computed(() => `${first.get()} ${last.get()}`); // Computed<string>
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export function computed<T>(fn: () => T, equals?: (a: T, b: T) => boolean): Computed<T>;
|
|
93
|
+
|
|
94
|
+
/** A disposable handle returned by {@link effect}. */
|
|
95
|
+
export interface EffectHandle {
|
|
96
|
+
/** Dispose the effect now. It also auto-disposes with its owner (component/list item). */
|
|
97
|
+
dispose(): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Run a side effect that re-runs whenever a reactive value it reads changes.
|
|
102
|
+
* Runs once immediately, then again (batched on a microtask) on any change.
|
|
103
|
+
* Dependencies are tracked automatically — there is no dependency array.
|
|
104
|
+
*
|
|
105
|
+
* The function may return a cleanup function (same convention as a `ref`); it
|
|
106
|
+
* runs **before the next run** and **on dispose**. Use the return value for
|
|
107
|
+
* per-run teardown — `onCleanup` is component-scoped (fires on unmount), not
|
|
108
|
+
* per-run.
|
|
109
|
+
*
|
|
110
|
+
* Created inside a component or list item, it auto-disposes with that scope;
|
|
111
|
+
* created at module top level, it lives until you call `dispose()`.
|
|
112
|
+
*
|
|
113
|
+
* ```ts
|
|
114
|
+
* // persist on change
|
|
115
|
+
* effect(() => localStorage.setItem("theme", theme.get()));
|
|
116
|
+
*
|
|
117
|
+
* // per-run cleanup
|
|
118
|
+
* effect(() => {
|
|
119
|
+
* const id = setInterval(() => poll(query.get()), 1000);
|
|
120
|
+
* return () => clearInterval(id);
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* For reactive *content on screen*, use a binding (`${() => …}`), not an effect.
|
|
125
|
+
*/
|
|
126
|
+
export function effect(fn: () => void | (() => void)): EffectHandle;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Keyed list rendering. `items` may be a reactive function or a plain array;
|
|
130
|
+
* `keyFn` returns a stable unique key; `renderFn` returns the template for one item.
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* each(() => todos.get(), (t) => t.id, (t) => html`<li>${() => t.text}</li>`)
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export function each<T>(
|
|
137
|
+
items: () => readonly T[],
|
|
138
|
+
keyFn: (item: T) => unknown,
|
|
139
|
+
renderFn: (item: T) => TemplateResult
|
|
140
|
+
): EachResult;
|
|
141
|
+
export function each<T>(
|
|
142
|
+
items: readonly T[],
|
|
143
|
+
keyFn: (item: T) => unknown,
|
|
144
|
+
renderFn: (item: T) => TemplateResult
|
|
145
|
+
): EachResult;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render `child`; if it **throws synchronously while building its markup** (a
|
|
149
|
+
* setup/render error that would otherwise break the whole `mount`), tear down the
|
|
150
|
+
* partial work and render `fallback` instead. Place the call in a template slot.
|
|
151
|
+
*
|
|
152
|
+
* Catches synchronous setup/render throws only. Errors in reactive *updates* are
|
|
153
|
+
* already contained per binding by the core; *async* errors belong to
|
|
154
|
+
* `@zoijs/resource` / `@zoijs/action`'s `error()` state. It renders once (no reset
|
|
155
|
+
* — re-mount the subtree to retry), logs in dev, and is silent in production.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* html`<section>
|
|
159
|
+
* ${boundary(() => RiskyWidget(), (err) => html`<p>Couldn't load this.</p>`)}
|
|
160
|
+
* </section>`
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export function boundary<C, F>(
|
|
164
|
+
child: (() => C) | C,
|
|
165
|
+
fallback: ((error: unknown) => F) | F
|
|
166
|
+
): C | F;
|
|
167
|
+
|
|
168
|
+
/** Toggle development warnings (default: `dev` is `true`). */
|
|
169
|
+
export function configure(options: { dev?: boolean }): void;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Register a teardown function for the current component or list item. It runs
|
|
173
|
+
* when that component is unmounted or that list item is removed. Use it for
|
|
174
|
+
* timers, subscriptions, or third-party widgets created during setup.
|
|
175
|
+
*
|
|
176
|
+
* ```ts
|
|
177
|
+
* function Clock() {
|
|
178
|
+
* const now = createState(Date.now());
|
|
179
|
+
* const id = setInterval(() => now.set(Date.now()), 1000);
|
|
180
|
+
* onCleanup(() => clearInterval(id));
|
|
181
|
+
* return html`<time>${() => now.get()}</time>`;
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export function onCleanup(fn: () => void): void;
|
package/src/reactivity/core.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { onCleanup } from "./owner.js";
|
|
18
18
|
import { isDev } from "./env.js";
|
|
19
|
+
import { reportCreate, reportRun, reportWrite, reportDispose } from "./devtools.js";
|
|
19
20
|
|
|
20
21
|
const CLEAN = 0;
|
|
21
22
|
const CHECK = 1;
|
|
@@ -70,6 +71,7 @@ function readNode(node) {
|
|
|
70
71
|
function writeNode(node, next) {
|
|
71
72
|
if (node.equals(node.value, next)) return; // equality-gated
|
|
72
73
|
node.value = next;
|
|
74
|
+
reportWrite(node); // devtools: only fires on an actual change (dev + attached)
|
|
73
75
|
for (const observer of [...node.observers]) markStale(observer, DIRTY);
|
|
74
76
|
}
|
|
75
77
|
|
|
@@ -120,6 +122,7 @@ function runComputation(node) {
|
|
|
120
122
|
} finally {
|
|
121
123
|
currentObserver = previousObserver;
|
|
122
124
|
}
|
|
125
|
+
reportRun(node); // devtools: this node actually recomputed (dev + attached)
|
|
123
126
|
if (node.isEffect) {
|
|
124
127
|
// An effect may return a cleanup function (same convention as a ref): it runs
|
|
125
128
|
// before the next run and on dispose. Anything else is ignored.
|
|
@@ -156,6 +159,7 @@ function disposeNode(node) {
|
|
|
156
159
|
node.disposed = true;
|
|
157
160
|
cleanupSources(node);
|
|
158
161
|
if (node.isEffect) runEffectCleanup(node); // final cleanup on dispose
|
|
162
|
+
reportDispose(node); // devtools: node left the graph (dev + attached)
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
const warned = new WeakSet();
|
|
@@ -169,6 +173,7 @@ function warnOnce(node, message) {
|
|
|
169
173
|
|
|
170
174
|
export function createState(initial, equals = Object.is) {
|
|
171
175
|
const node = { value: initial, observers: new Set(), equals, fn: null };
|
|
176
|
+
reportCreate(node, "state");
|
|
172
177
|
return {
|
|
173
178
|
get: () => readNode(node),
|
|
174
179
|
set: (next) => writeNode(node, next),
|
|
@@ -188,6 +193,7 @@ export function computed(fn, equals = Object.is) {
|
|
|
188
193
|
equals,
|
|
189
194
|
};
|
|
190
195
|
onCleanup(() => disposeNode(node)); // disposed with its owner scope
|
|
196
|
+
reportCreate(node, "computed");
|
|
191
197
|
return {
|
|
192
198
|
get: () => readNode(node),
|
|
193
199
|
peek: () => {
|
|
@@ -209,6 +215,7 @@ export function effect(fn) {
|
|
|
209
215
|
cleanup: null,
|
|
210
216
|
};
|
|
211
217
|
onCleanup(() => disposeNode(node)); // disposed with its owner scope
|
|
218
|
+
reportCreate(node, "effect"); // before its first run, so the run is attributed
|
|
212
219
|
runComputation(node);
|
|
213
220
|
node.state = CLEAN;
|
|
214
221
|
return { dispose: () => disposeNode(node) };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// devtools.js — read-only inspection hook for the reactive graph (RFC 0005).
|
|
2
|
+
//
|
|
3
|
+
// This is the ONE seam between the engine and an external inspector
|
|
4
|
+
// (@zoijs/devtools, or a browser extension). It is, by construction:
|
|
5
|
+
//
|
|
6
|
+
// • Off by default. Until something attaches, every report below is a single
|
|
7
|
+
// `if (!inspector) return` — so an un-inspected app (every production app)
|
|
8
|
+
// pays no measurable cost and exposes nothing.
|
|
9
|
+
// • Read-light. The HOT path — readNode, run on every `.get()` — is NOT
|
|
10
|
+
// instrumented. Only lifecycle points are (create / run / write / dispose).
|
|
11
|
+
// The inspector reads a node's `sources` / `observers` on demand for the
|
|
12
|
+
// graph view, so even an *attached* inspector adds no per-read cost.
|
|
13
|
+
// • Dev-only. attachInspector() is a no-op under configure({ dev:false }), so
|
|
14
|
+
// an inspector can never run against an app shipped in production mode.
|
|
15
|
+
// • Read-only. The engine hands the inspector raw nodes to OBSERVE; it never
|
|
16
|
+
// reads a value back from the inspector or lets it mutate the graph.
|
|
17
|
+
//
|
|
18
|
+
// Not part of the learnable nine-function surface — reached through the dedicated
|
|
19
|
+
// subpath `@zoijs/core/devtools`, by tooling, never by application code.
|
|
20
|
+
|
|
21
|
+
import { isDev } from "./env.js";
|
|
22
|
+
|
|
23
|
+
let inspector = null;
|
|
24
|
+
|
|
25
|
+
// A small stack lets the renderer tag the node(s) created while it wires a DOM
|
|
26
|
+
// binding with the DOM they update (e.g. { kind:"text", el }); see labelNext.
|
|
27
|
+
const labels = [];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attach a read-only inspector and start receiving reports. Returns a detach
|
|
31
|
+
* function. No-op (returns a no-op) in production mode or with a falsy inspector.
|
|
32
|
+
*/
|
|
33
|
+
export function attachInspector(next) {
|
|
34
|
+
if (!isDev() || !next) return () => {};
|
|
35
|
+
inspector = next;
|
|
36
|
+
if (inspector.onAttach) inspector.onAttach();
|
|
37
|
+
return () => {
|
|
38
|
+
if (inspector === next) inspector = null;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True while an inspector is attached — a cheap guard for optional extra work. */
|
|
43
|
+
export function inspecting() {
|
|
44
|
+
return inspector !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---- engine-side reports — each a no-op unless an inspector is attached -------
|
|
48
|
+
|
|
49
|
+
export function reportCreate(node, kind) {
|
|
50
|
+
if (!inspector) return;
|
|
51
|
+
inspector.onCreate(node, kind, labels.length ? labels[labels.length - 1] : undefined);
|
|
52
|
+
}
|
|
53
|
+
export function reportRun(node) {
|
|
54
|
+
if (inspector) inspector.onRun(node);
|
|
55
|
+
}
|
|
56
|
+
export function reportWrite(node) {
|
|
57
|
+
if (inspector) inspector.onWrite(node);
|
|
58
|
+
}
|
|
59
|
+
export function reportDispose(node) {
|
|
60
|
+
if (inspector) inspector.onDispose(node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tag the node(s) created while `run` executes with `label` — used by the
|
|
65
|
+
* renderer to attribute a binding effect to the DOM node it updates. The label is
|
|
66
|
+
* only ever read inside reportCreate, so this costs one null-check when no
|
|
67
|
+
* inspector is attached.
|
|
68
|
+
*/
|
|
69
|
+
export function labelNext(label, run) {
|
|
70
|
+
if (!inspector) return run();
|
|
71
|
+
labels.push(label);
|
|
72
|
+
try {
|
|
73
|
+
return run();
|
|
74
|
+
} finally {
|
|
75
|
+
labels.pop();
|
|
76
|
+
}
|
|
77
|
+
}
|