aktion-runtime 0.5.0 → 0.5.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.
@@ -230,6 +230,48 @@ brackets doesn't matter.
230
230
  `effect { ... }` (no brackets) is equivalent to `effect [on:mount] { ... }` —
231
231
  both run the body once on mount.
232
232
 
233
+ ### Scope — top-level vs. component-local
234
+ An effect can live at the program top level OR inside a
235
+ `component Name() { … }` body. The syntax is identical; only the
236
+ lifecycle differs:
237
+
238
+ - **Top-level** — mounted once when the program parses, torn down on
239
+ `setResponse` / `clear()`. Use for global concerns (analytics,
240
+ app-wide shortcuts, hydration of shared atoms).
241
+ - **Component-local** — mounted once per component instance on its
242
+ first render, torn down when the instance disappears from the tree.
243
+ Each instance gets its own timers, watched-atom subscriptions, and
244
+ `cleanup(fn)` registrations. Use for per-instance work (per-row
245
+ polling, modal focus management, observers attached to a widget).
246
+
247
+ ```
248
+ _app_ = App()
249
+ $value = 10
250
+
251
+ # Top-level — one shared interval for the whole program.
252
+ effect [on:every(1000)] {
253
+ $value = $value + 1
254
+ }
255
+
256
+ component App() {
257
+ return Box([Text("Value: " + $value)])
258
+ }
259
+ ```
260
+
261
+ ```
262
+ _app_ = App()
263
+ $value = 10
264
+
265
+ component App() {
266
+ # Component-local — interval starts on first render and is cleared
267
+ # automatically when the App instance leaves the tree.
268
+ effect [on:every(1000)] {
269
+ $value = $value + 1
270
+ }
271
+ return Box([Text("Value: " + $value)])
272
+ }
273
+ ```
274
+
233
275
  ### Examples
234
276
 
235
277
  ```
@@ -1,4 +1,5 @@
1
1
  import { EvaluationContext } from '../runtime/evaluator.js';
2
+ import { EffectDeclaration } from '../parser/types.js';
2
3
  import { StateStore } from '../runtime/state.js';
3
4
  import { Router } from '../runtime/router.js';
4
5
  import { ComponentLibrary } from '../library/types.js';
@@ -18,6 +19,20 @@ export interface RenderOptions {
18
19
  * render as `[unknown component: <Name>]` so the failure is visible.
19
20
  */
20
21
  evaluationContext?: () => EvaluationContext;
22
+ /**
23
+ * Mount `effect [ ...deps ] { … }` declarations discovered inside a
24
+ * `component { … }` body. Called by the renderer after every render of
25
+ * the instance; the implementation is expected to be idempotent so
26
+ * re-renders are no-ops once the effects are mounted. The host wires
27
+ * this to the same `EffectRunner` that handles top-level effects.
28
+ */
29
+ mountInstanceEffects?: (instanceKey: string, decls: ReadonlyArray<EffectDeclaration>, getCtx: () => EvaluationContext) => void;
30
+ /**
31
+ * Tear down every per-instance effect mounted under `instanceKey`.
32
+ * Invoked when the component instance disappears from the render tree
33
+ * (between two `beginRender`/`endRender` passes).
34
+ */
35
+ unmountInstanceEffects?: (instanceKey: string) => void;
21
36
  }
22
37
  export declare class Renderer {
23
38
  private options;
@@ -36,6 +51,13 @@ export declare class Renderer {
36
51
  private readonly instanceDisposers;
37
52
  /** Instance paths seen during the current render — used to GC stale state. */
38
53
  private aliveInstances;
54
+ /**
55
+ * User-declared component instances that currently hold per-instance
56
+ * effects (mounted via `mountInstanceEffects`). Tracked separately from
57
+ * `instanceStates` so the renderer can fire `unmountInstanceEffects` on
58
+ * GC even when an instance never registered `useInstanceState`.
59
+ */
60
+ private readonly instancesWithEffects;
39
61
  constructor(options: RenderOptions);
40
62
  /**
41
63
  * Swap the component library backing this renderer. Used when the host
@@ -25,11 +25,30 @@ export declare class EffectRunner {
25
25
  /** Get any errors raised at mount-time (denied capabilities, parse issues). */
26
26
  getErrors(): ReadonlyArray<string>;
27
27
  /**
28
- * Mount every effect declaration in `decls`. Idempotent: declarations
29
- * that are already mounted under the same name are left alone, those
30
- * that vanish from the new program are torn down.
28
+ * Mount every top-level effect declaration in `decls`. Idempotent:
29
+ * declarations that are already mounted under the same name are left
30
+ * alone, those that vanish from the new program are torn down.
31
+ *
32
+ * Only touches global (top-level) effects. Per-instance effects mounted
33
+ * inside `component { … }` bodies are managed via `syncInstanceEffects`
34
+ * / `unmountInstance` and are not affected by this call.
31
35
  */
32
36
  syncEffects(decls: ReadonlyArray<EffectDeclaration>, getCtx: () => EvaluationContext): void;
37
+ /**
38
+ * Mount per-instance effects discovered inside a `component { … }` body.
39
+ * Idempotent: re-rendering the same instance with the same effect set is
40
+ * a no-op; effects that vanished from the body since the last render are
41
+ * torn down. Effects belonging to other instances are untouched.
42
+ */
43
+ syncInstanceEffects(instanceKey: string, decls: ReadonlyArray<EffectDeclaration>, getCtx: () => EvaluationContext): void;
44
+ /**
45
+ * Tear down every effect that belongs to the given component instance
46
+ * (i.e. mounted via `syncInstanceEffects(instanceKey, …)`). Called by
47
+ * the renderer when an instance disappears from the tree so timers,
48
+ * interval handles, and state subscriptions don't outlive the
49
+ * component the user can see.
50
+ */
51
+ unmountInstance(instanceKey: string): void;
33
52
  reset(): void;
34
53
  private mount;
35
54
  private unmount;
@@ -85,6 +85,17 @@ export interface EvaluationContext {
85
85
  componentDecls: Map<string, ComponentDeclaration>;
86
86
  /** Effect declarations (`effect [ ...deps ] { ... }`), keyed by auto-generated name. */
87
87
  effectDecls: Map<string, EffectDeclaration>;
88
+ /**
89
+ * Stack of per-component-invocation effect collection frames.
90
+ *
91
+ * When this stack is non-empty, an `EffectDeclaration` encountered while
92
+ * walking a block body is appended to the top frame instead of being
93
+ * registered globally on `effectDecls`. The renderer drains the frame
94
+ * immediately after `evaluateUserComponent` returns so it can mount the
95
+ * declarations on a per-instance scope (instead of globally, once per
96
+ * program).
97
+ */
98
+ componentEffectStack: EffectDeclaration[][];
88
99
  /** Action declarations (`action Foo() { ... }`). */
89
100
  actionDecls: Map<string, ActionDeclaration>;
90
101
  /** HTTP runtime (`http({...})` calls + interceptor configuration). */
@@ -135,6 +146,18 @@ export declare function resolveStateAlias(ctx: EvaluationContext, name: string):
135
146
  */
136
147
  export declare function planProgram(program: Program, ctx: EvaluationContext): void;
137
148
  export declare function evaluate(expr: Expression, ctx: EvaluationContext): unknown;
149
+ /**
150
+ * Result of `evaluateUserComponent`. `value` is the body's last
151
+ * expression value (a `ComponentNode`, another `UserComponentNode`, or a
152
+ * primitive) that the renderer will materialise. `effects` is the list of
153
+ * `effect [ ...deps ] { … }` declarations discovered inside the body —
154
+ * the renderer hands them to the host's `EffectRunner` so they mount on
155
+ * a per-instance scope and tear down when the instance unmounts.
156
+ */
157
+ export interface EvaluatedUserComponent {
158
+ value: unknown;
159
+ effects: ReadonlyArray<EffectDeclaration>;
160
+ }
138
161
  /**
139
162
  * Evaluate a user-declared component body in a fresh per-instance scope.
140
163
  * Called by the renderer once the stable instance key is known so
@@ -146,6 +169,7 @@ export declare function evaluate(expr: Expression, ctx: EvaluationContext): unkn
146
169
  *
147
170
  * Returns the body's last-expression value (typically a `ComponentNode`
148
171
  * the renderer can hand to the library, or another `UserComponentNode`
149
- * to expand recursively).
172
+ * to expand recursively) plus any `effect [ ...deps ] { … }` declarations
173
+ * discovered inside the body that the renderer must mount per-instance.
150
174
  */
151
- export declare function evaluateUserComponent(node: UserComponentNode, ctx: EvaluationContext, instanceKey: string): unknown;
175
+ export declare function evaluateUserComponent(node: UserComponentNode, ctx: EvaluationContext, instanceKey: string): EvaluatedUserComponent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aktion-runtime",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Aktion is a single web component that turns a compact, streaming-first DSL into a rich, interactive UI inside its shadow DOM. Works in React, Vue, Angular, Svelte, plain HTML — or no framework at all.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",