@jarenjs/app 0.34.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/README.md ADDED
@@ -0,0 +1,197 @@
1
+ # @jarenjs/app
2
+
3
+ Applications as JSON documents. This package rebuilds [hyperapp](https://github.com/jorgebucaran/hyperapp)'s dispatch loop on the Jaren suite and pushes its philosophy — *everything is data* — the rest of the way: hyperapp made effects and subscriptions data but kept actions and views as JavaScript functions; here **the whole application is one JSON value**:
4
+
5
+ | Slot | Written as | Compiled by |
6
+ |---|---|---|
7
+ | `state` | a JSON document | — |
8
+ | `view` | a [JSLT stylesheet](../json/docs/JSLT-FORMAT.md) producing [vnodes](../view/docs/VIEW-FORMAT.md) | `@jarenjs/json/jslt` |
9
+ | `actions` | named [query documents](../json/docs/QUERY-FORMAT.md) producing transitions | `@jarenjs/json/query` |
10
+ | transitions | next state, or an RFC 6902 **JSON Patch** | `@jarenjs/json/patch` |
11
+ | `subs` | entries with an EBV `when` query deciding liveness | `@jarenjs/json/query` |
12
+
13
+ Everything compiles **once** at `createApp` time; the running loop only calls specialized closures — the same design contract as every other Jaren engine. JavaScript enters at named, registered boundaries only: effect and subscription handlers, the `compileTypeTest` hook, the `validateState` invariant hook. No `eval`, CSP-safe, and the entire app is serializable: snapshot it, diff it, ship it over the wire, or have a constrained decoder generate it — an LLM cannot emit a syntactically invalid program in this framework.
14
+
15
+ The document contract is [docs/APP-FORMAT.md](docs/APP-FORMAT.md).
16
+
17
+ ## A complete app
18
+
19
+ ```javascript
20
+ import { createApp } from '@jarenjs/app';
21
+
22
+ const app = createApp({
23
+ "$app": "0.1",
24
+ "state": { "count": 0 },
25
+
26
+ "view": [
27
+ { "match": "$", "body":
28
+ ["main", {},
29
+ ["h1", {}, "Count: ", "$.count"],
30
+ ["button", { "on": { "click": "inc" } }, "+"],
31
+ ["button", { "on": { "click": { "action": "add", "with": 10 } } }, "+10"]] }
32
+ ],
33
+
34
+ "actions": {
35
+ "inc": { "patch": [{ "op": "replace", "path": "/count",
36
+ "value": { "$add": ["$.count", 1] } }] },
37
+ "add": { "patch": [{ "op": "replace", "path": "/count",
38
+ "value": { "$add": ["$.count", "$payload"] } }] }
39
+ }
40
+ }, { node: document.getElementById('app') });
41
+ ```
42
+
43
+ The view is a JSLT stylesheet: rules match state by location (JSONPath) and shape (JSON Schema, via `compileTypeTest`), bodies are query documents producing vnodes, and `$path`/`$root` are in scope — a rule rendering `/todos/3` can embed its own pointer in an event binding, which is why there are no payload-creator functions anywhere.
44
+
45
+ ## Actions and transitions
46
+
47
+ An action document is evaluated with `$` bound to the current state, `$event` bound to serializable event data (`{ type, value, checked, key }`) and `$payload` bound to the binding's `with` value. A binding can request more of the event declaratively — `{ "action": "selectRow", "with": { "id": "$.id" }, "event": ["shiftKey", "ctrlKey"] }` adds those members to `$event` from a closed allow-list of serializable fields (modifier keys, pointer coordinates, selection offsets, ...), and the `eventFields` option registers named JS extractors for anything the allow-list can't serialize (APP-FORMAT §3.1/§5.4). It returns a **transition**:
48
+
49
+ ```json
50
+ { "state": "the next state, whole — optional",
51
+ "patch": "an RFC 6902 patch applied copy-on-write — optional",
52
+ "effects": [{ "run": "http", "with": { "url": "/api" } }] }
53
+ ```
54
+
55
+ Returning nothing is a no-op. State updates are immutable and structure-sharing (the patch engine's copy-on-write), which feeds the renderer's `oldVnode === newVnode` fast path.
56
+
57
+ ## Effects and subscriptions
58
+
59
+ Side effects stay at the edges, as registered handlers:
60
+
61
+ ```javascript
62
+ createApp(doc, {
63
+ node,
64
+ effects: {
65
+ http: (props, dispatch) =>
66
+ fetch(props.url).then((r) => r.json()).then((data) => dispatch(props.done, data)),
67
+ },
68
+ subs: {
69
+ interval: (props, dispatch) => {
70
+ const id = setInterval(() => dispatch(props.tick), props.ms);
71
+ return () => clearInterval(id); // cleanup
72
+ },
73
+ },
74
+ });
75
+ ```
76
+
77
+ Subscription entries in the document carry a `when` query; after every state change the loop starts and stops handlers to match (`{ "run": "interval", "with": { "ms": 1000, "tick": "tick" }, "when": "$.running" }`). A broken `when` fails **closed** — a broken rule must never keep side effects alive — and is reported through `onError`.
78
+
79
+ ## Async tasks
80
+
81
+ Async work follows a documented convention — **correctness lives in state, cancellation lives in the host** — with one shipped helper packaging the host half:
82
+
83
+ ```javascript
84
+ import { createTaskEffect } from '@jarenjs/app';
85
+
86
+ // state: "tasks": { "list": { "id": 0, "status": "idle", "error": null } }
87
+ // start: patch increments /tasks/list/id AND the effect's with.id
88
+ // carries the same increment expression (evaluated pre-transition!)
89
+ // finish: the completion action guards { "$eq": ["$payload.id", "$.tasks.list.id"] }
90
+ // and yields the empty sequence for anything stale
91
+ effects: {
92
+ http: createTaskEffect((props, signal) =>
93
+ fetch(props.url, { signal }).then((r) => r.json())),
94
+ }
95
+ ```
96
+
97
+ The helper aborts a slot's in-flight predecessor (`mode: "switch"`, the default — `"exhaust"`, `"concat"` and `"parallel"` pick the other per-slot concurrency semantics), dispatches `done` with `{ id, result }` on resolve and `fail ?? done` with `{ id, error }` (a string) on failure, and dispatches nothing for an abort. The abort is only an optimization — an aborted request may already have resolved — so the state-side id guard is the guarantee: out-of-order and polling responses are rejected by construction. The handler exposes `cancel(slot)`/`cancelAll()`/`dispose()`; `app.destroy()` disposes it automatically. The full convention, with a runnable worked example the test suite executes verbatim, is [docs/TASKS.md](docs/TASKS.md).
98
+
99
+ ## One FIFO queue, observable transactions
100
+
101
+ Every dispatch — from the DOM, an effect, a listener, a subscription or a widget — is one **transaction** on one FIFO queue; nested dispatches queue, never interleave, so every listener observes every transaction in the same order with the state that transaction produced ([APP-FORMAT §8](docs/APP-FORMAT.md)). Boot is a transaction too: a failure in renderer construction, the initial-state check, a starting subscription, the first frame or the queued boot work rolls back everything acquired (effect handlers disposed, container emptied) and throws `JA0007`. `app.stop()` halts the loop one-way (no resume); `app.destroy()` is the terminal teardown — subscriptions cleaned, effect handlers disposed, widgets unmounted exactly once, the container emptied.
102
+
103
+ `app.observe(fn)` streams one bounded JSON record per transaction (`seq`, `action`, `source`, `status`, `changedPaths`, `scheduledEffects`, `durationMs`, `errorCode` — payloads only with the `capturePayloads` opt-in), and `createTransactionLog({ limit, redact })` packages the ring buffer with a redaction hook for support exports. `createFocusEffect({ container })` bridges focus, text selection and measurement through post-render `data-ref` intents, so accessible dialogs restore focus without a DOM node ever entering state (§8.4).
104
+
105
+ ## Widgets — imperative islands, declarative everything else
106
+
107
+ An irreducibly imperative island — a virtualized grid, a canvas, a map — lives behind a **registered widget** ([VIEW-FORMAT §7](../view/docs/VIEW-FORMAT.md)); the app document stays JSON and the app option only names the boundary, like `effects` and `subs`:
108
+
109
+ ```javascript
110
+ createApp({
111
+ state: { grid: { rows: hugeArray, scrollTop: 0 }, selected: null },
112
+ view: [
113
+ { "match": "$", "body": ["main", {}, { "$apply": "$.grid" }] },
114
+ { "match": "$.grid", "body":
115
+ ["jaren-widget", { "name": "virtual-list", "key": "list", "props": {
116
+ "rows": "$.rows", "scrollTop": "$.scrollTop",
117
+ "binding": { "action": "select", "with": {} } } }] },
118
+ ],
119
+ actions: { select: { patch: [{ op: 'add', path: '/selected', value: '$payload.id' }] } },
120
+ }, { node, widgets: { 'virtual-list': virtualListWidget } });
121
+ ```
122
+
123
+ Jaren owns state and orchestration; the widget owns its DOM. Its `props` come from the view stylesheet and are compared **by reference** — the JSLT memo means a transition that doesn't touch the widget's state slice never calls into the widget at all. The widget dispatches by composing runtime data into the binding its props carry and handing it to `emit`, which flows through the ordinary binding path — `with` payloads and `event` extraction included — and it is unmounted deterministically when it leaves the tree.
124
+
125
+ ### A ready-made splitter, and a document store
126
+
127
+ Two IDE-shaped primitives ship ready to bind, so a two-pane surface (the studio, the play playground) doesn't re-implement them:
128
+
129
+ - **`createSplitterWidget({ action, grid, rail, cssVar, min, max, step })`** — a drag handle over a pane boundary. It drives a CSS ratio variable *live* during a drag (no per-move dispatch — that would flood the transaction log and undo) and commits the ratio through `action` on pointer-up only, plus keyboard resize as an ARIA separator. Register it like any widget; parameterize the grid/rail selectors, the CSS variable and the commit action so each surface binds its own.
130
+ - **`createDocStore({ storage, key })`** — a keyed `save`/`load`/`remove`/`names`/`all` CRUD over an injected `storage` (`localStorage` in the browser, an in-memory object in tests), so the package never touches `localStorage` itself. Paired with **`encodeShare(snapshot)`** / **`decodeShare(token)`**, a Unicode-safe base64url share-link codec (a corrupt token decodes to `null`, never a throw), it is the new/save/load/delete/share pattern behind the studio and play surfaces.
131
+
132
+ ## Invariants the model can't cheat
133
+
134
+ ```javascript
135
+ import { JarenValidator } from '@jarenjs/validate';
136
+ import { createTypeTestCompiler } from '@jarenjs/validate/query';
137
+
138
+ const validate = new JarenValidator().compile(stateSchema); // may carry $query assertions
139
+
140
+ createApp(doc, {
141
+ node,
142
+ compileTypeTest: createTypeTestCompiler(), // enables schema matches & $valid/$assert/$as
143
+ validateState: (state) => validate(state), // every transition checked; rejected = not applied
144
+ });
145
+ ```
146
+
147
+ `validateState` runs against every candidate next state; a rejection blocks the transition (fail closed) and surfaces as a `JA2005` error with the validator's structured errors in `detail`. The app package itself never imports the validator — the same boundary discipline as `@jarenjs/forms`.
148
+
149
+ ## The standard forms stylesheet
150
+
151
+ The marquee integration: render any [`@jarenjs/forms`](../forms) model with **zero hand-written render code**. `createFormView()` returns a plain-JSON JSLT rule set that dispatches over a `buildFormViewModel` tree by *shape* (JSONPath filter selectors on each node's `control`), and `createFormActions()` returns the matching action documents that write keystrokes back into the state — choosing the correct RFC 6902 op per node (`replace` for array elements, where `add` would insert; `add` for object members, where it means set-or-replace).
152
+
153
+ ```javascript
154
+ import { createApp, createFormView, createFormActions, formEventFields } from '@jarenjs/app';
155
+ import { buildFormModel, compileFormRules, createInitialData, buildFormViewModel } from '@jarenjs/forms';
156
+
157
+ const model = buildFormModel(schema);
158
+ const rules = compileFormRules(model);
159
+
160
+ const app = createApp({
161
+ state: { data: createInitialData(model) },
162
+ view: [
163
+ ...createFormView(), // the shipped rule set
164
+ { match: '$', body: ['main', {}, { $apply: '$.form' }] },
165
+ ],
166
+ actions: createFormActions({ dataPointer: '/data' }),
167
+ }, {
168
+ node: document.getElementById('app'),
169
+ eventFields: { ...formEventFields() }, // decode the JSON-carrying controls
170
+ viewModel: (state) => ({ // the derivation boundary
171
+ form: buildFormViewModel(model, state.data, { rules, validateFields: true }),
172
+ }),
173
+ });
174
+ ```
175
+
176
+ Schema in, live form out: text/email/number/date/color inputs, textareas, checkboxes, selects with precomputed options, nested object fieldsets, arrays with add/remove buttons, inline errors, and `x-form` visibility/enablement/computed reacting per keystroke. The `viewModel` option is the general **derivation boundary**: it maps state to the view stylesheet's input before every render, so JS-computed derivations enter the render path without ever entering the state. A DOM control's value is a string, and two controls carry something else: a select over a non-string enum, and the `json` editor over a structured value. Both round-trip through JSON text and decode it in `formEventFields()`, the format's one sanctioned place for host JavaScript at the DOM boundary (APP-FORMAT §5.4) — **register it or those two controls write nothing**. Remaining 0.1 limits (documented in `src/forms.js`): a cleared number input writes `null`, and arrays need to exist in the data (give them `default: []` in the schema).
177
+
178
+ ## Headless and server-side
179
+
180
+ Without a `node`, the app runs headless: `getVnode()` returns the current view output for any renderer, and SSR is one composition:
181
+
182
+ ```javascript
183
+ import { renderToString } from '@jarenjs/view';
184
+ renderToString(createApp(doc).getVnode());
185
+ ```
186
+
187
+ ## API
188
+
189
+ `createApp(appDoc, options)` → `{ dispatch(name, payload?), getState(), getVnode(), render(), subscribe(listener), observe(observer), stop(), destroy() }`
190
+
191
+ Also exported: `compileActions`, `compileSubs`, `createFormView`, `createFormActions`, `formEventFields`, `createTaskEffect`, `createFocusEffect`, `createTransactionLog`, `createSplitterWidget`, `createDocStore`, `encodeShare`, `decodeShare`, and the error classes (`AppCompileError`, `AppRuntimeError`, `HostValueError`, `toError`, `APP_CODES`).
192
+
193
+ Options: `node`, `document`, `effects`, `subs`, `eventFields` (named `$event` field extractors), `widgets` (registered widget definitions, forwarded to the renderer), `compileTypeTest`, `validateState`, `viewModel`, `onError` (default rethrows), `schedule` (render batching; default microtask — pass `(f) => f()` for synchronous tests). Compile failures throw `AppCompileError` (`JA0xxx`, with a `docPath` into the app document); runtime failures route `AppRuntimeError` (`JA2xxx`) through `onError`. The full code table is in [APP-FORMAT.md](docs/APP-FORMAT.md) §10.
194
+
195
+ ## Development
196
+
197
+ Unit tests live in `test/app/` at the repository root (`npm run test:app`). See [ROADMAP](../../docs/ROADMAP.md) for what's next: dirty-path-pruned re-rendering and time-travel tooling over the action log.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file Compiling the transition vocabulary of an app document: named
3
+ * action documents and subscription entries. Everything here runs once,
4
+ * at `createApp` time — the loop only ever calls compiled closures.
5
+ *
6
+ * An **action document** is a Jaren JSON Query document (QUERY-FORMAT.md)
7
+ * evaluated with `$` bound to the current state and two externals:
8
+ *
9
+ * - `$event` — the serializable event data (`{ type, value, checked,
10
+ * key }`) when the dispatch came from the DOM, else `null`
11
+ * - `$payload` — the binding's `with` value, else `null`
12
+ *
13
+ * It returns a **transition object** (or nothing for a no-op):
14
+ *
15
+ * - `state` — the next state, whole
16
+ * - `patch` — an RFC 6902 JSON Patch applied to the state (after
17
+ * `state`, when both are present)
18
+ * - `effects` — `[{ "run": name, "with"?: props }]` handed to the
19
+ * registered effect handlers
20
+ */
21
+ export type ActionCompileOptions = {
22
+ /**
23
+ * Enables `$valid`/`$assert`/`$as` schema operators inside action
24
+ * documents; typically `createTypeTestCompiler()` from
25
+ * `@jarenjs/validate/query`.
26
+ */
27
+ compileTypeTest?: (schema: any, docPath: string) => ((value: any) => boolean);
28
+ };
29
+ /**
30
+ * The compile-time options shared by every embedded query document.
31
+ * @typedef {Object} ActionCompileOptions
32
+ * @property {(schema: any, docPath: string) => ((value: any) => boolean)} [compileTypeTest]
33
+ * Enables `$valid`/`$assert`/`$as` schema operators inside action
34
+ * documents; typically `createTypeTestCompiler()` from
35
+ * `@jarenjs/validate/query`.
36
+ */
37
+ /**
38
+ * Compile the `actions` member of an app document.
39
+ * @param {any} actions
40
+ * @param {ActionCompileOptions} options
41
+ * @returns {Map<string, any>} action name → compiled query
42
+ */
43
+ export declare function compileActions(actions: any, options: ActionCompileOptions): Map<string, any>;
44
+ export type CompiledSub = {
45
+ /**
46
+ * - The registered handler name.
47
+ */
48
+ run: string;
49
+ /**
50
+ * - The entry's `with` value (`null` when absent).
51
+ */
52
+ props: any;
53
+ /**
54
+ * - Compiled liveness query, or `null` (always live).
55
+ */
56
+ when: any;
57
+ /**
58
+ * - Compiled props query evaluated against the
59
+ * state (with `$item` bound per instance under `for`), or `null`.
60
+ */
61
+ withQuery: any;
62
+ /**
63
+ * - Compiled restart-key query, or `null` (the
64
+ * key derives from the resolved props by value).
65
+ */
66
+ keyQuery: any;
67
+ /**
68
+ * - Compiled fan-out query yielding the item
69
+ * set, or `null` (a single-instance subscription).
70
+ */
71
+ forQuery: any;
72
+ };
73
+ /**
74
+ * Compile the `subs` member of an app document:
75
+ * `[{ "run": name, "with"?: props, "when"?: <EBV query>,
76
+ * "withQuery"?: query, "key"?: query, "for"?: query }]`.
77
+ *
78
+ * `with` is verbatim data; `withQuery` derives the props from the state
79
+ * and makes the subscription DYNAMIC — it restarts when its resolved
80
+ * key changes (`key` overrides the derived-from-props default). `for`
81
+ * fans the declaration out to one instance per item of its result. The
82
+ * combinations that would make one entry ambiguous are JA0008: `with`
83
+ * beside `withQuery`, `with` beside `for`, and `key` without either
84
+ * `withQuery` or `for`.
85
+ * @param {any} subs
86
+ * @param {ActionCompileOptions} options
87
+ * @returns {CompiledSub[]}
88
+ */
89
+ export declare function compileSubs(subs: any, options: ActionCompileOptions): CompiledSub[];
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @file The Jaren application loop.
3
+ *
4
+ * `createApp` takes an **app document** — one JSON value holding the
5
+ * initial state, a JSLT view stylesheet, named action documents and
6
+ * subscription entries — compiles every embedded document once, and runs
7
+ * a serialized dispatch loop over the compiled closures:
8
+ *
9
+ * DOM event → binding → action document → transition → next state
10
+ * → effects → listeners → subscriptions refresh → batched re-render
11
+ *
12
+ * JavaScript enters only at named, registered boundaries: effect and
13
+ * subscription handlers, the `compileTypeTest` hook, and the optional
14
+ * `validateState` invariant hook. Everything between the boundaries is
15
+ * data. See docs/APP-FORMAT.md for the document contract.
16
+ *
17
+ * **Transaction model (APP-FORMAT §8).** Every dispatch is one
18
+ * transaction on one FIFO queue. Only the queue drain evaluates and
19
+ * applies transitions; a dispatch from an effect, listener, observer,
20
+ * subscription handler, widget or lifecycle hook queues behind the
21
+ * current transaction and never nests. Within a transaction the order
22
+ * is: state commit → effect invocation → listener notification →
23
+ * subscription reconciliation → render scheduling → observer
24
+ * notification; all of it completes before the next transaction begins.
25
+ * Every listener therefore observes every transaction in the same
26
+ * order, and each notification carries the state produced by exactly
27
+ * that transaction. Native event data is reduced to JSON synchronously
28
+ * at dispatch time, before queuing. A listener, observer or cleanup
29
+ * error is isolated: it is reported through `onError`, and whatever the
30
+ * sink throws is re-thrown only after the drain has fully completed —
31
+ * the queue always drains, cleanups are never skipped.
32
+ */
33
+ export type AppOptions = {
34
+ /**
35
+ * - DOM element to mount into; omit for a headless
36
+ * app (drive it via `getVnode`/`subscribe`).
37
+ */
38
+ node?: any;
39
+ /**
40
+ * - The DOM document (defaults to
41
+ * `node.ownerDocument`).
42
+ */
43
+ document?: any;
44
+ /**
45
+ * Effect handlers by name. A handler function may carry an optional
46
+ * `dispose()` member, called exactly once by `app.destroy()` (a
47
+ * handler registered under several names is disposed once).
48
+ */
49
+ effects?: Record<string, (props: any, dispatch: Dispatch) => void>;
50
+ /**
51
+ * Subscription handlers by name; may return a cleanup function.
52
+ */
53
+ subs?: Record<string, (props: any, dispatch: Dispatch) => (() => void) | void>;
54
+ /**
55
+ * Named event-field extractors: when a binding requests a field by
56
+ * name, an extractor registered here wins over the built-in
57
+ * allow-list. An extractor receives the native event and MUST return
58
+ * a JSON value (`$event` stays serializable end to end).
59
+ */
60
+ eventFields?: Record<string, (nativeEvent: any) => any>;
61
+ /**
62
+ * - Registered widget
63
+ * definitions by name for `jaren-widget` vnodes (VIEW-FORMAT §7),
64
+ * forwarded to the renderer — the mechanism lives in `@jarenjs/view`;
65
+ * the app only names the boundary, like effects and subs.
66
+ */
67
+ widgets?: Record<string, any>;
68
+ /**
69
+ * Enables JSON Schema operators (`$valid`/`$assert`/`$as`) and schema
70
+ * matches inside the view and the action documents.
71
+ */
72
+ compileTypeTest?: (schema: any, docPath: string) => ((value: any) => boolean);
73
+ /**
74
+ * Invariant hook, called with every candidate next state plus a
75
+ * context carrying the previous state, the acting action name, its
76
+ * payload and the transition's changed paths (`null` = unknown, the
77
+ * whole state must be treated as changed — selective validation on
78
+ * `changes` is only sound when the hook falls back to a full check
79
+ * for `null`). A rejection (`false` or `{ valid: false }`) blocks the
80
+ * transition (fail closed) and reports `JA2005` through `onError`.
81
+ * The hook is host code and may itself fail: a throwing validator is
82
+ * isolated as `JA2015` (the transaction fails, the original cause is
83
+ * preserved, and the queue keeps draining — parked errors from the
84
+ * default rethrowing sink surface only after the drain). The hook
85
+ * also runs once at boot against the initial state (see
86
+ * {@link ValidateContext}).
87
+ */
88
+ validateState?: (state: any, context: ValidateContext) => boolean | {
89
+ valid: boolean;
90
+ errors?: any;
91
+ };
92
+ /**
93
+ * - The derivation boundary:
94
+ * maps the state to the view stylesheet's input document before every
95
+ * render (default identity). This is where JS-computed derivations —
96
+ * `buildFormViewModel` from `@jarenjs/forms`, aggregations, joins —
97
+ * enter the render path without ever entering the state.
98
+ */
99
+ viewModel?: (state: any) => any;
100
+ /**
101
+ * - Runtime error sink;
102
+ * default rethrows. Errors reported from isolated sites (listeners,
103
+ * observers, cleanups, unknown event fields) never break the
104
+ * transaction queue: whatever the sink throws surfaces to the outer
105
+ * dispatch caller only after the drain completes.
106
+ */
107
+ onError?: (error: Error) => void;
108
+ /**
109
+ * - Render scheduler;
110
+ * default batches on a microtask. Pass `(f) => f()` for synchronous
111
+ * rendering (tests, SSR pipelines).
112
+ */
113
+ schedule?: (flush: () => void) => void;
114
+ /**
115
+ * - Called exactly once per
116
+ * settled, NONTERMINAL committed frame: after the DOM patch and
117
+ * widget mounts complete, and — when a widget hook error was parked
118
+ * during the frame — BEFORE that error is delivered to `onError`
119
+ * (the committed frame is real; its callback is never starved by
120
+ * error delivery). Never called for a pass that ended in terminal
121
+ * teardown, and never on headless apps. The post-render
122
+ * focus/measurement queue (`createFocusEffect`) plugs in here.
123
+ */
124
+ afterRender?: () => void;
125
+ /**
126
+ * - The dispatch-loop guard (default
127
+ * 1000): the maximum number of transactions one drain may process
128
+ * before the queue is abandoned with `JA2010` — an accidental
129
+ * action→effect→action loop diagnoses instead of hanging.
130
+ */
131
+ maxTurns?: number;
132
+ /**
133
+ * - The subscription fan-out
134
+ * bound (default 256): a `for` declaration resolving more instances
135
+ * than this reports `JA2017` and keeps its previous instance set —
136
+ * an unbounded fan-out driven by state is a resource bug waiting for
137
+ * a bad query.
138
+ */
139
+ maxSubInstances?: number;
140
+ /**
141
+ * - Include `payload` and `event`
142
+ * values in transaction records handed to observers (default false —
143
+ * diagnostics must not leak data by default).
144
+ */
145
+ capturePayloads?: boolean;
146
+ };
147
+ export type ValidateContext = {
148
+ /**
149
+ * - The state the transition started from
150
+ * (`null` for the boot-time initial-state check).
151
+ */
152
+ previous: any;
153
+ /**
154
+ * - The acting action name (`null`
155
+ * for the boot-time initial-state check).
156
+ */
157
+ action: string | null;
158
+ /**
159
+ * - The dispatch payload (`null` when absent).
160
+ */
161
+ payload: any;
162
+ /**
163
+ * - Changed JSON Pointers when the
164
+ * transition was patch-only, else `null` (= unknown, validate fully).
165
+ */
166
+ changes: string[] | null;
167
+ };
168
+ export type TransactionRecord = {
169
+ /**
170
+ * - Monotonic transaction sequence number.
171
+ */
172
+ seq: number;
173
+ /**
174
+ * - The dispatched action name.
175
+ */
176
+ action: string;
177
+ /**
178
+ * - What queued it: `'dispatch'` (external),
179
+ * `'binding'` (DOM/widget), `'effect'`, `'subscription'`, or
180
+ * `'setState'` (a whole-state replacement from outside the loop).
181
+ */
182
+ source: string;
183
+ status: 'applied' | 'noop' | 'rejected' | 'failed';
184
+ /**
185
+ * - Changed JSON Pointers, or
186
+ * `null` when the whole state was replaced (unknown = everything).
187
+ */
188
+ changedPaths: string[] | null;
189
+ /**
190
+ * - Names of effects invoked.
191
+ */
192
+ scheduledEffects: string[];
193
+ durationMs: number;
194
+ /**
195
+ * - The `JA2xxx` code when the
196
+ * transaction failed or was rejected.
197
+ */
198
+ errorCode: string | null;
199
+ payload?: any;
200
+ event?: any;
201
+ };
202
+ export type Dispatch = (name: string, payload?: any, domEvent?: any, eventFields?: string[] | null) => void;
203
+ /**
204
+ * Compile an app document and start the loop.
205
+ *
206
+ * Boot is a transaction: compiling the documents, creating the
207
+ * renderer, validating the initial state, starting the initial
208
+ * subscriptions, painting the first frame and draining the dispatches
209
+ * queued by starting handlers either all succeed, or every
210
+ * already-acquired resource (subscriptions, effect handlers, the
211
+ * renderer — the container ends empty) is disposed and `createApp`
212
+ * throws `JA0007` (an `AppCompileError` carrying the original failure
213
+ * as `cause`). `onError` observes individual boot-time failures first —
214
+ * a sink that swallows a subscription-start failure, the initial-state
215
+ * check or an error inside queued boot work recovers it and boots the
216
+ * rest; renderer-construction and first-frame failures are always
217
+ * fatal; the default rethrowing sink aborts boot on any of them. After
218
+ * a successful boot the returned app never throws from `createApp`
219
+ * paths again.
220
+ *
221
+ * @param {any} appDoc
222
+ * @param {AppOptions} [options]
223
+ */
224
+ export declare function createApp(appDoc: any, options?: AppOptions): {
225
+ dispatch: (name: string, payload?: any, domEvent?: any, eventFields?: string[] | null) => void;
226
+ /** The current state (treat as immutable). */
227
+ getState: () => any;
228
+ /**
229
+ * Replace the whole state from OUTSIDE the action loop and re-render.
230
+ * Unlike `dispatch`, this runs no reducer and no effects — it is the
231
+ * host-driven override for external state sync: SSR hydration, or a
232
+ * studio hot-swapping an edited `state` block into a running nested
233
+ * app without a reboot (the diff re-render keeps the DOM, so focus,
234
+ * scroll and uncontrolled inputs survive). Listeners are notified with
235
+ * `null` changed-paths (treat everything as changed); `when`-gated
236
+ * subscriptions refresh; a render is scheduled. A no-op when the state
237
+ * is reference-identical or the loop has been stopped.
238
+ *
239
+ * It is a TRANSACTION, with every guarantee a dispatch has: it takes
240
+ * its turn in the FIFO queue, `validateState` decides before the
241
+ * commit, listeners all observe the same state, the turn guard counts
242
+ * it, one record reaches the observers, and a sink failure settles at
243
+ * this caller. Replacing state directly had none of those — a listener
244
+ * that dispatched saw the two transactions interleave and never
245
+ * observed its own committed state, an invalid replacement committed
246
+ * unvalidated, and the resulting `JA2011` emerged from the next
247
+ * unrelated dispatch.
248
+ * @param {any} next - the replacement state
249
+ */
250
+ setState(next: any): void;
251
+ /** The current view output — for SSR or custom renderers. */
252
+ getVnode: () => any;
253
+ render: () => void;
254
+ /**
255
+ * Observe state changes. The listener receives the new state and the
256
+ * transition's changed paths: an array of JSON Pointers when the
257
+ * transition was patch-only (see the patch engine's `changes` option
258
+ * for the invalidation-sound semantics), or `null` when the whole
259
+ * state was replaced — treat everything as changed. Listeners run
260
+ * inside the transaction, in registration order, all observing the
261
+ * same state/changes pair; a throwing listener is isolated (JA2011).
262
+ * @param {(state: any, changes: string[] | null) => void} listener
263
+ * @returns {() => void} unsubscribe
264
+ */
265
+ subscribe(listener: (state: any, changes: string[] | null) => void): () => void;
266
+ /**
267
+ * Observe completed transactions (APP-FORMAT §8.3). The observer
268
+ * receives one bounded JSON metadata record per transaction, after
269
+ * the transaction fully settled (state, effects, listeners,
270
+ * subscriptions, render scheduling). Payload/event values are
271
+ * included only when the app was created with `capturePayloads`.
272
+ * A throwing observer is isolated and never corrupts the queue.
273
+ * @param {(tx: TransactionRecord) => void} observer
274
+ * @returns {() => void} unsubscribe
275
+ */
276
+ observe(observer: (tx: TransactionRecord) => void): () => void;
277
+ /**
278
+ * Stop the loop — one-way and nonterminal, not a resumable pause:
279
+ * live subscriptions are cleaned up (isolated), listeners are
280
+ * cleared and further dispatches are ignored, permanently. The
281
+ * renderer and effect handlers stay untouched — `destroy()` is the
282
+ * terminal teardown that owns them.
283
+ */
284
+ stop(): void;
285
+ /**
286
+ * Terminal teardown: `stop()` plus observer removal, effect-handler
287
+ * `dispose()` (each handler identity once), and renderer
288
+ * destruction (widgets unmount exactly once, the container is left
289
+ * empty). Idempotent; scheduled render flushes become exact no-ops;
290
+ * every cleanup error is isolated so siblings always run.
291
+ */
292
+ destroy(): void;
293
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * @file A bounded transaction log over `app.observe` (APP-FORMAT §8.3).
3
+ *
4
+ * The log lives OUTSIDE application state by design: diagnostics are
5
+ * host memory, never data. Records are the observer's bounded JSON
6
+ * metadata; payload capture stays whatever the app was created with
7
+ * (`capturePayloads`), so the log adds no leak surface of its own. A
8
+ * redaction hook lets a host scrub or drop records before they are
9
+ * retained at all.
10
+ */
11
+ export type TransactionLogOptions = {
12
+ /**
13
+ * - Maximum retained records (default 200);
14
+ * older records fall off the front.
15
+ */
16
+ limit?: number;
17
+ /**
18
+ * - Applied to every record
19
+ * before retention; return the (possibly rewritten) record, or
20
+ * `null`/`undefined` to drop it entirely. Secrets and personal data
21
+ * are the host's responsibility — this is the hook to enforce it.
22
+ */
23
+ redact?: (record: any) => any;
24
+ };
25
+ /**
26
+ * @typedef {Object} TransactionLogOptions
27
+ * @property {number} [limit] - Maximum retained records (default 200);
28
+ * older records fall off the front.
29
+ * @property {(record: any) => any} [redact] - Applied to every record
30
+ * before retention; return the (possibly rewritten) record, or
31
+ * `null`/`undefined` to drop it entirely. Secrets and personal data
32
+ * are the host's responsibility — this is the hook to enforce it.
33
+ */
34
+ /**
35
+ * Create a bounded transaction log. Wire it up with
36
+ * `app.observe(log.observer)`; read it back with `log.entries()`;
37
+ * export it as versioned JSON with `log.export()`.
38
+ *
39
+ * @example
40
+ * const log = createTransactionLog({ limit: 100 });
41
+ * const stop = app.observe(log.observer);
42
+ * // ... later, in a support bundle:
43
+ * const dump = log.export(); // { version: 1, entries: [...] }
44
+ *
45
+ * @param {TransactionLogOptions} [options]
46
+ */
47
+ export declare function createTransactionLog(options?: TransactionLogOptions): {
48
+ /**
49
+ * The observer to register with `app.observe`.
50
+ * @param {any} record
51
+ */
52
+ observer(record: any): void;
53
+ /** The retained records, oldest first (a fresh array each call). */
54
+ entries(): any[];
55
+ /** Drop every retained record. */
56
+ clear(): void;
57
+ /**
58
+ * A versioned export envelope for support bundles. The version
59
+ * covers the envelope shape; record fields follow the observer
60
+ * contract of the app that produced them.
61
+ * @returns {{ version: 1, entries: any[] }}
62
+ */
63
+ export(): {
64
+ version: 1;
65
+ entries: any[];
66
+ };
67
+ };