@jarenjs/linq 0.49.2 → 0.56.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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/linq",
3
3
  "private": false,
4
- "version": "0.49.2",
4
+ "version": "0.56.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -11,12 +11,49 @@
11
11
  "types": "./types/index.d.ts",
12
12
  "default": "./src/index.js"
13
13
  },
14
+ "./schema": {
15
+ "types": "./types/schema.d.ts",
16
+ "default": "./src/schema/index.js"
17
+ },
18
+ "./model": {
19
+ "types": "./types/model.d.ts",
20
+ "default": "./src/model/index.js"
21
+ },
22
+ "./jslt": {
23
+ "types": "./types/jslt.d.ts",
24
+ "default": "./src/jslt/index.js"
25
+ },
26
+ "./migration": {
27
+ "types": "./types/migration.d.ts",
28
+ "default": "./src/migration/index.js"
29
+ },
30
+ "./contract": {
31
+ "types": "./types/contract.d.ts",
32
+ "default": "./src/contract/index.js"
33
+ },
34
+ "./flow": {
35
+ "types": "./types/flow.d.ts",
36
+ "default": "./src/flow/index.js"
37
+ },
38
+ "./app": {
39
+ "types": "./types/app.d.ts",
40
+ "default": "./src/app/index.js"
41
+ },
42
+ "./forms": {
43
+ "types": "./types/forms.d.ts",
44
+ "default": "./src/forms/index.js"
45
+ },
46
+ "./db": {
47
+ "types": "./types/db.d.ts",
48
+ "default": "./src/db/index.js"
49
+ },
14
50
  "./package.json": "./package.json"
15
51
  },
16
52
  "files": [
17
53
  "types/",
18
54
  "src/",
19
- "docs/"
55
+ "docs/",
56
+ "ARCHITECTURE.md"
20
57
  ],
21
58
  "description": "A C#-familiar fluent LINQ surface for the Jaren suite: expression capture into plain query documents, deferred immutable sequences, and a provider seam that runs the same document in memory or anywhere else",
22
59
  "author": "joham",
@@ -47,7 +84,23 @@
47
84
  "prepack": "npm run build:types"
48
85
  },
49
86
  "dependencies": {
50
- "@jarenjs/core": "^0.49.2",
51
- "@jarenjs/json": "^0.49.2"
87
+ "@jarenjs/core": "^0.56.0",
88
+ "@jarenjs/json": "^0.56.0"
89
+ },
90
+ "peerDependencies": {
91
+ "@jarenjs/db": "^0.56.0",
92
+ "@jarenjs/formats": "^0.56.0",
93
+ "@jarenjs/validate": "^0.56.0"
94
+ },
95
+ "peerDependenciesMeta": {
96
+ "@jarenjs/db": {
97
+ "optional": true
98
+ },
99
+ "@jarenjs/formats": {
100
+ "optional": true
101
+ },
102
+ "@jarenjs/validate": {
103
+ "optional": true
104
+ }
52
105
  }
53
106
  }
@@ -0,0 +1,255 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `action()`, `transition()`, `effect()` and `bind()` — the
4
+ * transition half of an app document (APP-FORMAT §3–§5.1) and the event
5
+ * binding that reaches it (§4).
6
+ *
7
+ * An action is ONE query document over the state, so everything inside
8
+ * it — a patch value, an effect's props, a branch — is captured in the
9
+ * same scope, in one pass: `transition()` and `effect()` therefore
10
+ * assemble plain objects and leave the spelling to the capture that is
11
+ * already running, rather than starting their own. That is also what
12
+ * makes the format's own guarantee expressible: the increment in a
13
+ * patch and the one in the effect's props are the SAME expression,
14
+ * because both evaluate against the pre-transition state.
15
+ *
16
+ * A binding is built at RENDER time by the view stylesheet, so `bind()`
17
+ * is the same kind of value in a JSLT body — a payload may embed the
18
+ * matched node's own members, which is why the format needs no
19
+ * payload-creator function.
20
+ */
21
+
22
+ import { LinqBuildError } from '../errors.js';
23
+ import { effectDescriptor, readEffects } from '../effect.js';
24
+ import { describeValue } from '../json-boundary.js';
25
+ import { isSchemaBuilder } from '../schema/brand.js';
26
+ import { captureAction } from './capture.js';
27
+ import { readPatch } from './patch.js';
28
+
29
+ /** The action brand: how `defineApp` tells a captured action apart. */
30
+ export const ACTION = Symbol.for('@jarenjs/linq/app-action');
31
+
32
+ /** The members `action()` takes beside its callback — both TYPES only. */
33
+ const ACTION_MEMBERS = Object.freeze(['payload', 'event']);
34
+
35
+ /** The members a transition object carries, in §3.2's order of application. */
36
+ const TRANSITION_MEMBERS = Object.freeze(['state', 'patch', 'effects']);
37
+
38
+ /** The members §4 gives the object binding form. */
39
+ const BIND_MEMBERS = Object.freeze(['payload', 'event', 'preventDefault', 'stopPropagation']);
40
+
41
+ /**
42
+ * The host-object-valued fields §3.1 excludes BY NAME. Every OTHER name
43
+ * is allowed — the built-in allow-list and a host extractor registered
44
+ * under any name it likes are both legal, and the pen cannot see the
45
+ * host's registry — but an extractor registered under one of these
46
+ * would shadow the exclusion the format exists to state, so the pen
47
+ * refuses the request and names §5.4's own worked example instead.
48
+ */
49
+ const EXCLUDED_FIELDS = new Set([
50
+ 'target', 'currentTarget', 'relatedTarget', 'srcElement', 'view',
51
+ 'files', 'dataTransfer', 'touches', 'targetTouches', 'changedTouches',
52
+ 'path', 'composedPath', 'clipboardData', 'submitter',
53
+ ]);
54
+
55
+ /** @param {any} value */
56
+ function isPlainObject(value) {
57
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
58
+ }
59
+
60
+ /**
61
+ * A member set the pen knows, or `JL0101` naming the one it does not.
62
+ * @param {any} spec
63
+ * @param {readonly string[]} members
64
+ * @param {string} what
65
+ */
66
+ function closedTo(spec, members, what) {
67
+ for (const key of Object.keys(spec)) {
68
+ if (!members.includes(key)) {
69
+ throw new LinqBuildError('JL0101',
70
+ `${what} does not take '${key}' — it takes ${members.join(', ')}`, `/${key}`);
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * A schema builder that TYPES something the document does not carry.
77
+ * @param {any} value
78
+ * @param {string} what - the member's own name, a NOUN: the message makes
79
+ * it the subject of a sentence, so a phrase here reads as nonsense
80
+ * @param {string} types - what the builder types, for the clause after it
81
+ * @param {string} at
82
+ */
83
+ function typeOnly(value, what, types, at) {
84
+ if (!isSchemaBuilder(value)) {
85
+ throw new LinqBuildError('JL0101',
86
+ `${what} is a schema-pen builder — it types ${types}, and the app format carries no `
87
+ + `schema for it, so nothing is emitted for it; got ${describeValue(value)}`, at);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * One effect invocation (§5.1): `{ run, with? }`. The props are a value
93
+ * in the action's own scope — an expression, a literal, or a tree of
94
+ * both — because the action capture spells the whole transition.
95
+ * @param {string} run - the registered handler name (`options.effects[run]`)
96
+ * @param {any} [props] - the handler's `with`
97
+ * @returns {any} the effect declaration
98
+ * @example
99
+ * effect('http', { url: '/api/todos', done: 'todo/loaded' });
100
+ * effect('contract', { op: 'catalog.load', input: x.payload });
101
+ */
102
+ export function effect(run, props = undefined) {
103
+ return effectDescriptor(run, props, (value) => value);
104
+ }
105
+
106
+ /**
107
+ * A transition object (§3.2): the next state whole, an RFC 6902 patch
108
+ * over it, and the effects that run after it settles — written in the
109
+ * order the runtime applies them.
110
+ *
111
+ * Returning nothing from an action is the format's own no-op; a
112
+ * transition with no member is that same empty object, and is allowed.
113
+ *
114
+ * @param {{ state?: any, patch?: readonly any[], effects?: readonly any[] }} spec
115
+ * @returns {any} the transition, for the action capture to spell
116
+ * @throws {LinqBuildError} `JL0101` a member the pen does not know, or a
117
+ * patch/effects list that is not one
118
+ * @example
119
+ * transition({ patch: [add((st) => st.todos, x.payload)] });
120
+ * transition({ state: () => null, effects: [effect('save')] });
121
+ */
122
+ export function transition(spec) {
123
+ if (!isPlainObject(spec)) {
124
+ throw new LinqBuildError('JL0101',
125
+ `transition() takes { state?, patch?, effects? }, got ${describeValue(spec)}`);
126
+ }
127
+ closedTo(spec, TRANSITION_MEMBERS, 'transition()');
128
+ const out = {};
129
+ if (spec.state !== undefined) out.state = spec.state;
130
+ if (spec.patch !== undefined) out.patch = readPatch(spec.patch);
131
+ if (spec.effects !== undefined) out.effects = readEffects(spec.effects, 'transition() effects');
132
+ return out;
133
+ }
134
+
135
+ /**
136
+ * One action document (§3): a callback captured over the state, `$event`
137
+ * and `$payload`, whose result is a transition.
138
+ *
139
+ * `payload` and `event` type what the dispatch carries — the format
140
+ * holds no schema for either, so the pen emits nothing for them; what
141
+ * they buy is the typed `x.payload` and the `$event` members an action
142
+ * may read.
143
+ *
144
+ * @param {(state: any, externals: any) => any} fn - `(s, x) => transition(…)`
145
+ * @param {{ payload?: any, event?: readonly string[] }} [options]
146
+ * @returns {any} the action declaration
147
+ * @throws {LinqBuildError} `JL0101` a member the pen does not know, or a
148
+ * `payload` that is not a builder; `JL0102` an `event` field §3.1
149
+ * excludes; `JL0104` a name §3.1 does not bind
150
+ * @example
151
+ * action((s, x) => transition({ patch: [add((st) => st.todos, x.payload)] }),
152
+ * { payload: s.object({ text: s.string() }) });
153
+ */
154
+ export function action(fn, options = undefined) {
155
+ if (typeof fn !== 'function') {
156
+ throw new LinqBuildError('JL0101',
157
+ `action() takes a callback (s, x) => transition(…), got ${describeValue(fn)}`);
158
+ }
159
+ if (options !== undefined) {
160
+ if (!isPlainObject(options)) {
161
+ throw new LinqBuildError('JL0101',
162
+ `action() options are { payload?, event? }, got ${describeValue(options)}`);
163
+ }
164
+ closedTo(options, ACTION_MEMBERS, 'action()');
165
+ if (options.payload !== undefined) {
166
+ typeOnly(options.payload, 'action() payload', "the dispatch's $payload", '/payload');
167
+ }
168
+ if (options.event !== undefined) readEventFields(options.event, 'action()');
169
+ }
170
+ const document = captureAction('action()', fn);
171
+ const out = { document };
172
+ Object.defineProperty(out, ACTION, { value: true, enumerable: false });
173
+ return Object.freeze(out);
174
+ }
175
+
176
+ /**
177
+ * The `event` member of a binding (§4): an array of the extra `$event`
178
+ * field names to resolve. A name §3.1 excludes by construction is
179
+ * `JL0102`; every other name is allowed, because a host extractor
180
+ * registered under it wins over the built-in list and the pen cannot
181
+ * see the host's registry.
182
+ * @param {any} fields
183
+ * @param {string} what
184
+ * @returns {string[]}
185
+ */
186
+ function readEventFields(fields, what) {
187
+ if (!Array.isArray(fields)) {
188
+ throw new LinqBuildError('JL0101',
189
+ `${what} event is an array of field names (APP-FORMAT §4), got ${describeValue(fields)}`,
190
+ '/event');
191
+ }
192
+ return fields.map((name, i) => {
193
+ if (typeof name !== 'string' || name === '') {
194
+ throw new LinqBuildError('JL0101',
195
+ `${what} event[${i}] is a field name, got ${describeValue(name)}`, `/event/${i}`);
196
+ }
197
+ if (EXCLUDED_FIELDS.has(name)) {
198
+ throw new LinqBuildError('JL0102',
199
+ `an event binding cannot request '${name}' — APP-FORMAT §3.1 excludes target, files, `
200
+ + 'touch lists and every other host-object-valued field by construction, because '
201
+ + '$event MUST survive JSON.stringify, the same invariant as state. Register an '
202
+ + 'extractor under a name of its own (§5.4\'s worked example maps event.target.files '
203
+ + "to opaque string tokens under 'fileTokens') and request that.", `/event/${i}`);
204
+ }
205
+ return name;
206
+ });
207
+ }
208
+
209
+ /**
210
+ * One event binding (§4): `{ action, with?, event?, preventDefault?,
211
+ * stopPropagation? }`, in the member order the format writes them.
212
+ *
213
+ * A payload is data built at render time, so it may embed anything in
214
+ * the stylesheet body's scope — the matched node's own members, `$path`,
215
+ * `$root` — which is what replaces a payload-creator function.
216
+ *
217
+ * @template {string} Names
218
+ * @param {Names} name - the action to dispatch
219
+ * @param {{ payload?: any, event?: readonly string[], preventDefault?: boolean, stopPropagation?: boolean }} [options]
220
+ * @returns {any} the binding, for the view's body capture to spell
221
+ * @throws {LinqBuildError} `JL0101` an empty name, a member the pen does
222
+ * not know, or a non-boolean control; `JL0102` an `event` field §3.1 excludes
223
+ * @example
224
+ * bind('selectRow', { payload: { id: v.id }, event: ['shiftKey', 'ctrlKey'] });
225
+ * bind('rowOpen', { stopPropagation: true });
226
+ */
227
+ export function bind(name, options = undefined) {
228
+ if (typeof name !== 'string' || name === '') {
229
+ throw new LinqBuildError('JL0101',
230
+ `bind() takes an action name as a non-empty string, got ${describeValue(name)}`,
231
+ '/action');
232
+ }
233
+ const out = { action: name };
234
+ if (options !== undefined) {
235
+ if (!isPlainObject(options)) {
236
+ throw new LinqBuildError('JL0101',
237
+ 'bind() options are { payload?, event?, preventDefault?, stopPropagation? }, got '
238
+ + describeValue(options));
239
+ }
240
+ closedTo(options, BIND_MEMBERS, 'bind()');
241
+ if (options.payload !== undefined) out.with = options.payload;
242
+ if (options.event !== undefined) out.event = readEventFields(options.event, 'bind()');
243
+ for (const control of ['preventDefault', 'stopPropagation']) {
244
+ if (options[control] === undefined) continue;
245
+ if (typeof options[control] !== 'boolean') {
246
+ throw new LinqBuildError('JL0101',
247
+ `bind() ${control} is a boolean — it is allowed only on the object binding form `
248
+ + `and defaults to false (APP-FORMAT §4), got ${describeValue(options[control])}`,
249
+ `/${control}`);
250
+ }
251
+ out[control] = options[control];
252
+ }
253
+ }
254
+ return out;
255
+ }
@@ -0,0 +1,63 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The one capture under an app document's query-valued members.
4
+ *
5
+ * An action is a query document evaluated with three names and no others
6
+ * (APP-FORMAT §3.1): `$` is the whole state, `$event` the serializable
7
+ * event slice, `$payload` the dispatch payload. So a callback receives
8
+ * the state at its first argument and those two externals at its second,
9
+ * and a name the loop does not bind is `JL0104` here — where the fix can
10
+ * be named — rather than an unbound external at dispatch time.
11
+ *
12
+ * A subscription's dynamic members evaluate in a NARROWER scope: `when`
13
+ * and `for` see the state alone, `withQuery` and `key` additionally see
14
+ * `$item` and only under `for` (§5.3, "compiled closed-world"). The
15
+ * pen mirrors that scope exactly, so a document it writes cannot be the
16
+ * `JA0008` a free variable would be at `createApp` time.
17
+ *
18
+ * Every capture here folds NOTHING: a transition, a binding and an
19
+ * effect's props are spelled as the constructors the format's own
20
+ * examples carry (`{ "op": "replace", … }`, `{ "ms": 1000 }`), never
21
+ * collapsed into one `$const`.
22
+ */
23
+
24
+ import { captureQuery } from '../capture-root.js';
25
+
26
+ /** §3.1's ambient vocabulary, for a `JL0104` message. */
27
+ export const ACTION_SCOPE = 'an action document (APP-FORMAT §3.1)';
28
+
29
+ /** The externals the dispatch loop binds on every action. */
30
+ const ACTION_EXTERNALS = Object.freeze(['event', 'payload']);
31
+
32
+ /** How an unbound action name could be reached, appended to `JL0104`. */
33
+ const ACTION_ADVICE = () => ' — the whole state is the first argument; §3.1 binds $event and '
34
+ + '$payload and nothing else, and a host value reaches an action through a binding\'s '
35
+ + 'payload or an event-field extractor';
36
+
37
+ /**
38
+ * Capture one action-scope callback: `(s, x) => …` over `$`, `$event`
39
+ * and `$payload`.
40
+ * @param {string} what - the method, for the message
41
+ * @param {(state: any, externals: any) => any} fn
42
+ * @returns {any} the query document (plain JSON)
43
+ */
44
+ export function captureAction(what, fn) {
45
+ return captureQuery(what, ACTION_EXTERNALS, fn, { advice: ACTION_ADVICE, fold: false });
46
+ }
47
+
48
+ /**
49
+ * Capture one subscription member over §5.3's closed world.
50
+ * @param {string} what - the member, for the message
51
+ * @param {readonly string[]} externals - `['item']` under `for`, else none
52
+ * @param {any} value - a callback, or a query document written by hand
53
+ * @returns {any} the query document (plain JSON)
54
+ */
55
+ export function captureSub(what, externals, value) {
56
+ if (typeof value !== 'function') return JSON.parse(JSON.stringify(value));
57
+ const advice = () => (externals.length === 0
58
+ ? ` — a subscription's ${what} is compiled closed-world against the state alone; $item `
59
+ + 'exists only under a "for" declaration (APP-FORMAT §5.3)'
60
+ : ` — a subscription's ${what} under "for" sees the state and $item, nothing else `
61
+ + '(APP-FORMAT §5.3)');
62
+ return captureQuery(`sub() ${what}`, externals, value, { advice, fold: false });
63
+ }
@@ -0,0 +1,260 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `defineApp()` — one `jaren-app` 0.1 document (APP-FORMAT §2),
4
+ * deep-frozen, that `createApp` takes unchanged, and beside it the JSON
5
+ * Schema of its state.
6
+ *
7
+ * The two are answered as two members of ONE result and never merged:
8
+ * the format has no slot for a state schema, and the hook that wants one
9
+ * is `options.validateState` (§6), which is a `createApp` option rather
10
+ * than a document member. `{ document, stateSchema }` is therefore what
11
+ * the pen returns — the document is exactly the format's, and the schema
12
+ * goes where the invariant lives.
13
+ *
14
+ * The initial state is DERIVED from the state builder's `default()`s
15
+ * when the author does not write one: a member contributes its default,
16
+ * an object recurses, and a REQUIRED member that resolves to nothing is
17
+ * `JL0102` naming its pointer — because a state whose required member is
18
+ * absent fails its own `validateState` on the boot transaction, which is
19
+ * a fatal `JA0007` and a bad way to learn about a missing default.
20
+ *
21
+ * `defineApp` also holds one rule §4 leaves to run time: every literal
22
+ * action name the view binds must be declared. The runtime reports an
23
+ * unknown name as `JA2001` per dispatch, silently dropping the user's
24
+ * click; the pen can see the whole document at once and refuses it.
25
+ */
26
+
27
+ import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
28
+
29
+ import { LinqBuildError } from '../errors.js';
30
+ import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
31
+ import { isSchemaBuilder, schemaOf } from '../schema/brand.js';
32
+ import { ACTION } from './action.js';
33
+ import { SUB } from './sub.js';
34
+
35
+ const APP_VERSION = '0.1';
36
+
37
+ /** The members `defineApp()` takes. */
38
+ const APP_MEMBERS = Object.freeze(['state', 'initial', 'schema', 'view', 'actions', 'subs']);
39
+
40
+ /** @param {any} value */
41
+ function isPlainObject(value) {
42
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
43
+ }
44
+
45
+ /** A JSON value, copied: the document is a value of its own. @param {any} v */
46
+ const copy = (v) => JSON.parse(JSON.stringify(v));
47
+
48
+ /**
49
+ * The initial value a schema's `default()`s describe.
50
+ *
51
+ * A `default` answers itself. An object schema recurses: every member
52
+ * that resolves contributes, and a member `required` names but which
53
+ * resolves to nothing is refused. An object resolves when it is
54
+ * required, or when at least one of its own members did — so an
55
+ * optional block of defaults appears and an optional empty one does not.
56
+ *
57
+ * @param {any} schema
58
+ * @param {string} at - the pointer into the state, for the message
59
+ * @param {boolean} needed - whether the parent requires this member
60
+ * @returns {{ has: boolean, value?: any }}
61
+ */
62
+ function initialOf(schema, at, needed) {
63
+ if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
64
+ return { has: false };
65
+ }
66
+ if (schema.default !== undefined) return { has: true, value: copy(schema.default) };
67
+ if (schema.const !== undefined) return { has: true, value: copy(schema.const) };
68
+ if (!isPlainObject(schema.properties)) return { has: false };
69
+
70
+ const required = Array.isArray(schema.required) ? schema.required : [];
71
+ const out = {};
72
+ let any = false;
73
+ for (const [name, member] of Object.entries(schema.properties)) {
74
+ const isRequired = required.includes(name);
75
+ const resolved = initialOf(member, `${at}/${name}`, isRequired);
76
+ if (resolved.has) { setObjectMember(out, name, resolved.value); any = true; continue; }
77
+ if (!isRequired) continue;
78
+ throw new LinqBuildError('JL0102',
79
+ `the initial state cannot be derived: '${at}/${name}' is required and declares no `
80
+ + 'default — give the member a default(), or pass the whole initial state as '
81
+ + "defineApp()'s 'initial'", `${at}/${name}`);
82
+ }
83
+ return needed || any ? { has: true, value: out } : { has: false };
84
+ }
85
+
86
+ /**
87
+ * A literal action name, or `null`: a value starting with `$` is a query
88
+ * expression naming the action at render time, which no pen can resolve.
89
+ * @param {any} value
90
+ * @returns {string | null}
91
+ */
92
+ function literalName(value) {
93
+ return typeof value === 'string' && value !== '' && value[0] !== '$' ? value : null;
94
+ }
95
+
96
+ /**
97
+ * Every literal action name the view binds. §4 gives a binding two
98
+ * forms and this reads both: a string under an `on` map (a vnode's
99
+ * event props, VIEW-FORMAT's own member), and an object carrying an
100
+ * `action` member anywhere — that member IS §4's vocabulary, so an
101
+ * object holding one is a binding wherever a widget's `emit` will find
102
+ * it.
103
+ * @param {any} node
104
+ * @param {Set<string>} out
105
+ */
106
+ function collectBoundActions(node, out) {
107
+ if (Array.isArray(node)) {
108
+ for (const child of node) collectBoundActions(child, out);
109
+ return;
110
+ }
111
+ if (node === null || typeof node !== 'object') return;
112
+ for (const [key, value] of Object.entries(node)) {
113
+ if (key === 'action') {
114
+ const name = literalName(value);
115
+ if (name !== null) out.add(name);
116
+ }
117
+ if (key === 'on' && isPlainObject(value)) {
118
+ for (const binding of Object.values(value)) {
119
+ const name = literalName(binding);
120
+ if (name !== null) out.add(name);
121
+ }
122
+ }
123
+ collectBoundActions(value, out);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * The `view` member: a JSLT stylesheet, envelope or bare rule array
129
+ * (§2), from the JSLT pen or by hand.
130
+ * @param {any} view
131
+ * @returns {any}
132
+ */
133
+ function readView(view) {
134
+ if (view === undefined) {
135
+ throw new LinqBuildError('JL0101',
136
+ 'defineApp() needs a view — a JSLT stylesheet, from stylesheet([rule(…)]) or as a '
137
+ + 'bare rule array; the format requires the member and the runtime refuses an app '
138
+ + 'without one (JA0002)', '/view');
139
+ }
140
+ if (!Array.isArray(view) && !isPlainObject(view)) {
141
+ throw new LinqBuildError('JL0101',
142
+ `defineApp() view is a JSLT stylesheet document or a bare rule array, got ${describeValue(view)}`,
143
+ '/view');
144
+ }
145
+ return copy(requireJson(view, 'defineApp() view'));
146
+ }
147
+
148
+ /**
149
+ * Write a `jaren-app` 0.1 document (APP-FORMAT.md §2) and the JSON
150
+ * Schema of its state.
151
+ *
152
+ * @param {any} spec - `{ state, initial?, schema?, view, actions?, subs? }`
153
+ * @returns {{ document: any, stateSchema: any }} the deep-frozen document
154
+ * and the state's schema (`null` when the state is a plain value with
155
+ * no `schema` beside it)
156
+ * @throws {LinqBuildError} `JL0101` a value the pen cannot spell;
157
+ * `JL0102` an initial state no default describes, or a bound action
158
+ * name `actions` does not declare
159
+ * @example
160
+ * const { document, stateSchema } = defineApp({
161
+ * state: s.object({ count: s.integer().default(0) }),
162
+ * view: [rule('$', (v) => ['h1', {}, 'Count: ', v.count])],
163
+ * actions: { inc: action((st) => transition({ patch: [replace((c) => c.count, st.count.add(1))] })) },
164
+ * });
165
+ * createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) });
166
+ */
167
+ export function defineApp(spec) {
168
+ if (!isPlainObject(spec)) {
169
+ throw new LinqBuildError('JL0101',
170
+ `defineApp() takes { state, initial?, schema?, view, actions?, subs? }, got ${describeValue(spec)}`);
171
+ }
172
+ for (const key of Object.keys(spec)) {
173
+ if (!APP_MEMBERS.includes(key)) {
174
+ throw new LinqBuildError('JL0101',
175
+ `defineApp() does not take '${key}' — it takes ${APP_MEMBERS.join(', ')}`, `/${key}`);
176
+ }
177
+ }
178
+
179
+ const stateIsBuilder = isSchemaBuilder(spec.state);
180
+ if (stateIsBuilder && spec.schema !== undefined) {
181
+ throw new LinqBuildError('JL0101',
182
+ "defineApp() takes 'schema' beside a state given as a plain JSON value — a state "
183
+ + 'given as a builder IS its schema', '/schema');
184
+ }
185
+ if (spec.schema !== undefined && !isSchemaBuilder(spec.schema)) {
186
+ throw new LinqBuildError('JL0101',
187
+ `defineApp() schema is a schema-pen builder, got ${describeValue(spec.schema)}`, '/schema');
188
+ }
189
+ const stateSchema = stateIsBuilder ? schemaOf(spec.state)
190
+ : spec.schema !== undefined ? schemaOf(spec.schema) : null;
191
+
192
+ let state;
193
+ if (spec.initial !== undefined) state = copy(requireJson(spec.initial, 'defineApp() initial'));
194
+ else if (stateIsBuilder) state = initialOf(stateSchema, '', true).value;
195
+ else if (spec.state !== undefined) state = copy(requireJson(spec.state, 'defineApp() state'));
196
+
197
+ const view = readView(spec.view);
198
+
199
+ /** @type {Record<string, any> | undefined} */
200
+ let actions;
201
+ if (spec.actions !== undefined) {
202
+ if (!isPlainObject(spec.actions)) {
203
+ throw new LinqBuildError('JL0101',
204
+ `defineApp() actions is an object of named action() declarations, got ${describeValue(spec.actions)}`,
205
+ '/actions');
206
+ }
207
+ requireNameMap(spec.actions, 'defineApp() actions', '/actions');
208
+ actions = {};
209
+ for (const [name, declared] of Object.entries(spec.actions)) {
210
+ if (!isPlainObject(declared) || declared[ACTION] !== true) {
211
+ throw new LinqBuildError('JL0101',
212
+ `defineApp() action '${name}' is action((s, x) => transition(…)), got ${describeValue(declared)}`,
213
+ `/actions/${name}`);
214
+ }
215
+ setObjectMember(actions, name, declared.document);
216
+ }
217
+ }
218
+
219
+ /** @type {any[] | undefined} */
220
+ let subs;
221
+ if (spec.subs !== undefined) {
222
+ if (!Array.isArray(spec.subs)) {
223
+ throw new LinqBuildError('JL0101',
224
+ `defineApp() subs is an array of sub() declarations, got ${describeValue(spec.subs)}`,
225
+ '/subs');
226
+ }
227
+ subs = spec.subs.map((declared, i) => {
228
+ if (!isPlainObject(declared) || declared[SUB] !== true) {
229
+ throw new LinqBuildError('JL0101',
230
+ `defineApp() subs[${i}] is sub(run, options?), got ${describeValue(declared)}`,
231
+ `/subs/${i}`);
232
+ }
233
+ return { ...declared };
234
+ });
235
+ }
236
+
237
+ /** @type {Set<string>} */
238
+ const bound = new Set();
239
+ collectBoundActions(view, bound);
240
+ const declared = Object.keys(actions ?? {});
241
+ for (const name of bound) {
242
+ if (declared.includes(name)) continue;
243
+ throw new LinqBuildError('JL0102',
244
+ `the view binds the action '${name}', which "actions" does not declare — the runtime `
245
+ + 'drops such a dispatch and reports JA2001 (APP-FORMAT §4), so the user\'s click does '
246
+ + `nothing; the declared actions are ${declared.length === 0 ? 'none'
247
+ : declared.map((n) => `'${n}'`).join(', ')}`, '/view');
248
+ }
249
+
250
+ const document = { $app: APP_VERSION };
251
+ if (state !== undefined) document.state = state;
252
+ document.view = view;
253
+ if (actions !== undefined) document.actions = actions;
254
+ if (subs !== undefined) document.subs = subs;
255
+
256
+ return Object.freeze({
257
+ document: deepFreeze(document),
258
+ stateSchema: stateSchema === null ? null : deepFreeze(copy(stateSchema)),
259
+ });
260
+ }
@@ -0,0 +1,20 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/app` — a `jaren-app` 0.1 application by code.
4
+ * `defineApp()` writes the document `createApp` runs: a state whose
5
+ * initial value comes from its own schema's defaults, the JSLT pen's
6
+ * stylesheet as the view, actions captured over APP-FORMAT §3.1's three
7
+ * names, patch paths derived from the state shape, and subscriptions
8
+ * compiled in §5.3's closed world. It answers the document and the
9
+ * state's JSON Schema as two members of one result — the format has no
10
+ * slot for the second, and `validateState` is where it belongs.
11
+ *
12
+ * Everything the document holds is data, so everything this pen writes
13
+ * is JSON: nothing here imports `@jarenjs/app`, and the loop's compiler
14
+ * stays the only judge of what an app means.
15
+ */
16
+
17
+ export { defineApp } from './define.js';
18
+ export { action, transition, effect, bind } from './action.js';
19
+ export { add, append, replace, remove, move, copy, test } from './patch.js';
20
+ export { sub } from './sub.js';