@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.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @file A tiny persisted document store plus a share-link codec — the
3
+ * save / load / delete / list + share primitives an IDE-style surface (the
4
+ * studio, the play playground) needs, with the storage backend injected by
5
+ * the host so the package stays free of `localStorage`.
6
+ *
7
+ * `createDocStore` is a keyed CRUD over an injected `storage` ({ read, write }):
8
+ * the store is one JSON object `{ [key]: { name → value } }`, read once at
9
+ * creation and written back on every mutation. `encodeShare` / `decodeShare`
10
+ * turn a small snapshot into a Unicode-safe base64url token and back —
11
+ * forgivingly: a corrupt token decodes to `null`, never a throw.
12
+ */
13
+ /**
14
+ * @param {Object} opts
15
+ * @param {{ read: () => any, write: (store: any) => void }} opts.storage
16
+ * the host persistence adapter (localStorage in the browser, an in-memory
17
+ * object in tests)
18
+ * @param {string} [opts.key='experiments'] - the store's collection key
19
+ * @returns {{
20
+ * save: (name: string, value: any) => void,
21
+ * load: (name: string) => any,
22
+ * remove: (name: string) => void,
23
+ * names: () => string[],
24
+ * all: () => Record<string, any>,
25
+ * }}
26
+ */
27
+ export declare function createDocStore({ storage, key }: {
28
+ storage: {
29
+ read: () => any;
30
+ write: (store: any) => void;
31
+ };
32
+ key?: string;
33
+ }): {
34
+ save: (name: string, value: any) => void;
35
+ load: (name: string) => any;
36
+ remove: (name: string) => void;
37
+ names: () => string[];
38
+ all: () => Record<string, any>;
39
+ };
40
+ /**
41
+ * Encode a snapshot as a base64url token: Unicode-safe (TextEncoder),
42
+ * portable between browser and Node.
43
+ * @param {any} snapshot
44
+ * @returns {string} the base64url token
45
+ */
46
+ export declare function encodeShare(snapshot: any): string;
47
+ /**
48
+ * Decode a base64url token back to its snapshot, forgivingly.
49
+ * @param {string} token
50
+ * @returns {any} the snapshot object, or `null` when the token is unusable
51
+ */
52
+ export declare function decodeShare(token: string): any;
@@ -0,0 +1,178 @@
1
+ /**
2
+ * @file Error types for @jarenjs/app, built on `@jarenjs/core`'s coded
3
+ * contract: every failure carries a stable `code` (JA0xxx compile,
4
+ * JA2xxx runtime), a bare `reason`, a composed `message`, and — where
5
+ * one exists — the `docPath` of the offending member of the app
6
+ * document. The feedback shape a repair loop needs.
7
+ */
8
+ import { CodedError } from '@jarenjs/core/errors';
9
+ /**
10
+ * The runtime code table (the `CSV_CODES` shape): one entry per code
11
+ * this package can raise, proven in sync with APP-FORMAT.md's normative
12
+ * table by a test.
13
+ */
14
+ export declare const APP_CODES: Readonly<{
15
+ JA0001: "the app document is not an object";
16
+ JA0002: "view is missing or failed to compile";
17
+ JA0003: "actions is not an object of named documents";
18
+ JA0004: "an action document failed to compile";
19
+ JA0005: "subs is not an array of subscription entries";
20
+ JA0006: "a subscription entry is malformed or its when failed to compile";
21
+ JA0007: "the app failed to boot after compilation succeeded";
22
+ JA0008: "a subscription dynamic member failed to compile or combines invalidly";
23
+ JA2001: "an unknown action was dispatched";
24
+ JA2002: "an action, when document or event-field extractor threw";
25
+ JA2003: "an action produced a transition that is not an object";
26
+ JA2004: "a transition patch failed to apply";
27
+ JA2005: "the next state violated the app invariants";
28
+ JA2006: "a transition named an effect with no registered handler";
29
+ JA2007: "an effect handler threw";
30
+ JA2008: "a subscription entry names no registered handler";
31
+ JA2009: "a binding requested an unknown event field";
32
+ JA2010: "the dispatch loop exceeded maxTurns transactions in one drain";
33
+ JA2011: "a state listener or transaction observer threw";
34
+ JA2012: "a cleanup threw while stopping, reconciling or destroying";
35
+ JA2013: "a subscription handler threw while starting";
36
+ JA2014: "a post-render intent named a data-ref with no rendered target";
37
+ JA2015: "the validateState hook itself threw";
38
+ JA2016: "a subscription dynamic query (withQuery, key or for) failed at runtime";
39
+ JA2017: "a subscription fan-out exceeded maxSubInstances";
40
+ }>;
41
+ /**
42
+ * A defect in the app document itself, raised while `createApp` compiles
43
+ * it. Codes:
44
+ *
45
+ * - `JA0001` — the app document is not an object
46
+ * - `JA0002` — `view` is missing or not a stylesheet document
47
+ * - `JA0003` — `actions` is not an object of named documents
48
+ * - `JA0004` — an action document failed to compile (see `cause`)
49
+ * - `JA0005` — `subs` is not an array of subscription entries
50
+ * - `JA0006` — a subscription entry is malformed or its `when` failed
51
+ * to compile (see `cause`)
52
+ * - `JA0007` — the app failed to boot: the renderer construction, the
53
+ * initial-state check, the initial subscriptions, the first frame or
54
+ * the queued boot work failed after compilation succeeded; every
55
+ * already-acquired resource was rolled back (see `cause`)
56
+ * - `JA0008` — a subscription's dynamic member (`withQuery`, `key`,
57
+ * `for`) failed to compile (see `cause`), or the members combine
58
+ * invalidly (`with` beside `withQuery` or `for`; `key` without
59
+ * either)
60
+ */
61
+ export declare class AppCompileError extends CodedError {
62
+ /**
63
+ * @param {string} code
64
+ * @param {string} reason - The bare reason; `message` is composed as
65
+ * `${code}: ${reason} at ${docPath}` per the coded contract.
66
+ * @param {string} [docPath] - JSON Pointer into the app document;
67
+ * `''` is the document root, `undefined` means no location (never
68
+ * normalized to `''` — root and unknown are different facts).
69
+ * @param {Error} [cause]
70
+ */
71
+ constructor(code: string, reason: string, docPath?: string, cause?: Error);
72
+ }
73
+ /**
74
+ * A failure while the app is running. Codes:
75
+ *
76
+ * - `JA2001` — an unknown action was dispatched
77
+ * - `JA2002` — an action or `when` document threw while evaluating,
78
+ * or a registered event-field extractor threw (see `cause`; the
79
+ * extractor's member binds `null` and the dispatch still runs)
80
+ * - `JA2003` — an action produced a transition that is not an object
81
+ * - `JA2004` — a transition's `patch` failed to apply (see `cause`)
82
+ * - `JA2005` — the next state violated the app's invariants
83
+ * (`validateState` rejected it); the transition was NOT applied
84
+ * - `JA2006` — a transition named an effect with no registered handler
85
+ * - `JA2007` — an effect handler threw (see `cause`)
86
+ * - `JA2008` — a subscription entry names no registered handler
87
+ * - `JA2009` — a binding requested an unknown event field (the member
88
+ * is bound `null`; the dispatch itself is NOT dropped)
89
+ * - `JA2010` — the dispatch loop exceeded `maxTurns` transactions in
90
+ * one drain (an accidental action/effect loop); the queue was
91
+ * abandoned
92
+ * - `JA2011` — a state listener or transaction observer threw
93
+ * (isolated; the queue drains on)
94
+ * - `JA2012` — a cleanup threw while stopping/reconciling/destroying
95
+ * (isolated; sibling cleanups still run)
96
+ * - `JA2013` — a subscription handler threw while starting; the slot
97
+ * stays stopped
98
+ * - `JA2014` — a post-render focus/measure intent named a `data-ref`
99
+ * with no rendered target
100
+ * - `JA2015` — the `validateState` hook itself threw (see `cause`) —
101
+ * distinct from a rejection verdict (`JA2005`); the transaction
102
+ * fails and the queue keeps draining
103
+ * - `JA2016` — a subscription's dynamic query (`withQuery`, `key` or
104
+ * `for`) threw while evaluating — a cyclic resolved value included —
105
+ * and the subscription failed closed (see `cause`; `docPath` names
106
+ * the member)
107
+ * - `JA2017` — a subscription fan-out resolved more instances than
108
+ * `maxSubInstances`; the previous instance set was kept
109
+ */
110
+ export declare class AppRuntimeError extends CodedError {
111
+ /** Structured detail, e.g. validateState errors for JA2005. */
112
+ detail: any;
113
+ /**
114
+ * @param {string} code
115
+ * @param {string} reason - The bare reason; `message` is composed
116
+ * from `code`, `reason` and the location per the coded contract.
117
+ * @param {{ docPath?: string, cause?: Error }} [options] - `docPath`
118
+ * is a JSON Pointer into the app document where one exists
119
+ * (`undefined` when there is no location — never `''`, which means
120
+ * the document root); `cause` retains what host code threw.
121
+ */
122
+ constructor(code: string, reason: string, options?: {
123
+ docPath?: string;
124
+ cause?: Error;
125
+ });
126
+ }
127
+ /**
128
+ * A non-Error value thrown by host code, wrapped for the framework's
129
+ * error channels. JavaScript permits `throw null`, `throw undefined`,
130
+ * strings, numbers and arbitrary objects; host extension points
131
+ * (effects, subscriptions, validators, extractors, listeners,
132
+ * observers, widgets, sinks) may produce any of them, and the
133
+ * framework's isolation guarantees must hold for all of them.
134
+ *
135
+ * The original value is retained as an OWN `cause` property — set even
136
+ * when the value is `undefined`, so `Object.hasOwn(err, 'cause')`
137
+ * distinguishes "threw undefined" from "no cause" — and the message
138
+ * describes the value without invoking any user coercion (`toString`
139
+ * on a hostile object is never called).
140
+ */
141
+ export declare class HostValueError extends Error {
142
+ /** @param {unknown} value - The value host code threw. */
143
+ constructor(value: unknown);
144
+ }
145
+ /**
146
+ * `value instanceof Error` without trusting the value: `instanceof`
147
+ * walks the prototype chain, which a revoked or hostile proxy turns
148
+ * into a throw. A value whose very classification throws is treated as
149
+ * not-an-Error and wrapped.
150
+ * @param {unknown} value
151
+ * @returns {value is Error}
152
+ */
153
+ export declare function isErrorSafely(value: unknown): value is Error;
154
+ /**
155
+ * Read an error's `message` without trusting it: JavaScript permits an
156
+ * own `message` accessor (or a proxy `get` trap) that throws, and the
157
+ * framework must never fail while formatting a failure. The original
158
+ * error object is never mutated — it stays the causal identity; this
159
+ * only projects a safe diagnostic string.
160
+ * @param {Error} error
161
+ * @returns {string}
162
+ */
163
+ export declare function safeErrorMessage(error: Error): string;
164
+ /**
165
+ * The one host-failure normalization policy (APP-FORMAT §10.1): every
166
+ * value caught at a host boundary passes through here, and the policy
167
+ * is TOTAL — no ECMAScript value, revoked proxies and throwing
168
+ * accessors included, can make it throw. An `Error` instance passes by
169
+ * IDENTITY — wherever a contract promises the original error as
170
+ * `cause`, that identity survives; anything else (a value whose
171
+ * classification itself throws included) is wrapped in a
172
+ * {@link HostValueError} that retains the original value as an own
173
+ * `cause` property. No caught value is ever assumed to have
174
+ * `.message`, and no thrown value is ever used as an absence sentinel.
175
+ * @param {unknown} value - Whatever host code threw.
176
+ * @returns {Error}
177
+ */
178
+ export declare function toError(value: unknown): Error;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @file The post-render focus/measurement queue (APP-FORMAT §8.4).
3
+ *
4
+ * Focus, text selection and measurement need a real DOM element at a
5
+ * moment when the frame is committed — but DOM nodes must never enter
6
+ * state. The bridge is a JSON intent naming a `data-ref` token that the
7
+ * view places as an ordinary attribute:
8
+ *
9
+ * view: ["input", { "data-ref": "search" }]
10
+ * action: { "effects": [{ "run": "focus", "with": { "ref": "search" } }] }
11
+ *
12
+ * Intents queue during the transaction and flush after the NEXT
13
+ * committed frame — after the DOM patch and after widget mounts, so a
14
+ * target inside freshly rendered markup is already connected. Only a
15
+ * state change schedules a frame: an intent queued by an effect-only
16
+ * action (no state transition) waits until the next state-changing
17
+ * transaction or a manual `render()` commits one — pair the intent
18
+ * with the transition that produces its target. A missing target is a
19
+ * diagnosable `JA2014`, never a silent no-op. Destroying the app (or
20
+ * `dispose()`) cancels pending intents; a headless app never flushes
21
+ * (there is no frame), which makes the queue a documented no-op there.
22
+ */
23
+ export type FocusEffectOptions = {
24
+ /**
25
+ * - The rendered root (the same element
26
+ * handed to `createApp` as `node`); intents resolve inside it.
27
+ */
28
+ container: any;
29
+ /**
30
+ * - Sink for `JA2014`
31
+ * missing-target diagnostics; default: the first one is thrown after
32
+ * the flush completes (siblings still run). The sink itself is
33
+ * isolated: a throwing sink never stops the flush — every sibling
34
+ * intent still resolves, and the first error the sink threw
35
+ * surfaces after the flush completes.
36
+ */
37
+ onError?: (error: Error) => void;
38
+ };
39
+ export type FocusEffect = ((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
40
+ flush: () => void;
41
+ dispose: () => void;
42
+ };
43
+ /**
44
+ * @typedef {Object} FocusEffectOptions
45
+ * @property {any} container - The rendered root (the same element
46
+ * handed to `createApp` as `node`); intents resolve inside it.
47
+ * @property {(error: Error) => void} [onError] - Sink for `JA2014`
48
+ * missing-target diagnostics; default: the first one is thrown after
49
+ * the flush completes (siblings still run). The sink itself is
50
+ * isolated: a throwing sink never stops the flush — every sibling
51
+ * intent still resolves, and the first error the sink threw
52
+ * surfaces after the flush completes.
53
+ */
54
+ /**
55
+ * The effect handler returned by {@link createFocusEffect}, with its
56
+ * host-side controls.
57
+ * @typedef {((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
58
+ * flush: () => void,
59
+ * dispose: () => void,
60
+ * }} FocusEffect
61
+ */
62
+ /**
63
+ * Create the post-render intent queue as a registered effect. Intent
64
+ * props (all JSON):
65
+ *
66
+ * - `ref` (REQUIRED, string) — the `data-ref` token to resolve;
67
+ * - `op` (OPTIONAL) — `"focus"` (default), `"select"`, or
68
+ * `"measure"`;
69
+ * - `done` (REQUIRED for `measure`) — the action dispatched with
70
+ * `{ id, ref, rect }` where `rect` is the JSON-reduced bounding
71
+ * rect;
72
+ * - `id` (OPTIONAL) — echoed in the `measure` completion payload.
73
+ *
74
+ * Wire `flush` as the app's `afterRender` so intents resolve exactly
75
+ * once per committed frame, ordered after widget mounts:
76
+ *
77
+ * @example
78
+ * const focus = createFocusEffect({ container: node });
79
+ * const app = createApp(doc, {
80
+ * node,
81
+ * effects: { focus },
82
+ * afterRender: focus.flush,
83
+ * });
84
+ * // app.destroy() disposes the queue through the handler's dispose()
85
+ *
86
+ * @param {FocusEffectOptions} options
87
+ * @returns {FocusEffect}
88
+ */
89
+ export declare function createFocusEffect(options: FocusEffectOptions): FocusEffect;
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @file The standard form rules — the shipped JSLT rule set that renders
3
+ * any `@jarenjs/forms` view model (`buildFormViewModel`) to vnodes, and
4
+ * the standard action documents that write user input back into the
5
+ * state. Everything both factories return is PLAIN JSON: no functions,
6
+ * no imports from forms — the rules dispatch on the view-model *shape*
7
+ * (JSONPath filter selectors on `control`), which is the whole point of
8
+ * the format stack.
9
+ *
10
+ * Wiring (see the README for the complete walkthrough):
11
+ *
12
+ * view: [...createFormView(), { match: '$', body: [..., { $apply: '$.form' }] }]
13
+ * actions: { ...createFormActions({ dataPointer: '/data' }) }
14
+ * options: { viewModel: (state) => ({ form: buildFormViewModel(model, state.data, { rules }) }) }
15
+ *
16
+ * A DOM control's value is a STRING, and two of these controls carry
17
+ * something else: a select over a non-string enum, and the `json`
18
+ * editor over an arbitrary value. Both round-trip through the JSON text
19
+ * the view model precomputes (`option.key`) or the operator types, and
20
+ * both decode it in a registered event-field extractor —
21
+ * {@link formEventFields}, the format's one sanctioned place for host
22
+ * JavaScript at the DOM boundary (APP-FORMAT §5.4). A host that renders
23
+ * these controls MUST register them.
24
+ *
25
+ * Remaining limitation, documented rather than hidden: a cleared number
26
+ * input writes `null` (which surfaces as a validation error, not a
27
+ * dispatch error).
28
+ */
29
+ /**
30
+ * The event-field extractors the standard form controls need, for
31
+ * `createApp`'s `eventFields` (APP-FORMAT §5.4).
32
+ *
33
+ * One extractor, `formJsonValue`: the control's value parsed as JSON.
34
+ * A select carries `option.key` (the view model's JSON text for the
35
+ * typed enum value) and the `json` editor carries whatever the operator
36
+ * typed. Unparsable text yields `null` rather than throwing, so a
37
+ * half-typed JSON document is a validation problem — visible, fixable —
38
+ * instead of a dispatch error; `json` fields therefore want a schema
39
+ * that rejects `null` if absence is not acceptable.
40
+ *
41
+ * @example
42
+ * createApp(doc, { node, eventFields: { ...formEventFields() } });
43
+ *
44
+ * @returns {Record<string, (event: any) => any>}
45
+ */
46
+ export declare function formEventFields(): Record<string, (event: any) => any>;
47
+ export type FormViewOptions = {
48
+ /**
49
+ * - JSONPath of the form view-model node inside
50
+ * the view input document (default `'$.form'`).
51
+ */
52
+ root?: string;
53
+ /**
54
+ * - CSS class prefix (default `'jaren-form'`).
55
+ */
56
+ classPrefix?: string;
57
+ /**
58
+ * - Overrides for the
59
+ * standard action names (`input`/`check`/`number`/`add`/`remove`).
60
+ */
61
+ actions?: Record<string, string>;
62
+ /**
63
+ * - Add-item button text (default `'+'`).
64
+ */
65
+ addLabel?: string;
66
+ /**
67
+ * - Remove-item button text (default `'×'`).
68
+ */
69
+ removeLabel?: string;
70
+ /**
71
+ * - Accessible
72
+ * names for the two symbol buttons. `@jarenjs/forms`'
73
+ * `formChromeLabels(catalog)` resolves them from a message catalog;
74
+ * the English defaults apply when absent.
75
+ */
76
+ labels?: {
77
+ addItem?: string;
78
+ removeItem?: string;
79
+ };
80
+ /**
81
+ * - (actions) JSON Pointer to the form
82
+ * data inside the app state (default `'/data'`).
83
+ */
84
+ dataPointer?: string;
85
+ };
86
+ /**
87
+ * Options shared by `createFormView` / `createFormActions`.
88
+ * @typedef {Object} FormViewOptions
89
+ * @property {string} [root] - JSONPath of the form view-model node inside
90
+ * the view input document (default `'$.form'`).
91
+ * @property {string} [classPrefix] - CSS class prefix (default `'jaren-form'`).
92
+ * @property {Record<string, string>} [actions] - Overrides for the
93
+ * standard action names (`input`/`check`/`number`/`add`/`remove`).
94
+ * @property {string} [addLabel] - Add-item button text (default `'+'`).
95
+ * @property {string} [removeLabel] - Remove-item button text (default `'×'`).
96
+ * @property {{addItem?: string, removeItem?: string}} [labels] - Accessible
97
+ * names for the two symbol buttons. `@jarenjs/forms`'
98
+ * `formChromeLabels(catalog)` resolves them from a message catalog;
99
+ * the English defaults apply when absent.
100
+ * @property {string} [dataPointer] - (actions) JSON Pointer to the form
101
+ * data inside the app state (default `'/data'`).
102
+ */
103
+ /**
104
+ * The standard form rule set: a JSLT rule array rendering a
105
+ * `buildFormViewModel` tree. Concatenate it into an app's view
106
+ * stylesheet and `{"$apply": "<root>"}` the form node from a page rule.
107
+ *
108
+ * @param {FormViewOptions} [options]
109
+ * @returns {any[]} A JSLT rule array (plain JSON).
110
+ */
111
+ export declare function createFormView(options?: FormViewOptions): any[];
112
+ /**
113
+ * The standard form actions: named query documents that write user
114
+ * input into the form data at `dataPointer + payload.pointer`. RFC 6902
115
+ * `add` is set-or-replace for object members, so untouched (absent)
116
+ * fields are created on first input.
117
+ *
118
+ * @param {FormViewOptions} [options]
119
+ * @returns {Record<string, any>} An `actions` fragment (plain JSON) to
120
+ * spread into an app document.
121
+ */
122
+ export declare function createFormActions(options?: FormViewOptions): Record<string, any>;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file @jarenjs/app — applications as JSON documents: the compiled
3
+ * dispatch loop over @jarenjs/json engines and the @jarenjs/view
4
+ * renderer. See README.md and docs/APP-FORMAT.md.
5
+ */
6
+ export { createApp } from './app.js';
7
+ export { compileActions, compileSubs } from './actions.js';
8
+ export { createFormView, createFormActions, formEventFields } from './forms.js';
9
+ export { createTaskEffect } from './tasks.js';
10
+ export { createFocusEffect } from './focus.js';
11
+ export { createTransactionLog } from './diagnostics.js';
12
+ export { createSplitterWidget } from './splitter.js';
13
+ export { createDocStore, encodeShare, decodeShare } from './docstore.js';
14
+ export { AppCompileError, AppRuntimeError, HostValueError, toError, APP_CODES } from './errors.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @file A reusable drag-splitter widget for a two-pane (or rail|editor|stage)
3
+ * grid. It drives a CSS ratio variable LIVE during a drag — no per-move
4
+ * dispatch, which would flood the transaction log and undo — and commits the
5
+ * ratio on pointer-UP only, plus keyboard resize as an ARIA separator. Every
6
+ * DOM call is guarded so it mounts inertly over a headless stub (there the
7
+ * live drag is browser-verified). The host binds it as a widget and
8
+ * parameterizes the grid/rail selectors, the CSS variable and the commit
9
+ * action, so studio, play and any future two-pane surface share one splitter.
10
+ */
11
+ /**
12
+ * @param {Object} opts
13
+ * @param {string} opts.action - the app action dispatched with the committed ratio
14
+ * @param {string} [opts.grid='.jstudio'] - selector for the grid element (the host's closest ancestor)
15
+ * @param {string} [opts.rail] - selector for a fixed left rail inside the grid; the ratio
16
+ * spans from the rail's right edge (or the grid's left when absent) to the grid's right
17
+ * @param {string} [opts.cssVar='--js-ratio'] - the CSS custom property the grid reads for the split
18
+ * @param {number} [opts.min=0.1] - the smallest left-pane ratio (also Home)
19
+ * @param {number} [opts.max=0.9] - the largest left-pane ratio (also End)
20
+ * @param {number} [opts.step=0.05] - the arrow-key step
21
+ * @param {number} [opts.fineStep=0.01] - the Shift+arrow step
22
+ * @returns {{ mount: Function, update: Function, unmount: Function }}
23
+ */
24
+ export declare function createSplitterWidget(opts: {
25
+ action: string;
26
+ grid?: string;
27
+ rail?: string;
28
+ cssVar?: string;
29
+ min?: number;
30
+ max?: number;
31
+ step?: number;
32
+ fineStep?: number;
33
+ }): {
34
+ mount: Function;
35
+ update: Function;
36
+ unmount: Function;
37
+ };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * @file The host half of the async-task convention (docs/TASKS.md).
3
+ *
4
+ * The convention has two halves and this module is deliberately only one
5
+ * of them: **correctness lives in state** — a task slot's monotonic `id`
6
+ * and the completion action's guard reject stale responses — while
7
+ * **cancellation and concurrency live here**, as an optimization that
8
+ * stops wasting the wire. An aborted fetch may already have resolved and
9
+ * its dispatch may already be queued, so a host that only aborts is
10
+ * still wrong; the state-side guard is the guarantee.
11
+ *
12
+ * No timers, no state beyond the per-slot records, no dependencies
13
+ * (`AbortController` is platform).
14
+ */
15
+ export type TaskRun = (props: any, signal: AbortSignal) => any | PromiseLike<any>;
16
+ export type TaskMode = 'switch' | 'exhaust' | 'concat' | 'parallel';
17
+ export type TaskEffectOptions = {
18
+ /**
19
+ * - The per-slot concurrency mode
20
+ * (default `"switch"`).
21
+ */
22
+ mode?: TaskMode;
23
+ };
24
+ export type TaskEffect = ((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
25
+ cancel: (slot?: string) => void;
26
+ cancelAll: () => void;
27
+ dispose: () => void;
28
+ };
29
+ /**
30
+ * The host's task function, typically wrapping `fetch`. A synchronous
31
+ * return is allowed — the effect settles every result through one
32
+ * uniform promise boundary either way.
33
+ * @callback TaskRun
34
+ * @param {any} props - The effect's `with` value, verbatim.
35
+ * @param {AbortSignal} signal - Aborted when the slot's concurrency mode
36
+ * supersedes this task, when the host cancels the slot, or when the
37
+ * effect is disposed; pass it to `fetch` (or ignore it — the
38
+ * state-side id guard stays correct either way).
39
+ * @returns {any | PromiseLike<any>} The JSON result, or a promise of it.
40
+ */
41
+ /**
42
+ * Per-slot concurrency modes (APP-FORMAT §9.2):
43
+ *
44
+ * - `"switch"` (default) — starting a task aborts the slot's
45
+ * in-flight predecessor; the newest request wins.
46
+ * - `"exhaust"` — while the slot has an in-flight task, new starts are
47
+ * ignored entirely (nothing dispatched) — the mode for a
48
+ * non-idempotent commit where a double-click must not double-run.
49
+ * - `"concat"` — new starts queue and run one after another, in
50
+ * order — only for deliberately ordered commands.
51
+ * - `"parallel"` — every start runs concurrently; the consumer owns
52
+ * the merge rule.
53
+ *
54
+ * Whatever the mode, correctness stays visible in JSON state: the task
55
+ * slot's monotonic `id` and the completion action's guard remain the
56
+ * authority on which response may land.
57
+ * @typedef {'switch' | 'exhaust' | 'concat' | 'parallel'} TaskMode
58
+ */
59
+ /**
60
+ * @typedef {Object} TaskEffectOptions
61
+ * @property {TaskMode} [mode] - The per-slot concurrency mode
62
+ * (default `"switch"`).
63
+ */
64
+ /**
65
+ * The effect handler returned by {@link createTaskEffect}, with its
66
+ * host-side controls.
67
+ * @typedef {((props: any, dispatch: (name: string, payload?: any) => void) => void) & {
68
+ * cancel: (slot?: string) => void,
69
+ * cancelAll: () => void,
70
+ * dispose: () => void,
71
+ * }} TaskEffect
72
+ */
73
+ /**
74
+ * Package `run` as a registered effect handler implementing the
75
+ * async-task convention. The effect's `with` props (all JSON):
76
+ *
77
+ * - `id` (REQUIRED) — the task identity, echoed back verbatim in the
78
+ * completion payload for the state-side guard;
79
+ * - `done` (REQUIRED) — the action dispatched on settle;
80
+ * - `fail` (OPTIONAL, string) — the action for rejections; absent,
81
+ * rejections dispatch `done` with `{ id, error }` instead of
82
+ * `{ id, result }` — one completion action guarding on
83
+ * `$payload.error` is the query-friendliest shape;
84
+ * - `slot` (OPTIONAL, string) — the concurrency key, default `""`;
85
+ * what a new start does to the slot's in-flight task is the
86
+ * effect's `mode` (see {@link TaskMode});
87
+ * - anything else `run` needs (a URL, a query, ...).
88
+ *
89
+ * Settlement, exactly: `run` is invoked through a uniform promise
90
+ * boundary, so a synchronous throw and a non-promise return settle
91
+ * through the same path as a rejection/resolution. A resolution
92
+ * dispatches `done` with `{ id, result }`; an abort rejection
93
+ * (`err.name === "AbortError"`) dispatches **nothing** — a superseded
94
+ * task is dead by design, its successor's dispatch carries the story;
95
+ * any other rejection dispatches `fail ?? done` with `{ id, error }`
96
+ * where `error` is a string, never an Error object — JSON only crosses
97
+ * the boundary. After `dispose()` no settlement dispatches anything.
98
+ * A malformed `id`/`done`/`fail`/`slot` is a host programming error:
99
+ * the handler throws a `TypeError`, which the loop reports as `JA2007`.
100
+ * Settlement is TOTAL for every rejection value (hostile accessors,
101
+ * revoked proxies included) and never creates an unhandled rejection
102
+ * from framework code; a settlement dispatch that itself throws (a
103
+ * rethrowing error sink surfacing at the dispatch boundary) is
104
+ * re-raised on its own microtask so the host's global error handling
105
+ * observes it.
106
+ *
107
+ * Host-side controls on the returned handler:
108
+ *
109
+ * - `cancel(slot)` — abort the slot's in-flight task(s) and discard
110
+ * its queued (`concat`) starts;
111
+ * - `cancelAll()` — `cancel` for every slot;
112
+ * - `dispose()` — terminal: `cancelAll()` plus a permanent guard —
113
+ * late settlements can no longer dispatch, and new starts are
114
+ * ignored. Idempotent. `app.destroy()` calls it automatically for
115
+ * every registered handler exposing it.
116
+ *
117
+ * @example
118
+ * createApp(doc, {
119
+ * effects: {
120
+ * http: createTaskEffect((props, signal) =>
121
+ * fetch(props.url, { signal }).then((r) => r.json())),
122
+ * },
123
+ * });
124
+ *
125
+ * @param {TaskRun} run
126
+ * @param {TaskEffectOptions} [options]
127
+ * @returns {TaskEffect}
128
+ */
129
+ export declare function createTaskEffect(run: TaskRun, options?: TaskEffectOptions): TaskEffect;