neo.mjs 10.0.0-beta.6 → 10.0.1
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/.github/RELEASE_NOTES/v10.0.0-beta.1.md +20 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.2.md +73 -0
- package/.github/RELEASE_NOTES/v10.0.0-beta.3.md +39 -0
- package/.github/RELEASE_NOTES/v10.0.0.md +52 -0
- package/ServiceWorker.mjs +2 -2
- package/apps/portal/index.html +1 -1
- package/apps/portal/resources/data/blog.json +24 -0
- package/apps/portal/view/ViewportController.mjs +6 -4
- package/apps/portal/view/examples/List.mjs +28 -19
- package/apps/portal/view/home/FooterContainer.mjs +1 -1
- package/examples/functional/button/base/MainContainer.mjs +207 -0
- package/examples/functional/button/base/app.mjs +6 -0
- package/examples/functional/button/base/index.html +11 -0
- package/examples/functional/button/base/neo-config.json +6 -0
- package/learn/blog/v10-deep-dive-functional-components.md +293 -0
- package/learn/blog/v10-deep-dive-reactivity.md +522 -0
- package/learn/blog/v10-deep-dive-state-provider.md +432 -0
- package/learn/blog/v10-deep-dive-vdom-revolution.md +194 -0
- package/learn/blog/v10-post1-love-story.md +383 -0
- package/learn/guides/uibuildingblocks/WorkingWithVDom.md +26 -2
- package/package.json +3 -3
- package/src/DefaultConfig.mjs +2 -2
- package/src/Neo.mjs +47 -45
- package/src/component/Abstract.mjs +412 -0
- package/src/component/Base.mjs +18 -380
- package/src/core/Base.mjs +34 -33
- package/src/core/Effect.mjs +30 -34
- package/src/core/EffectManager.mjs +101 -14
- package/src/core/Observable.mjs +69 -65
- package/src/form/field/Text.mjs +11 -5
- package/src/functional/button/Base.mjs +384 -0
- package/src/functional/component/Base.mjs +51 -145
- package/src/layout/Cube.mjs +8 -4
- package/src/manager/VDomUpdate.mjs +179 -94
- package/src/mixin/VdomLifecycle.mjs +4 -1
- package/src/state/Provider.mjs +41 -27
- package/src/util/VDom.mjs +11 -4
- package/src/util/vdom/TreeBuilder.mjs +38 -62
- package/src/worker/mixin/RemoteMethodAccess.mjs +1 -6
- package/test/siesta/siesta.js +15 -3
- package/test/siesta/tests/VdomCalendar.mjs +7 -7
- package/test/siesta/tests/VdomHelper.mjs +7 -7
- package/test/siesta/tests/classic/Button.mjs +113 -0
- package/test/siesta/tests/core/EffectBatching.mjs +46 -41
- package/test/siesta/tests/functional/Button.mjs +113 -0
- package/test/siesta/tests/state/ProviderNestedDataConfigs.mjs +59 -0
- package/test/siesta/tests/vdom/Advanced.mjs +14 -8
- package/test/siesta/tests/vdom/VdomAsymmetricUpdates.mjs +9 -9
- package/test/siesta/tests/vdom/VdomRealWorldUpdates.mjs +6 -6
- package/test/siesta/tests/vdom/layout/Cube.mjs +11 -7
- package/test/siesta/tests/vdom/table/Container.mjs +9 -5
- package/src/core/EffectBatchManager.mjs +0 -67
@@ -0,0 +1,522 @@
|
|
1
|
+
# Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity
|
2
|
+
|
3
|
+
In the main article of our series, we explored the "heartbreak" of modern frontend development: the constant battle
|
4
|
+
against the main thread, the tedious "memoization tax," and the architectural nightmares of complex state management.
|
5
|
+
These are not isolated issues; they are symptoms of a foundational problem in how mainstream frameworks handle reactivity.
|
6
|
+
|
7
|
+
Welcome to the first deep dive into the architecture of Neo.mjs v10. In this article, we're going to dissect the engine
|
8
|
+
that makes the old problems obsolete: **The Two-Tier Reactivity System**. This is a revolutionary approach that
|
9
|
+
seamlessly unifies two powerful paradigms—a classic "push" system and a modern "pull" system—into one elegant
|
10
|
+
developer experience. This isn't just a new feature; it's a new reality for how you can write and reason about your
|
11
|
+
application's state and rendering logic.
|
12
|
+
|
13
|
+
*(Part 2 of 5 in the v10 blog series. Details at the bottom.)*
|
14
|
+
|
15
|
+
---
|
16
|
+
|
17
|
+
## Act I: Tier 1 - The Classic "Push" System
|
18
|
+
|
19
|
+
Unlike many frameworks, Neo.mjs has *always* had a reactive config system. Since its earliest versions, you could take a
|
20
|
+
component instance and change its properties directly, and the UI would update automatically.
|
21
|
+
|
22
|
+
```javascript
|
23
|
+
// This has always worked in Neo.mjs
|
24
|
+
const myButton = Neo.get('my-button');
|
25
|
+
myButton.text = 'Click me now!'; // The button's text in the DOM updates
|
26
|
+
```
|
27
|
+
|
28
|
+
This has always been powered by a robust system of prototype-based getters and setters.
|
29
|
+
For any reactive **Named Config** (e.g., `text_`), the framework provides three optional lifecycle hooks that you can
|
30
|
+
implement to hook into its lifecycle:
|
31
|
+
|
32
|
+
* `beforeGetText(value)`: Run just before a value is read.
|
33
|
+
* `beforeSetText(value, oldValue)`: Run before a new value is set, allowing for validation or transformation.
|
34
|
+
* `afterSetText(value, oldValue)`: Run after a value has been successfully changed, perfect for triggering side effects.
|
35
|
+
|
36
|
+
This powerful, hook-based API is an imperative, **"push-based"** system. Think of it like a **manual phone tree**: when a
|
37
|
+
config changes, your `afterSet` hook is responsible for explicitly "calling" all the other parts of the component
|
38
|
+
that need to know about the change. It offers precise, granular control, but it means you are manually managing the
|
39
|
+
dependency graph.
|
40
|
+
|
41
|
+
```javascript readonly
|
42
|
+
// Example: Implementing an afterSet hook
|
43
|
+
import Base from 'neo.mjs/src/core/Base.mjs';
|
44
|
+
|
45
|
+
class MyComponent extends Base {
|
46
|
+
static config = {
|
47
|
+
className: 'My.AfterSetExample',
|
48
|
+
// A reactive config with a trailing underscore
|
49
|
+
message_: 'Hello'
|
50
|
+
}
|
51
|
+
|
52
|
+
// This hook automatically runs after 'message' is set
|
53
|
+
afterSetMessage(value, oldValue) {
|
54
|
+
console.log(`Message changed from "${oldValue}" to "${value}"`);
|
55
|
+
// Manually update a dependent property or trigger a UI update
|
56
|
+
this.someOtherProperty = `Processed: ${value.toUpperCase()}`;
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
const myInstance = Neo.create(MyComponent);
|
61
|
+
myInstance.message = 'World'; // Console will log: Message changed from "Hello" to "World"
|
62
|
+
console.log(myInstance.someOtherProperty); // Logs: Processed: WORLD
|
63
|
+
```
|
64
|
+
|
65
|
+
For v10, we didn't replace this system—we super-charged it. We asked: what if we could add a second, fully automatic
|
66
|
+
tier to this foundation?
|
67
|
+
|
68
|
+
## Act II: Tier 2 - The Modern "Pull" System
|
69
|
+
|
70
|
+
The v10 release introduces the second tier: a declarative, **"pull-based"** system. Think of it like a **subscription
|
71
|
+
service**: you "subscribe" to a piece of state simply by reading it. When that state changes, the framework automatically
|
72
|
+
notifies all subscribers. You no longer manage the dependency graph—the framework does it for you.
|
73
|
+
|
74
|
+
This is powered by a new set of core primitives (`Neo.core.Config`, `Neo.core.Effect`, `Neo.core.EffectManager`)
|
75
|
+
that form a hyper-performant reactive foundation.
|
76
|
+
|
77
|
+
The true genius of this Two-Tier system is how they are seamlessly bridged together. Think of it like a **universal
|
78
|
+
power adapter**: you use a simple, familiar plug (`myButton.text = '...'`), and the adapter transparently handles
|
79
|
+
powering both systems at once.
|
80
|
+
|
81
|
+
When you define a a config with a trailing underscore (e.g., `text_`), the generated setter becomes this adapter. It
|
82
|
+
simultaneously:
|
83
|
+
|
84
|
+
1. **Powers Tier 2 ("Pull"):** It updates the underlying `Neo.core.Config` atom, automatically triggering any dependent effects.
|
85
|
+
2. **Powers Tier 1 ("Push"):** It calls the classic `afterSetText()` hook, allowing for explicit, imperative logic.
|
86
|
+
|
87
|
+
This means every config property is now an observable, atomic unit of state that works with both paradigms, giving you
|
88
|
+
the best of both worlds without any extra effort.
|
89
|
+
|
90
|
+
This upgrade set the stage for a revolutionary new way to think about component state.
|
91
|
+
|
92
|
+
## Act III: The Breakthrough - A Tale of Two States
|
93
|
+
|
94
|
+
The true power of the **Two-Tier Reactivity System** is not just that the two tiers exist, but how they work together.
|
95
|
+
With this unified engine in place, we could design a functional component model that solves one of the biggest
|
96
|
+
architectural challenges in modern UI development: the ambiguity between a component's public API and its private state.
|
97
|
+
|
98
|
+
This is best explained with a simple component:
|
99
|
+
|
100
|
+
```javascript
|
101
|
+
import {defineComponent, useConfig, useEvent} from 'neo.mjs';
|
102
|
+
|
103
|
+
export default defineComponent({
|
104
|
+
// 1. The Public API
|
105
|
+
config: {
|
106
|
+
className: 'My.Component',
|
107
|
+
greeting_: 'Hello' // This is a NAMED config
|
108
|
+
},
|
109
|
+
|
110
|
+
// 2. The Implementation
|
111
|
+
createVdom(config) {
|
112
|
+
// 3. The Private State
|
113
|
+
const [name, setName] = useConfig('World'); // This is an ANONYMOUS config
|
114
|
+
|
115
|
+
useEvent('click', () => setName(prev => prev === 'Neo' ? 'World' : 'Neo'));
|
116
|
+
|
117
|
+
return {
|
118
|
+
// 4. The Synergy
|
119
|
+
text: `${config.greeting}, ${name}!`
|
120
|
+
}
|
121
|
+
}
|
122
|
+
});
|
123
|
+
```
|
124
|
+
|
125
|
+
This small component demonstrates a paradigm that is likely unfamiliar to developers coming from other frameworks.
|
126
|
+
Let's break it down.
|
127
|
+
|
128
|
+
#### 1. Named Configs: The Public, Mutable API
|
129
|
+
|
130
|
+
The `greeting_` property is a **Named Config**. It is defined inside the `static config` block. (The trailing underscore
|
131
|
+
is the Neo.mjs convention to automatically generate a reactive getter and setter for a public property named `greeting`.)
|
132
|
+
Think of it as the component's public-facing API.
|
133
|
+
|
134
|
+
* **It's like props:** A parent component can provide an initial value for `greeting` when creating an instance.
|
135
|
+
* **It's NOT like props:** It is fully reactive and **directly mutable** from the outside.
|
136
|
+
|
137
|
+
Another component, or you directly in the browser console, can do this:
|
138
|
+
|
139
|
+
```javascript
|
140
|
+
const myComponent = Neo.get('my-component-id');
|
141
|
+
|
142
|
+
// Directly change the public API. The component will instantly re-render.
|
143
|
+
myComponent.greeting = 'Welcome';
|
144
|
+
```
|
145
|
+
|
146
|
+
This is a paradigm shift. It's not "props drilling" or complex state management. It's a direct, observable,
|
147
|
+
and reactive contract with the component.
|
148
|
+
|
149
|
+
#### 2. Anonymous Configs: The Private, Encapsulated State
|
150
|
+
|
151
|
+
The `const [name, setName] = useConfig('World')` line creates an **Anonymous Config**.
|
152
|
+
|
153
|
+
* **It's like `useState`:** It manages a piece of state that is completely private and encapsulated within the component.
|
154
|
+
* **It's NOT controllable from the outside:** No parent component or external code can see or modify the `name` state.
|
155
|
+
As shown in the example, it can only be changed via the `setName` function, which is called by the component's own
|
156
|
+
internal logic (like the `useEvent` hook).
|
157
|
+
|
158
|
+
#### 3. The Synergy: Effortless Composition
|
159
|
+
|
160
|
+
The magic happens inside the `createVdom` method. This single function, which is wrapped in a master `Neo.core.Effect`,
|
161
|
+
seamlessly reads from both state types:
|
162
|
+
|
163
|
+
* It accesses the public API via the `config` parameter. This object is a reactive proxy to the component's public API.
|
164
|
+
When the `vdomEffect` runs, simply accessing `config.greeting` is enough to register the public `greeting_` property
|
165
|
+
as a dependency.
|
166
|
+
* It accesses the private state directly from the hook's return value: `name`.
|
167
|
+
|
168
|
+
Because both `config.greeting` (a Named Config) and `name` (an Anonymous Config) are powered by the same atomic
|
169
|
+
`Neo.core.Config` engine, the master `vdomEffect` automatically tracks them both as dependencies.
|
170
|
+
|
171
|
+
If *either* an external force changes the public API (`myComponent.greeting = '...'`) or an internal event changes the
|
172
|
+
private state (`setName('Neo')`), the component's `vdomEffect` will re-run, and the UI will be updated surgically.
|
173
|
+
|
174
|
+
### Conclusion: The Best of Both Worlds
|
175
|
+
|
176
|
+
This "Tale of Two States" is more than just a new API; it's the foundation for a paradigm that solves the most
|
177
|
+
frustrating parts of modern frontend development. It delivers a developer experience that feels both radically simple
|
178
|
+
and incredibly powerful, resolving the long-standing conflict between mutability and predictability.
|
179
|
+
|
180
|
+
**1. Your State is Mutable by Design.**
|
181
|
+
In Neo.mjs, you are encouraged to work with state in the most natural way possible: direct mutation. The framework
|
182
|
+
provides several powerful methods to apply these mutations, from changing single properties to batching multiple updates
|
183
|
+
atomically, or even decoupling state changes from the render cycle entirely.
|
184
|
+
|
185
|
+
```javascript
|
186
|
+
// The recommended way is to mutate a component's public configs.
|
187
|
+
// The component's internal logic (e.g., an afterSet hook) directly mutates the vdom object, outside any effects.
|
188
|
+
// This triggers an asynchronous update cycle.
|
189
|
+
myComponent.text = 'New Title';
|
190
|
+
|
191
|
+
// For multiple changes, batch them with .set() for efficiency.
|
192
|
+
await myComponent.set({
|
193
|
+
iconCls: 'fa fa-rocket',
|
194
|
+
text : 'Launch'
|
195
|
+
});
|
196
|
+
|
197
|
+
// Change multiple configs without triggering an update cycle:
|
198
|
+
myComponent.setSilent({
|
199
|
+
iconCls: 'fa fa-cogs',
|
200
|
+
text : 'Settings'
|
201
|
+
});
|
202
|
+
// This is a powerful way to e.g. then update its parent, and trigger an aggregated update cycle for both
|
203
|
+
```
|
204
|
+
|
205
|
+
**2. The Update Process is Immutable by Default.**
|
206
|
+
Herein lies the magic. The moment you trigger an update, the framework takes a complete, serializable snapshot of your
|
207
|
+
component's current `vdom` and `vnode`. This JSON snapshot is, by its nature, an immutable copy. It's this frozen-in-time
|
208
|
+
representation that gets sent to the VDOM Worker for diffing.
|
209
|
+
|
210
|
+
**The Result: A Mutability Paradox.**
|
211
|
+
You get the best of both worlds, without compromise:
|
212
|
+
|
213
|
+
* **A Simple, Mutable Developer Experience:** You work with plain JavaScript objects and change them directly.
|
214
|
+
The framework doesn't force you into an unnatural, immutable style.
|
215
|
+
* **A Safe, Immutable Update Pipeline:** The VDOM worker operates on a predictable, isolated snapshot,
|
216
|
+
ensuring that rendering is always consistent and free from race conditions.
|
217
|
+
|
218
|
+
Because of this architecture, you are free to continue mutating the component's state in the App Worker *even while a
|
219
|
+
VDOM update is in flight*. The framework handles the queueing and ensures the next update will simply capture the new state.
|
220
|
+
|
221
|
+
This is why the entire ecosystem of manual memoization (`useMemo`, `useCallback`, `React.memo`) is rendered obsolete.
|
222
|
+
The architecture is **performant by default** because it gives you the developer ergonomics of direct mutation while
|
223
|
+
leveraging the performance and safety of an immutable, off-thread rendering process.
|
224
|
+
|
225
|
+
This is the new reality of reactivity in Neo.mjs v10. It's a system designed to let you fall in love with building,
|
226
|
+
not fighting, your components.
|
227
|
+
|
228
|
+
---
|
229
|
+
|
230
|
+
## Under the Hood: The Atomic Engine
|
231
|
+
|
232
|
+
For those who want to go deeper, let's look at the core primitives that make this all possible. The entire v10 reactivity
|
233
|
+
system is built on a foundation of three simple, powerful classes.
|
234
|
+
|
235
|
+
### `Neo.core.Config`: The Observable Box
|
236
|
+
[[Source]](https://github.com/neomjs/neo/blob/dev/src/core/Config.mjs)
|
237
|
+
|
238
|
+
At the very bottom of the stack is `Neo.core.Config`. You can think of this as an "observable box." It's a lightweight
|
239
|
+
container that holds a single value. Its only jobs are to hold that value and to notify a list of subscribers whenever
|
240
|
+
the value changes. It knows nothing about components, the DOM, or anything else.
|
241
|
+
|
242
|
+
```javascript readonly
|
243
|
+
// Example: Neo.core.Config - The Observable Box
|
244
|
+
import Config from 'neo.mjs/src/core/Config.mjs';
|
245
|
+
|
246
|
+
const myConfig = new Config('initial value');
|
247
|
+
```
|
248
|
+
|
249
|
+
### `Neo.core.Effect`: The Reactive Function
|
250
|
+
[[Source]](https://github.com/neomjs/neo/blob/dev/src/core/Effect.mjs)
|
251
|
+
|
252
|
+
An `Effect` is a function that automatically tracks its dependencies. When you create an `Effect`, you give it a function
|
253
|
+
to run. As that function runs, any `Neo.core.Config` instance whose value it reads will automatically register itself as
|
254
|
+
a dependency of that `Effect`.
|
255
|
+
|
256
|
+
If any of those dependencies change in the future, the `Effect` automatically re-runs its function. It's a self-managing
|
257
|
+
subscription that forms the basis of all reactivity in the framework.
|
258
|
+
|
259
|
+
```javascript readonly
|
260
|
+
// Example: Neo.core.Effect - The Reactive Function
|
261
|
+
import Effect from 'neo.mjs/src/core/Effect.mjs';
|
262
|
+
import Config from 'neo.mjs/src/core/Config.mjs';
|
263
|
+
|
264
|
+
let effectRunCount = 0;
|
265
|
+
const myConfig = new Config('initial value'); // Re-using myConfig from previous example
|
266
|
+
|
267
|
+
const myEffect = new Effect(() => {
|
268
|
+
effectRunCount++;
|
269
|
+
console.log('Effect ran. Current config value:', myConfig.get());
|
270
|
+
});
|
271
|
+
|
272
|
+
console.log('Initial effect run count:', effectRunCount); // Logs: Initial effect run count: 1
|
273
|
+
|
274
|
+
myConfig.set('new value'); // Console will log: Effect ran. Current config value: new value
|
275
|
+
console.log('After set, effect run count:', effectRunCount); // Logs: After set, effect run count: 2
|
276
|
+
```
|
277
|
+
|
278
|
+
### `Neo.core.EffectManager`: The Orchestrator
|
279
|
+
[[Source]](https://github.com/neomjs/neo/blob/dev/src/core/EffectManager.mjs)
|
280
|
+
|
281
|
+
This is the central singleton that makes the magic happen. The `EffectManager` keeps track of which `Effect` is currently
|
282
|
+
running. When a `Config` instance is read, it asks the `EffectManager`, "Who is watching me right now?" and adds the
|
283
|
+
current `Effect` to its list of subscribers.
|
284
|
+
|
285
|
+
### The Next Level: Mutable State, Immutable Updates
|
286
|
+
|
287
|
+
This is where the Neo.mjs reactivity model takes a significant leap beyond other frameworks. It's an architecture that
|
288
|
+
provides the intuitive ergonomics of direct mutation with the safety and performance of an immutable pipeline.
|
289
|
+
|
290
|
+
#### Synchronous State, Asynchronous DOM
|
291
|
+
|
292
|
+
First, a crucial distinction. The core `Effect` system within the App Worker runs **synchronously**, and it's built on a
|
293
|
+
principle of **atomic batching**. When you use a method like `myComponent.set({...})`, the framework automatically wraps
|
294
|
+
all state changes in a single batch. The `EffectManager` pauses execution, queues all triggered effects, and then runs
|
295
|
+
them exactly once, synchronously, after the batch is complete. This guarantees that all dependent reactive values
|
296
|
+
*within the App Worker* are updated immediately and consistently in the same turn of the event loop, with no "waiting for
|
297
|
+
the next tick" to know the state of your application logic.
|
298
|
+
|
299
|
+
However, the process of updating the actual DOM is **asynchronous**. It has to be. A call to `myComponent.update()` or a
|
300
|
+
change to a reactive config kicks off the "triangular worker communication":
|
301
|
+
|
302
|
+
1. **App Worker → VDOM Worker:** The App Worker sends a snapshot of the component's `vdom` and previous `vnode` to the VDOM Worker.
|
303
|
+
2. **VDOM Worker → Main Thread:** The VDOM Worker creates the new `vnode` tree. calculates the minimal set of changes
|
304
|
+
(the `deltas`). It sends both to the Main Thread.
|
305
|
+
3. **Main Thread → App Worker:** The Main Thread applies the `deltas` to the real DOM. It then sends the new `vnode`
|
306
|
+
back to the App Worker, which assigns it to the component (`myComponent.vnode = newVnode`) and resolves any promises
|
307
|
+
associated with the update cycle.
|
308
|
+
|
309
|
+
#### The Immutable Snapshot: The Key to the Paradox
|
310
|
+
|
311
|
+
The genius of this model lies in how the App Worker communicates with the VDOM Worker. It doesn't send a live, mutable
|
312
|
+
object. Instead, it creates a deep, JSON-serializable **snapshot** of the component's `vdom` tree.
|
313
|
+
|
314
|
+
This snapshot is, by its very nature, an **immutable copy**.
|
315
|
+
|
316
|
+
This single architectural choice unlocks the entire paradigm:
|
317
|
+
|
318
|
+
* **Developer Freedom:** As a developer in the App Worker, you are free to mutate your component's state and VDOM at
|
319
|
+
any time. You can change a property, push a new child into the `vdom.cn` array, and then immediately change another property.
|
320
|
+
* **Pipeline Safety:** The VDOM worker receives a clean, predictable, "frozen-in-time" version of the UI to work with.
|
321
|
+
It is completely isolated from any mutations that might be happening back in the App Worker while it's calculating the diff.
|
322
|
+
|
323
|
+
This completely eliminates the need for developers to manage immutability. You get a developer experience that is
|
324
|
+
fundamentally simpler and more aligned with how JavaScript objects naturally work, while the framework ensures the update
|
325
|
+
process is as safe and predictable as in the most rigidly immutable systems.
|
326
|
+
|
327
|
+
### Tying It All Together
|
328
|
+
|
329
|
+
When you define a component, the framework connects these pieces for you:
|
330
|
+
|
331
|
+
1. Every reactive config (both **Named** like `greeting_` and **Anonymous** via `useConfig`) is backed by its own
|
332
|
+
`Neo.core.Config` instance.
|
333
|
+
2. Your entire `createVdom` function is wrapped in a single, master `Neo.core.Effect`.
|
334
|
+
3. When `createVdom` runs, it reads from various `Config` instances, and the `EffectManager` ensures they are all
|
335
|
+
registered as dependencies of the master `Effect`.
|
336
|
+
4. When any of those configs change, the master `Effect` re-runs, your `createVdom` is executed again, and the UI updates.
|
337
|
+
|
338
|
+
This elegant, layered architecture is what provides the power and performance of the v10 reactivity system, delivering a
|
339
|
+
developer experience that is both simple on the surface and incredibly robust underneath.
|
340
|
+
|
341
|
+
---
|
342
|
+
|
343
|
+
## Architectural Proof: The Asynchronous Lifecycle
|
344
|
+
|
345
|
+
The Two-Tier Reactivity system isn't just for managing the state inside a single component. Its true power is revealed
|
346
|
+
when it's used to solve complex, application-wide architectural challenges. The most potent example of this is how
|
347
|
+
Neo.mjs v10 handles the "lazy-load paradox."
|
348
|
+
|
349
|
+
This is enabled by three fundamental v10 features: enhanced mixins, an async-aware lifecycle, and intelligent remote
|
350
|
+
method interception.
|
351
|
+
|
352
|
+
### 1. Enhanced Mixins: True Modules of State and Behavior
|
353
|
+
|
354
|
+
This is a core tenet of the Neo.mjs philosophy: **architectural depth enables surface-level simplicity.**
|
355
|
+
|
356
|
+
For v10, we revolutionized how our class system handles **mixins**. Previously, they could only copy methods. Now, they
|
357
|
+
can also carry their own `configs`, elevating them into truly self-contained modules of both state and behavior. This
|
358
|
+
allows us to encapsulate complex logic (e.g., for rendering or remote communication) into single, reusable modules that
|
359
|
+
can be cleanly applied to any class.
|
360
|
+
|
361
|
+
### 2. A Two-Phase, Async-Aware Lifecycle (`initAsync`)
|
362
|
+
|
363
|
+
Every class in Neo.mjs now has a two-phase initialization process. The `construct()` method runs instantly and
|
364
|
+
synchronously. It is then followed by `initAsync()`, an `async` method designed for long-running tasks.
|
365
|
+
The framework provides a reactive `isReady_` config that automatically flips to `true` only after the `initAsync()`
|
366
|
+
promise resolves.
|
367
|
+
|
368
|
+
```javascript readonly
|
369
|
+
// Example: Two-Phase, Async-Aware Lifecycle (initAsync)
|
370
|
+
import Base from 'neo.mjs/src/core/Base.mjs';
|
371
|
+
|
372
|
+
class MyAsyncService extends Base {
|
373
|
+
static config = {
|
374
|
+
className: 'My.AsyncService',
|
375
|
+
// isReady_ is automatically managed by the framework
|
376
|
+
}
|
377
|
+
|
378
|
+
async initAsync() {
|
379
|
+
await super.initAsync(); // Mandatory: Await the parent's initAsync
|
380
|
+
console.log('initAsync started. Simulating async work...');
|
381
|
+
await this.timeout(1000); // Simulate async work
|
382
|
+
console.log('initAsync finished.');
|
383
|
+
// isReady will flip to true *after* this promise resolves,
|
384
|
+
// triggering afterSetIsReady()
|
385
|
+
}
|
386
|
+
|
387
|
+
// This hook is called by the framework when isReady_ changes
|
388
|
+
afterSetIsReady(value, oldValue) {
|
389
|
+
super.afterSetIsReady(value, oldValue); // Call super if it exists
|
390
|
+
if (value === true) {
|
391
|
+
console.log('MyAsyncService is now ready!');
|
392
|
+
}
|
393
|
+
}
|
394
|
+
}
|
395
|
+
|
396
|
+
const service = Neo.create(MyAsyncService);
|
397
|
+
console.log('Service created. isReady (initial):', service.isReady); // Logs: Service created. isReady (initial): false
|
398
|
+
// Console will then log:
|
399
|
+
// initAsync started. Simulating async work...
|
400
|
+
// initAsync finished.
|
401
|
+
// MyAsyncService is now ready!
|
402
|
+
```
|
403
|
+
|
404
|
+
### 3. Intelligent Remote Method Interception
|
405
|
+
|
406
|
+
The framework's `RemoteMethodAccess` mixin is aware of this `isReady` state. When a remote call arrives for a main
|
407
|
+
thread addon that is not yet ready, it doesn't fail. Instead, it **intercepts the call**.
|
408
|
+
|
409
|
+
Let's walk through a practical example: using a powerful, but large, third-party charting library like AmCharts on the
|
410
|
+
main thread.
|
411
|
+
|
412
|
+
* Loading it upfront is bad for performance; it blocks the initial application load.
|
413
|
+
* Lazy-loading it creates a classic race condition: what happens if your App Worker sends a command to create a chart
|
414
|
+
*before* the AmCharts library has finished downloading and initializing?
|
415
|
+
|
416
|
+
In a traditional framework, this would require complex, manual state management. In Neo.mjs, the solution is an
|
417
|
+
elegant and automatic feature of the core reactivity system.
|
418
|
+
|
419
|
+
1. An `AmChart` wrapper component in the App Worker is mounted and sends a remote command: `Neo.main.addon.AmCharts.create(...)`.
|
420
|
+
2. On the main thread, the `AmCharts` addon receives the call. It checks its own `isReady` state, which is `false`.
|
421
|
+
3. Instead of executing the `create` method, it **caches the request** in an internal queue.
|
422
|
+
4. Crucially, it **immediately triggers its own `initAsync()` process**, which begins downloading the AmCharts library files.
|
423
|
+
5. Once the files are loaded, `initAsync()` resolves, and the addon's `isReady` flag flips to `true`.
|
424
|
+
6. The `afterSetIsReady()` hook—a standard feature of the reactivity system—automatically fires, processes the queue of
|
425
|
+
cached calls, and finally creates the chart.
|
426
|
+
|
427
|
+
The developer in the App Worker is completely shielded from this complexity. They simply call a method, and the framework
|
428
|
+
guarantees it will be executed correctly and in the right order. There are no manual loading flags, no race conditions,
|
429
|
+
and no complex queueing logic to write.
|
430
|
+
|
431
|
+
```javascript readonly
|
432
|
+
// Example: Intelligent Remote Method Interception (Simplified)
|
433
|
+
|
434
|
+
// --- Main Thread Addon ---
|
435
|
+
// This addon runs on the Main Thread and simulates loading a heavy library.
|
436
|
+
import AddonBase from 'neo.mjs/src/main/addon/Base.mjs';
|
437
|
+
|
438
|
+
class MyHeavyLibraryAddon extends AddonBase {
|
439
|
+
static config = {
|
440
|
+
className: 'Neo.main.addon.HeavyLibraryAddon',
|
441
|
+
// List methods that should be intercepted if the addon is not ready.
|
442
|
+
// The base class's onInterceptRemotes() will cache these calls.
|
443
|
+
interceptRemotes: ['loadResource', 'processData'],
|
444
|
+
// Expose the methods to the App Worker.
|
445
|
+
remotes: {
|
446
|
+
app: ['loadResource', 'processData']
|
447
|
+
}
|
448
|
+
}
|
449
|
+
|
450
|
+
// Subclasses must implement loadFiles() to load external resources.
|
451
|
+
// This method is awaited by initAsync().
|
452
|
+
async loadFiles() {
|
453
|
+
console.log('Addon: Simulating heavy library/resource loading...');
|
454
|
+
await this.timeout(1500); // Simulate async work
|
455
|
+
console.log('Addon: Heavy library/resource loaded.');
|
456
|
+
}
|
457
|
+
|
458
|
+
// Remote methods that can be called from the App Worker.
|
459
|
+
loadResource(url) {
|
460
|
+
console.log('Addon: Executing loadResource for:', url);
|
461
|
+
return `Resource from ${url} loaded!`;
|
462
|
+
}
|
463
|
+
|
464
|
+
processData(data) {
|
465
|
+
console.log('Addon: Executing processData with:', data);
|
466
|
+
return `Data processed: ${JSON.stringify(data)}`;
|
467
|
+
}
|
468
|
+
|
469
|
+
// The afterSetIsReady method (from AddonBase) will automatically
|
470
|
+
// process any queued remote calls once this.isReady becomes true.
|
471
|
+
}
|
472
|
+
|
473
|
+
// --- Simulation of App Worker making calls to Main Thread Addon ---
|
474
|
+
(async () => {
|
475
|
+
console.log('--- Simulation Start ---');
|
476
|
+
|
477
|
+
// These calls are made before the addon's initAsync (and thus loadFiles) completes.
|
478
|
+
// They will be intercepted and queued by the addon.Base logic.
|
479
|
+
console.log('Simulating App Worker call: loadResource (before addon ready)');
|
480
|
+
const result1Promise = Neo.main.addon.HeavyLibraryAddon.loadResource('/api/data/resource1');
|
481
|
+
|
482
|
+
console.log('Simulating App Worker call: processData (before addon ready)');
|
483
|
+
const result2Promise = Neo.main.addon.HeavyLibraryAddon.processData({ value: 42, type: 'example' });
|
484
|
+
|
485
|
+
// The promises will resolve once the addon becomes ready and processes the queued calls.
|
486
|
+
const [result1, result2] = await Promise.all([result1Promise, result2Promise]);
|
487
|
+
|
488
|
+
console.log('Result from loadResource:', result1);
|
489
|
+
console.log('Result from processData:', result2);
|
490
|
+
|
491
|
+
console.log('--- Simulation End ---');
|
492
|
+
})();
|
493
|
+
```
|
494
|
+
|
495
|
+
**Explanation of this example's relevance:**
|
496
|
+
This snippet demonstrates how Neo.mjs handles remote method calls to Main Thread addons that might not be immediately ready.
|
497
|
+
|
498
|
+
* `MyHeavyLibraryAddon` (Main Thread Addon):
|
499
|
+
* Extends AddonBase, inheriting the core logic for initAsync, isReady_, onInterceptRemotes, and afterSetIsReady.
|
500
|
+
* Defines interceptRemotes to specify which methods should be queued if the addon isn't ready.
|
501
|
+
* Implements loadFiles() to simulate the asynchronous loading of external resources (e.g., a large third-party library).
|
502
|
+
* Exposes loadResource and processData as remote methods that can be called from the App Worker.
|
503
|
+
* Simulation of App Worker Calls:
|
504
|
+
* Shows how an App Worker component would make calls to the Main Thread addon using Neo.main.addon.AddonClassName.methodName().
|
505
|
+
* These calls are made before the MyHeavyLibraryAddon has completed its initAsync (and loadFiles).
|
506
|
+
* The AddonBase's onInterceptRemotes automatically intercepts these calls, queues them, and returns a promise that will resolve later.
|
507
|
+
* Once MyHeavyLibraryAddon finishes its initAsync (simulated by loadFiles completing), its isReady_ config flips to true.
|
508
|
+
* The AddonBase's afterSetIsReady then automatically processes the queued calls, resolving the original promises.
|
509
|
+
|
510
|
+
This is the ultimate expression of the Neo.mjs philosophy: using the core reactivity engine not just to render UIs, but
|
511
|
+
to orchestrate the entire application's asynchronous state and logic. It's the final proof that a robust reactive foundation
|
512
|
+
doesn't just simplify your code — it makes entirely new patterns of development possible.
|
513
|
+
|
514
|
+
---
|
515
|
+
|
516
|
+
## The Neo.mjs v10 Blog Post Series
|
517
|
+
|
518
|
+
1. [A Frontend Love Story: Why the Strategies of Today Won't Build the Apps of Tomorrow](./v10-post1-love-story.md)
|
519
|
+
2. Deep Dive: Named vs. Anonymous State - A New Era of Component Reactivity
|
520
|
+
3. [Beyond Hooks: A New Breed of Functional Components for a Multi-Threaded World](./v10-deep-dive-functional-components.md)
|
521
|
+
4. [Deep Dive: The VDOM Revolution - JSON Blueprints & Asymmetric Rendering](./v10-deep-dive-vdom-revolution.md)
|
522
|
+
5. [Deep Dive: The State Provider Revolution](./v10-deep-dive-state-provider.md)
|