@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 +197 -0
- package/dist/types/actions.d.ts +89 -0
- package/dist/types/app.d.ts +293 -0
- package/dist/types/diagnostics.d.ts +67 -0
- package/dist/types/docstore.d.ts +52 -0
- package/dist/types/errors.d.ts +178 -0
- package/dist/types/focus.d.ts +89 -0
- package/dist/types/forms.d.ts +122 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/splitter.d.ts +37 -0
- package/dist/types/tasks.d.ts +129 -0
- package/docs/APP-FORMAT.md +804 -0
- package/docs/TASKS.md +254 -0
- package/package.json +57 -0
- package/schemas/jaren-app.draft-07.schema.json +86 -0
- package/schemas/jaren-app.schema.json +86 -0
- package/src/actions.js +167 -0
- package/src/app.js +1622 -0
- package/src/diagnostics.js +77 -0
- package/src/docstore.js +71 -0
- package/src/errors.js +245 -0
- package/src/focus.js +188 -0
- package/src/forms.js +325 -0
- package/src/index.js +16 -0
- package/src/splitter.js +113 -0
- package/src/tasks.js +299 -0
|
@@ -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;
|