@jarenjs/linq 0.56.0 → 0.67.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.
@@ -35,17 +35,17 @@ is the index of the ten, and it is how a reader reaches any of them.
35
35
  <!--fact:pens.index-->
36
36
  | Document | Lines | What it writes, and when to open it |
37
37
  |---|---:|---|
38
- | [LINQ-FORMAT.md](LINQ-FORMAT.md) | 812 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the ten guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the eleven documents, or one place to look up a method without knowing which pen owns it |
39
- | [QUERY-PEN.md](QUERY-PEN.md) | 1,636 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
38
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | 820 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the ten guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the eleven documents, or one place to look up a method without knowing which pen owns it |
39
+ | [QUERY-PEN.md](QUERY-PEN.md) | 1,724 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
40
40
  | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,218 | `./schema` — JSON Schema 2020-12: the structural keywords, the constraints and the annotations, each with a method of its own, plus `$query`, `$defs`/`$ref` recursion and the normalizer's per-field predicates. **Read it when** you are describing the shape of data — for validation, for a form, or as the base of an entity |
41
- | [MODEL-PEN.md](MODEL-PEN.md) | 1,083 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
41
+ | [MODEL-PEN.md](MODEL-PEN.md) | 1,092 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
42
42
  | [JSLT-PEN.md](JSLT-PEN.md) | 955 | `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose bodies are captured over the matched value. **Read it when** you are transforming one document into another |
43
43
  | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 781 | `./migration` — `$migration` 0.1 documents: the two shape hashes and the ordered steps the runner takes. **Read it when** you are moving a store from one model to the next |
44
- | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,217 | `./contract` — `$contract` 0.1 documents: the operations, their schemas, their declared behavior and their REST binding. **Read it when** you are declaring an API and want its client, its server and its tools typed from one document |
45
- | [FLOW-PEN.md](FLOW-PEN.md) | 1,026 | `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows, every query-valued member captured. **Read it when** you are declaring a state machine or a dependency graph of tasks |
44
+ | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,221 | `./contract` — `$contract` 0.1 documents: the operations, their schemas, their declared behavior and their REST binding. **Read it when** you are declaring an API and want its client, its server and its tools typed from one document |
45
+ | [FLOW-PEN.md](FLOW-PEN.md) | 1,033 | `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows, every query-valued member captured. **Read it when** you are declaring a state machine or a dependency graph of tasks |
46
46
  | [APP-PEN.md](APP-PEN.md) | 1,143 | `./app` — the `jaren-app` 0.1 document `createApp` runs, and the JSON Schema of its state beside it. **Read it when** you are declaring a whole application: state, view, actions, effects |
47
47
  | [FORMS-PEN.md](FORMS-PEN.md) | 940 | `./forms` — the `x-form` vocabulary on JSON Schema, and `assertOnSubmit()`, the same rules' layer-3 `$query` twin. **Read it when** you are turning a schema into a form |
48
- | [DB-CLIENT.md](DB-CLIENT.md) | 814 | `./db` — the client: the store's typed front door, not a pen, and the package's one runtime edge. **Read it when** you are reading or writing rows: `load`, `include`, `link`/`unlink`, `live` |
48
+ | [DB-CLIENT.md](DB-CLIENT.md) | 893 | `./db` — the client: the store's typed front door, not a pen, and the package's one runtime edge. **Read it when** you are reading or writing rows: `load`, `include`, `link`/`unlink`, `live` |
49
49
  <!--/fact-->
50
50
 
51
51
  Every row of that table is derived, and none of it is written here: the
@@ -53,7 +53,7 @@ sentence is the document's own opening line, the length is the file's,
53
53
  and `npm run docs:derive` writes the table out of the eleven documents
54
54
  beside this one. The line counts are not decoration — they are what tells
55
55
  a reader whether the document they are about to open is a ten-minute read
56
- or an afternoon — and `test/docs/linq-citations.test.js` holds each one
56
+ or an afternoon — and `test/docs/format-citations.test.js` holds each one
57
57
  equal to the file it names, independently of the derivation.
58
58
 
59
59
  A document missing from that table is a document a reader cannot reach:
@@ -182,7 +182,7 @@ mirrored in QUERY-PEN §9 (one table, held equal by a test):
182
182
  | `JL0102` | a pen was asked for a construct the format cannot carry: a function `refine`/`transform` (cross-field rules are `check()`; transforms are application code), a coercion the normalizer would never run, closed objects under `allOf`, an annotation on `never()`, a draft the pen does not write; in the JSLT pen an `apply()` as a bare object member (the `[]` idiom, JSLT-FORMAT §6.3 — the engine would fail at run time on the second child), a `match` of `{}` (the compiler's `JT0003`, earlier), an `apply()` outside a body; in the contract pen a path template form CONTRACT-FORMAT §4.2 reserves (the compiler's `JC0008`, earlier, naming the same form), a member mapped to `path` the template does not declare, or a hand-written operation `kind` outside `read`/`command`/`subscribe`; in the flow pen a guard given as a plain STRING (FLOW-FORMAT §3 makes a non-`$` literal vacuously true, so a projected display annotation must not decide execution), or a state or node id no declaration carries (the compiler's `JF0004`/`JF0006`/`JF0013`, earlier, naming the id); in the app pen a patch path that is not a chain of member reads and subscripts (a JSON Pointer cannot be written for it), an `$event` field APP-FORMAT §3.1 excludes by construction (`target`, `files`, a touch list — `$event` must survive `JSON.stringify`), an initial state no `default()` describes, a subscription-member combination §5.3 calls `JA0008`, or a view binding an action `actions` does not declare (the loop's `JA2001`, earlier, naming the declared ones); in the forms pen `preview`, which the format registry DERIVES from the field's own `format` |
183
183
  | `JL0103` | a `$defs` name collision (two distinct builders under one name), a `ref()` no definition answers, or a `lazy()` that does not return a named builder |
184
184
  | `JL0104` | a pen-owned keyword written through `meta()`, or an external a captured rule did not declare: a `check()` external other than `root`/`path`, a `compute()` external at all, a `body()` external other than `root`/`path` and its declared parameters — or `root`/`path` declared as one, since the engine binds them; a flow guard, effect `with`, node query or edge `select` external at all, since both flow engines evaluate with one `$` and nothing else; an app action naming anything but `$event` and `$payload` (APP-FORMAT §3.1's whole ambient vocabulary), a subscription member naming anything but `$item`, and that only under `for` (§5.3's closed world); a form rule naming anything but the context's `root`, `value` and `pointer` |
185
- | `JL0105` | a relation hop on the chain — the query pen (QUERY-PEN §4, relation navigation) — cannot lower: the member is a many-to-many relation, whose join table is not a queryable root in this version (`load({ include })` reads the memberships); the relation's key column or the key it references is composite or undeclared; or the provider's relation table holds something that is not a relation record |
185
+ | `JL0105` | a relation hop on the chain — the query pen (QUERY-PEN §4, relation navigation) — cannot lower: the relation's key column or the key it references is composite or undeclared; a many-to-many entry does not name its join row's columns (`{ joinTable, ownColumn, ownKey, targetColumn, targetKey }`), so there is no join root to lower through; the relation kind is not one this surface lowers; or the provider's relation table holds something that is not a relation record |
186
186
  | `JL0106` | a migration step names an entity or collection the target model does not declare (`transform`, `assert`, `derive`); or a `transform` over a planned document finds no draft to replace, or two drafts for one name |
187
187
  | `JL0107` | the client (`@jarenjs/linq/db`, [DB-CLIENT.md](DB-CLIENT.md)) was handed a member that is not the relation kind the operation needs: `include()` picks a declared relation member — a scalar member, or a name the model does not declare, is refused naming the declared ones; `link()`/`unlink()` attach many-to-many memberships only — a to-one or to-many relation is refused naming its kind |
188
188
 
@@ -201,18 +201,18 @@ and the bundle is the byte count the tree-shaking probe builds.
201
201
  <!--fact:pens.census-->
202
202
  | Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
203
203
  |---|---|---:|---:|---:|---:|---:|
204
- | [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 812 | — | — | — | — |
205
- | [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,636 | 34 | 8 | 14 | 173,080 B |
206
- | [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,218 | 66 | 10 | 4 | 32,427 B |
207
- | [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,083 | 28 | 6 | 3 | 40,857 B |
208
- | [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,124 B |
209
- | [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 23,599 B |
210
- | [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,217 | 37 | 6 | 3 | 44,644 B |
211
- | [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,026 | 16 | 7 | 3 | 19,181 B |
212
- | [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 46,862 B |
213
- | [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 36,587 B |
214
- | [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 814 | 36 | 4 | 2 | 478,172 B |
215
- | **eleven documents** | | **11,625** | **285** | **67** | | |
204
+ | [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 820 | — | — | — | — |
205
+ | [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,724 | 34 | 8 | 15 | 174,264 B |
206
+ | [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,218 | 66 | 10 | 4 | 33,156 B |
207
+ | [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,092 | 28 | 6 | 3 | 41,582 B |
208
+ | [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,856 B |
209
+ | [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 24,259 B |
210
+ | [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 45,298 B |
211
+ | [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 19,910 B |
212
+ | [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 47,444 B |
213
+ | [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 37,312 B |
214
+ | [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 893 | 41 | 4 | 2 | 623,994 B |
215
+ | **eleven documents** | | **11,820** | **291** | **67** | | |
216
216
  <!--/fact-->
217
217
 
218
218
  A pen whose mapping rows are far below its worked examples is a pen
@@ -254,16 +254,16 @@ it and each document publishes it. The rounded column is what
254
254
  <!--fact:pens.cost-->
255
255
  | Subpath | Document | Bundle | Rounded |
256
256
  |---|---|---:|---:|
257
- | `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 173,080 B | 173 kB |
258
- | `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 32,427 B | 32 kB |
259
- | `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 40,857 B | 41 kB |
260
- | `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,124 B | 19 kB |
261
- | `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 23,599 B | 24 kB |
262
- | `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 44,644 B | 45 kB |
263
- | `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,181 B | 19 kB |
264
- | `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 46,862 B | 47 kB |
265
- | `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 36,587 B | 37 kB |
266
- | `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 478,172 B | 478 kB |
257
+ | `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 174,264 B | 174 kB |
258
+ | `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 33,156 B | 33 kB |
259
+ | `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 41,582 B | 42 kB |
260
+ | `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,856 B | 20 kB |
261
+ | `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 24,259 B | 24 kB |
262
+ | `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 45,298 B | 45 kB |
263
+ | `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,910 B | 20 kB |
264
+ | `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 47,444 B | 47 kB |
265
+ | `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 37,312 B | 37 kB |
266
+ | `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 623,994 B | 624 kB |
267
267
  <!--/fact-->
268
268
 
269
269
  Read these as prices, not as scores. `./db` is the largest by an order of
@@ -504,6 +504,8 @@ it says.
504
504
  | `collection(schema, { key?, identity?, indexes?, renamedFrom? })` | `{ schema, key?, identity?, indexes?, 'x-rename'? }` — `key` is an RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`) or `null` (the store allocates, `identity` says how); the other options ride verbatim | `CollectionSpec<Infer<B>>`, carrying the document shape its paths are checked against | native; an option outside the four, a key that is neither pointer nor lambda nor `null`, an `indexes` that is not an array of `index()` entries, all `JL0101` |
505
505
  | `index(path, options?)` | `{ name, path, unique?, derive?, precision?, dims?, physical? }` — `path` is a captured lambda (`(p) => p.cell` → `$.cell`), a non-empty array of them (a composite), or a JSONPath string; `name` defaults to `by_<segments>`; the rest ride verbatim for the store's model walk to judge (§2.1) | `IndexPath<D>` over the collection's shape: a member the shape lacks does not compile | native; an option outside the six is `JL0101`; a lambda that answers an operator result or a surface method is `JL0102` |
506
506
 
507
+ | `expressionIndex(expression, options?)` | `{ name, expression, unique? }` — an index over a COMPUTED value: a `{ call, args }` node whose arguments are member lambdas, JSONPath strings, JSON scalars or further calls; `name` defaults to `by_<call>_<members>` | the expression's member lambdas are checked against the collection's shape | native; a node outside the vocabulary, an option outside the two, and anything that looks like SQL text are all `JL0101` |
508
+
507
509
  **The model document**
508
510
 
509
511
  | Method | Emits | Type reading | Status |
@@ -627,7 +629,8 @@ it says.
627
629
  | Method | Emits | Type reading | Status |
628
630
  |---|---|---|---|
629
631
  | `typedClient(client, contract)` | — (identity) | `TypedClient<C>`: `invoke` over the invokable operations, `subscribe` over the subscribe ones, `url` over all of them | native |
630
- | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C>`: one handler per invokable operation, `(input, ctx) => output \| Failure` | native; a missing or misspelled operation does not compile |
632
+ | `typedHttpClient(client, contract)` | — (identity) | `TypedHttpClient<C>`: `TypedClient<C>` plus `bytes` over `OpaqueOf<C>` — the opaque operations, whose success is a `ByteResponse` (a live stream) rather than the output type; for an `openHttpClient` client only, a local or port client has no `bytes` | native |
633
+ | `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C, Host = null, Carrier = 'http'>`: one handler per invokable operation, `(input, ctx) => output \| Failure`; `ctx` is `HandlerContext<Host, Carrier>` — the HTTP context by default, `Host` the host lifecycle's `ctx.host`, a carrier union a discriminated union to narrow on `ctx.carrier` (CONTRACT-FORMAT §7.7) | native; a missing or misspelled operation does not compile, and an HTTP-only member on a port/local context does not either |
631
634
  | `typedTools(tools, contract)` | — (identity) | `TypedTool<C>[]`: `name` is the id with `.` → `_`, `execute` takes the operation's ACCEPTED input | native |
632
635
 
633
636
  **What the pen does not judge**
@@ -764,12 +767,14 @@ it says.
764
767
  | `client.collections.<name>` | the store's collection | the same chain start and `live`, typed from the pen's collection schema (§2.5) |
765
768
  | `saveChanges()`, `transaction(fn)`, `close()`, `capabilities`, `store` | the store's | pass-throughs; `saveChanges` and `live` exist exactly when the model declares entities, as on the store; `store` is the escape hatch, typed `TypedStore` |
766
769
 
767
- **The two exported names**
770
+ **The exported names**
768
771
 
769
772
  | Name | Answers | Type reading |
770
773
  |---|---|---|
771
774
  | `open(model, options)` | a promise of the frozen client — `store`, `capabilities`, `entities`, `collections`, `transaction`, `close`, and `saveChanges`/`live` when the model declares entities | `Client<InferMeta<typeof model>>` for a pen model; `Client<E>` for `open<E>(json, …)`; the wide map for a bare JSON model |
775
+ | `defineReplication(header)` | a logical replication document builder — §2.7 | `ReplicationPen` |
772
776
  | `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
777
+ | `createDbLedger(client, options?)` | the contract idempotency ledger (`claim`/`commit`/`fail`/`lookup`/`sweep`) over a declared collection of the client's store — §2.6 | `DbLedger`; structurally `@jarenjs/contract`'s `Ledger` |
773
778
 
774
779
  **The entity handle**
775
780
 
@@ -792,12 +797,14 @@ it says.
792
797
  | `orderBy(key, options?)`, `orderByDescending(key, options?)` | `orderBy` | replaces; `options` is `{ empty?, collation? }` |
793
798
  | `thenBy(key, options?)`, `thenByDescending(key, options?)` | appends to `orderBy` | `JL0005` when no `orderBy` precedes it |
794
799
  | `take(n)`, `skip(n)` | `take`, `skip` | the offset window |
795
- | `after(cursor)` | `after` | the keyset cursor (§10.5); the ROOT only |
800
+ | `after(cursor)` | `after` | the keyset continuation (§10.5) a `page()` over the same ordering emitted — typed by the declared ordering, so a bare key does not compile; the ROOT only |
796
801
  | `maxDepth(n)` | `maxDepth` | the include depth bound (§10.4) |
797
802
  | `asNoTracking()` | — | changes the load, never the document |
798
803
  | `toSpec()`, `toJSON()` | the spec | plain deep-frozen JSON, a snapshot: mutating it changes nothing, and two builds are one document |
799
804
  | `toArray()` | — | `load(spec)`: the store's one statement |
800
- | `explain()` | — | `explainLoad(spec)`: the SQL, the includes, the pagination strategy |
805
+ | `cursor(options?)` | — | `loadCursor(spec, options)`: one root graph per pull from that same statement, its includes attached and bounded per root; `return()` releases it; `{ signal?, tracking? }` — untracked unless `tracking: true` |
806
+ | `page(options?)` | — | `page(spec, options)`: one bounded page over the composite keyset — `{ items, continuation, hasMore, snapshot }`, never more than `limit` roots or `maxBytes` bytes; `{ limit?, after?, maxBytes?, consistency?, signal?, tracking? }`; a `take`/`skip` on the graph beside it is the store's `JD0032` |
807
+ | `explain()` | — | `explainLoad(spec)`: the SQL, the includes, the pagination strategy, the per-root bounds |
801
808
 
802
809
  | Spec member | Emitted | Note |
803
810
  |---|---|---|
@@ -807,6 +814,7 @@ it says.
807
814
  | `orderBy: (p) => p.pid` | `orderBy: "$it.pid"` | a bare key, ascending |
808
815
  | `orderBy: { key, desc?, empty?, collation? }` | `orderBy: { $key, $dir, $empty, $collation }` | as the chain spells `$orderby`; an array of either is an array |
809
816
  | `take`, `skip` | `take`, `skip` | the window inside the subquery (a non-integer is the store's `JD0032`) |
817
+ | `maxRows`, `maxBytes` | `maxRows`, `maxBytes` | the per-root bounds (MODEL-FORMAT §10.4): rows of the relation per parent and serialised bytes per parent; crossing one is the store's `JD2073`, never a truncated graph. Defaults 1000 rows / 1 MiB (a `take` is the row bound of the include it windows); `Infinity` spells the unbounded case and emits as `null` |
810
818
  | `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
811
819
  | anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
812
820
  <!--/fact-->
@@ -745,7 +745,7 @@ from a drop plus a create, and guessing risks silent data loss.
745
745
 
746
746
  ## 7. Cost
747
747
 
748
- `@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->23,599<!--/fact--> bytes** as a minified,
748
+ `@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->24,259<!--/fact--> bytes** as a minified,
749
749
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
750
750
  measures and `npm run test:tree-shaking` reports, published rounded
751
751
  (<!--fact:bundle.migration.kb-->24<!--/fact--> kB) beside the other nine subpath prices in
@@ -778,4 +778,4 @@ proxy behind it, and the canonicalizer — and the two model documents
778
778
  themselves arrive as data, deep-frozen JSON that the consumer's own model
779
779
  module built. So a consumer who ships migrations to a browser does not
780
780
  ship the model pen with them; a consumer who OPENS a store does, and pays
781
- `./model`'s <!--fact:bundle.model-->40,857<!--/fact--> bytes for it.
781
+ `./model`'s <!--fact:bundle.model-->41,582<!--/fact--> bytes for it.
package/docs/MODEL-PEN.md CHANGED
@@ -197,11 +197,20 @@ no relations (MODEL-FORMAT §9.1); one physical engine sits under both.
197
197
  | `collection(schema, { key?, identity?, indexes?, renamedFrom? })` | `{ schema, key?, identity?, indexes?, 'x-rename'? }` — `key` is an RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`) or `null` (the store allocates, `identity` says how); the other options ride verbatim | `CollectionSpec<Infer<B>>`, carrying the document shape its paths are checked against | native; an option outside the four, a key that is neither pointer nor lambda nor `null`, an `indexes` that is not an array of `index()` entries, all `JL0101` |
198
198
  | `index(path, options?)` | `{ name, path, unique?, derive?, precision?, dims?, physical? }` — `path` is a captured lambda (`(p) => p.cell` → `$.cell`), a non-empty array of them (a composite), or a JSONPath string; `name` defaults to `by_<segments>`; the rest ride verbatim for the store's model walk to judge (§2.1) | `IndexPath<D>` over the collection's shape: a member the shape lacks does not compile | native; an option outside the six is `JL0101`; a lambda that answers an operator result or a surface method is `JL0102` |
199
199
 
200
- The option set is exactly `name`, `unique`, `derive`, `precision`,
201
- `dims`, `physical` (`src/model/collection.js:18`); the default name is
202
- `by_` plus the path's member segments, identifier-safe
200
+ | `expressionIndex(expression, options?)` | `{ name, expression, unique? }` — an index over a COMPUTED value: a `{ call, args }` node whose arguments are member lambdas, JSONPath strings, JSON scalars or further calls; `name` defaults to `by_<call>_<members>` | the expression's member lambdas are checked against the collection's shape | native; a node outside the vocabulary, an option outside the two, and anything that looks like SQL text are all `JL0101` |
201
+
202
+ The option set for `index()` is exactly `name`, `unique`, `derive`,
203
+ `precision`, `dims`, `physical` (`src/model/collection.js:18`); the
204
+ default name is `by_` plus the path's member segments, identifier-safe
203
205
  (`collection.js:76-80`) — `by_series_t`, `by_x_y`.
204
206
 
207
+ `expressionIndex()` takes only `name` and `unique`, because an
208
+ expression names the members it reads itself — nothing that describes a
209
+ member's storage belongs beside one. The pen resolves NO function name:
210
+ arity and determinism are the store's to check against the declarations
211
+ `openStore({ expressions })` was given, and a name this pen has never
212
+ heard of is not an error here. What it decides is the shape.
213
+
205
214
  ### 2.4 The model document
206
215
 
207
216
  The three calls that assemble the whole thing — the document, the
@@ -1053,10 +1062,10 @@ catch them:
1053
1062
 
1054
1063
  ## 7. Cost
1055
1064
 
1056
- `@jarenjs/linq/model` builds to **<!--fact:bundle.model-->40,857<!--/fact--> bytes** as a minified,
1065
+ `@jarenjs/linq/model` builds to **<!--fact:bundle.model-->41,582<!--/fact--> bytes** as a minified,
1057
1066
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1058
1067
  measures and `npm run test:tree-shaking` reports, published rounded
1059
- (<!--fact:bundle.model.kb-->41<!--/fact--> kB) beside the other nine subpath prices in
1068
+ (<!--fact:bundle.model.kb-->42<!--/fact--> kB) beside the other nine subpath prices in
1060
1069
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
1061
1070
 
1062
1071
  The probe is a gate, not a report: building a two-member model as a
@@ -1074,7 +1083,7 @@ them:
1074
1083
  byte of `packages/linq/src/model/`, because the subclasses are built by
1075
1084
  this subpath rather than patched onto the base classes.
1076
1085
 
1077
- The price above the schema pen's <!--fact:bundle.schema-->32,427<!--/fact--> is about 8 kB: the mixin, the
1086
+ The price above the schema pen's <!--fact:bundle.schema-->33,156<!--/fact--> is about 8 kB: the mixin, the
1078
1087
  three relation factories, `collection()`/`index()` with their capture,
1079
1088
  `defineModel()` — and the refusal MESSAGES, which are most of what §4
1080
1089
  costs. That is a deliberate trade: naming the rule and the spelling that
package/docs/QUERY-PEN.md CHANGED
@@ -200,8 +200,9 @@ value becomes the expression:
200
200
  `groupJoin`, `aggregate` (and a `mapAsync`) a relation name is an
201
201
  ordinary member again — and a `fromDocument` chain never hops: there
202
202
  the document decides what the items are. A hop that cannot lower is
203
- `JL0105` at build time (a many-to-many member: its join table is not a
204
- queryable root in this version; a composite key); a member read off
203
+ `JL0105` at build time (a composite key; a many-to-many entry that
204
+ does not name its join row's columns, so there is no join root to
205
+ lower through — MODEL-FORMAT §10.7); a member read off
205
206
  the to-many ARRAY before `all()` is `JL0005` with the fix named, where
206
207
  the same read off a stored array would answer nothing.
207
208
 
@@ -563,6 +564,8 @@ Runtime errors (`LinqRuntimeError`):
563
564
  | `JL2004` | an asynchronous provider cannot back the synchronous surface |
564
565
  | `JL2005` | a push queue was fed after it ended |
565
566
  | `JL2006` | a provider answered an element terminal with something other than one array |
567
+ | `JL2007` | a ledger settlement named a ref that settles no started record — `createDbLedger`'s fence ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)) |
568
+ | `JL2008` | a federated fetch reached its row or byte budget (§12.1) |
566
569
 
567
570
  Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
568
571
  through unwrapped — they already carry their own code and `docPath` —
@@ -604,12 +607,19 @@ because the engine itself materialises for `$orderby`/`$groupby`):
604
607
  `take(n)`, and an exception mid-chain all call `.return()` on the
605
608
  iterator — a generator left suspended holds a file handle or a read
606
609
  transaction open. `explain()` reports `{ barriers: [{ operator,
607
- reason }], hops, document }` — `hops` the relation hops the callbacks
608
- navigated, as on the sync surface (§4) — or, when a `mapAsync` sits in
609
- the chain, `{ split: { pushed, residual } }` instead of `document`
610
- (`toDocument()` refuses with `JL0005`: a host callback has no document
611
- form). No silent caps, no silent buffering: if a chain materialises,
612
- the report says which operator forced it.
610
+ reason }], streaming, barrier, hops, document }` — `hops` the relation
611
+ hops the callbacks navigated, as on the sync surface (§4); `streaming`
612
+ (`'row'` or `'buffered'`) and `barrier` what THIS surface does with the
613
+ item stream, one item at a time or a buffer at its first local barrier
614
+ or, when a `mapAsync` sits in the chain, `{ split: { pushed,
615
+ residual } }` instead of `document` (`toDocument()` refuses with
616
+ `JL0005`: a host callback has no document form). Over a provider the
617
+ pushed document's own class — a set residual, an external the store
618
+ cannot bind — is the provider's `explain(document, { externals:
619
+ bindings })` to report, and the cursor a `for await` pulls from
620
+ carries the same `streaming`/`barrier` (§12). No silent caps, no silent
621
+ buffering: if a chain materialises, the report says which operator
622
+ forced it.
613
623
 
614
624
  Re-enumeration follows the sync contract: each enumeration calls the
615
625
  source's iterator method again. A one-shot generator object simply
@@ -645,8 +655,11 @@ operators; a per-element async *predicate* is `mapAsync` then `where`.
645
655
  completion.
646
656
  - An `AbortSignal` is threaded to every callback and aborted on early
647
657
  termination and on failure. A rejected callback FAILS CLOSED: the
648
- first failure wins, every in-flight sibling aborts, the source
649
- closes (the `compileDag` discipline).
658
+ first rejection aborts every in-flight sibling and closes the source,
659
+ including while its next pull is pending. Ordered mode still delivers
660
+ preceding values and reports callback failures in source order. The
661
+ producer is closed once; a failed close accompanies the callback's
662
+ original failure in an `AggregateError`.
650
663
  - `mapAsync` is NOT translatable to a provider. A provider-backed
651
664
  chain that reaches it SPLITS: everything before is pushed to the
652
665
  provider whole, everything after runs locally, and `explain()`
@@ -674,8 +687,12 @@ operators; a per-element async *predicate* is `mapAsync` then `where`.
674
687
  `iterateCsvStream` output);
675
688
  - any sync iterable (wrapped);
676
689
  - a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
677
- shape the SQL provider's row iterator implements later, adopted
678
- as-is;
690
+ shape the store's own row cursor implements (`QueryCursor`), adopted
691
+ as-is. A provider that offers `cursor(document, options)` — the
692
+ store's collections and entity sets do — is handed the pushed
693
+ document there when the chain is ITERATED, so a `for await` pulls
694
+ one row at a time from an open statement and a `break` releases it;
695
+ `toArray()` and the other terminals still push one whole window;
679
696
  - a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
680
697
  feed/end-style readers with no pull protocol of their own (josl's
681
698
  push parsers deliberately have no backpressure protocol; the queue
@@ -689,6 +706,72 @@ engine async (`packages/json` is untouched and strictly synchronous),
689
706
  it does not add a second operator table, and it does not add
690
707
  `selectAwait`/`whereAwait` variants.
691
708
 
709
+ ### 12.1 The federation boundary (`federate`)
710
+
711
+ A query document reads ONE input, and a join whose sides come from two
712
+ unrelated sources is `JL0005` (§8). That refusal stands: nothing in a
713
+ chain relaxes it. What `federate()` adds is the one place to opt OUT of
714
+ it explicitly, by naming the sources and the bounds together:
715
+
716
+ ```js
717
+ const fed = federate({
718
+ sources: { orders: shop.entity('Order'), events: analytics },
719
+ maxRows: 50_000,
720
+ maxBytes: 32 * 1024 * 1024,
721
+ });
722
+
723
+ const rows = await fromAsync(fed.source('orders'))
724
+ .where((o) => o.placedAt.gt(cutoff))
725
+ .join(fromAsync(fed.source('events')), (o) => o.id, (e) => e.orderId,
726
+ (o, e) => ({ id: o.id, at: e.at }))
727
+ .toArray();
728
+ ```
729
+
730
+ `fed.source(name)` is an ordinary provider source (§8) with the root
731
+ `$.<name>[*]`, and all of a federation's sources share one scope — which
732
+ is exactly what admits the join. The federation is what executes it:
733
+
734
+ 1. each side's own document — the filters and the projection the chain
735
+ already packed per side — runs against ITS source, over that
736
+ source's own root;
737
+ 2. the smaller side (by declared `estimatedRows`, else the first named)
738
+ is streamed into a hash table keyed by the join key, counting rows
739
+ and serialized bytes against the budget as it fills;
740
+ 3. the other side is streamed and PROBED: a row whose key no build row
741
+ carries cannot pair, so it is dropped before it costs anything;
742
+ 4. the caller's own document runs in the engine over the two reduced
743
+ sets — the **resident join**, which is what decides.
744
+
745
+ Step 4 is the contract. This boundary spells no join semantics of its
746
+ own: the engine's `$eq` decides which rows pair, its ordering orders
747
+ them, its projection shapes them. The hash table bounds the FETCH and
748
+ nothing else, so a reduction that cannot key a value (a compound join
749
+ key) keeps the row rather than guessing at it.
750
+
751
+ `fed.source(name).explain(document)` answers the plan without running
752
+ any of it: the `strategy`, the `budget`, the `build` and `probe` sides
753
+ with their estimates, their own documents and whether each streams, and
754
+ the `resident` document the engine answers.
755
+
756
+ **A budget is a refusal, not a spill.** A side that reaches `maxRows` or
757
+ `maxBytes` stops at the row that WOULD have broken it and raises
758
+ `JL2008`; every cursor the call opened is closed exactly once, whether
759
+ it answered, refused, failed or was aborted. A `signal` on the call is
760
+ read at the ROW boundary — where a cursor can be let go without
761
+ abandoning a pull the source is still inside — and a declared
762
+ `estimatedRows` decides only which side BUILDS, never how much is held:
763
+ a source that under-reports is refused by the budget all the same. A source offering a
764
+ cursor (§12) is pulled row by row, so the bound is enforced before the
765
+ memory is spent; one offering only `execute` answers whole, and
766
+ `explain()` says `buffered` rather than pretending otherwise.
767
+
768
+ Refused, by name: more than two sides in one federated document, a
769
+ binding over a root the federation does not carry, and a join with no
770
+ equality between one member of each side — without one the fetch is the
771
+ cross product of two sources, which is what the budget exists to
772
+ refuse. Non-goals: no spill, no distributed transaction, no
773
+ cross-source write.
774
+
692
775
  ## 13. Worked examples
693
776
 
694
777
  Every `js` fence below is EXECUTED. `test/linq/pen-docs.test.js` writes
@@ -1111,6 +1194,11 @@ proves that too, so the exclusion cannot hide a chain refusal).
1111
1194
  | `JL2004` | a provider's `execute()` answered a promise on the synchronous surface |
1112
1195
  | `JL2005` | a push queue was fed after `end()` |
1113
1196
  | `JL2006` | a provider answered an element terminal with something other than one array |
1197
+ | `JL2008` | a federated fetch reached one side's row or byte budget (§12.1) |
1198
+
1199
+ `JL2007` is the client door's, not the chain's: `createDbLedger`'s stale
1200
+ settlement ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)); it is
1201
+ listed with the runtime errors above and raised by no chain module.
1114
1202
 
1115
1203
  Every message below is the one the chain raised when the spelling beside
1116
1204
  it was run, with the code prefix (`JL0005: `) removed. Where a refusal
@@ -1583,14 +1671,14 @@ are shorter:
1583
1671
  ## 17. Cost
1584
1672
 
1585
1673
  A consumer importing `from` from `@jarenjs/linq` and calling one
1586
- terminal bundles **<!--fact:bundle.chain-->173,080<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
1674
+ terminal bundles **<!--fact:bundle.chain-->174,264<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
1587
1675
  `platform: 'neutral'`). The figure is measured by
1588
1676
  `scripts/check-tree-shaking.js`'s chain probe and compared with this
1589
1677
  section on every `npm run test:tree-shaking`: it is derived, never typed,
1590
1678
  and a stale one is red here rather than wrong in a document somebody
1591
1679
  reads.
1592
1680
 
1593
- Of that, **<!--fact:bundle.chain.own-->39,025<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
1681
+ Of that, **<!--fact:bundle.chain.own-->40,280<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
1594
1682
  `async.js`, `expression.js`, `document.js`, `provider.js`,
1595
1683
  `concurrency.js`, `errors.js` and `schema-of.js`. The remaining ~134 kB
1596
1684
  is the query ENGINE and the core it stands on: a chain's document has to
@@ -1611,13 +1699,13 @@ making:
1611
1699
  `@jarenjs/formats`** — the client's optional peers. A consumer of the
1612
1700
  chain alone installs nothing new;
1613
1701
  - **no pen bytes at all**, in either direction: the pens carry no chain
1614
- module either, which is what keeps a <!--fact:bundle.jslt.kb-->19<!--/fact--> kB JSLT
1615
- pen <!--fact:bundle.jslt.kb-->19<!--/fact--> kB.
1702
+ module either, which is what keeps a <!--fact:bundle.jslt.kb-->20<!--/fact--> kB JSLT
1703
+ pen <!--fact:bundle.jslt.kb-->20<!--/fact--> kB.
1616
1704
 
1617
1705
  `docs/CONSUMING.md` states the rounded price of all ten subpaths in one
1618
1706
  table, each figure held equal to the same measurements. Two of its rows
1619
- are the ones to read together: the chain at <!--fact:bundle.chain.kb-->173<!--/fact--> kB and
1620
- `./db` at <!--fact:bundle.db.kb-->478<!--/fact--> kB.
1707
+ are the ones to read together: the chain at <!--fact:bundle.chain.kb-->174<!--/fact--> kB and
1708
+ `./db` at <!--fact:bundle.db.kb-->624<!--/fact--> kB.
1621
1709
  The client costs what the store costs, by construction, and the chain
1622
1710
  costs what running a query costs.
1623
1711
 
@@ -1626,7 +1714,7 @@ the reason is worth knowing: a bundler counts a shared module once, and
1626
1714
  the chain and every pen share the expression capture (`expression.js`)
1627
1715
  and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
1628
1716
  and object helpers). A consumer importing the chain AND the schema pen
1629
- bundles **<!--fact:bundle.chain.withSchemaPen-->194,155<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->11,352<!--/fact--> bytes** less than the sum of the
1717
+ bundles **<!--fact:bundle.chain.withSchemaPen-->195,342<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->12,078<!--/fact--> bytes** less than the sum of the
1630
1718
  figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
1631
1719
  what those shared modules weigh. The probe measures that pair too, so
1632
1720
  the saving is derived like everything else here. What the chain does NOT
@@ -1180,10 +1180,10 @@ hand, or generate it some other way, when:
1180
1180
 
1181
1181
  ## 7. Cost
1182
1182
 
1183
- `@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->32,427<!--/fact--> bytes** as a minified,
1183
+ `@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->33,156<!--/fact--> bytes** as a minified,
1184
1184
  tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
1185
1185
  measures and `npm run test:tree-shaking` reports, published rounded
1186
- (<!--fact:bundle.schema.kb-->32<!--/fact--> kB) beside the other nine subpath prices in
1186
+ (<!--fact:bundle.schema.kb-->33<!--/fact--> kB) beside the other nine subpath prices in
1187
1187
  [docs/CONSUMING.md](../../../docs/CONSUMING.md).
1188
1188
 
1189
1189
  The probe is a gate, not a report: building
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/linq",
3
3
  "private": false,
4
- "version": "0.56.0",
4
+ "version": "0.67.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./types/index.d.ts",
@@ -84,13 +84,13 @@
84
84
  "prepack": "npm run build:types"
85
85
  },
86
86
  "dependencies": {
87
- "@jarenjs/core": "^0.56.0",
88
- "@jarenjs/json": "^0.56.0"
87
+ "@jarenjs/core": "^0.67.0",
88
+ "@jarenjs/json": "^0.67.0"
89
89
  },
90
90
  "peerDependencies": {
91
- "@jarenjs/db": "^0.56.0",
92
- "@jarenjs/formats": "^0.56.0",
93
- "@jarenjs/validate": "^0.56.0"
91
+ "@jarenjs/db": "^0.67.0",
92
+ "@jarenjs/formats": "^0.67.0",
93
+ "@jarenjs/validate": "^0.67.0"
94
94
  },
95
95
  "peerDependenciesMeta": {
96
96
  "@jarenjs/db": {
package/src/app/action.js CHANGED
@@ -19,6 +19,7 @@
19
19
  * payload-creator function.
20
20
  */
21
21
 
22
+ import { isJsonObject } from '@jarenjs/core/object';
22
23
  import { LinqBuildError } from '../errors.js';
23
24
  import { effectDescriptor, readEffects } from '../effect.js';
24
25
  import { describeValue } from '../json-boundary.js';
@@ -52,11 +53,6 @@ const EXCLUDED_FIELDS = new Set([
52
53
  'path', 'composedPath', 'clipboardData', 'submitter',
53
54
  ]);
54
55
 
55
- /** @param {any} value */
56
- function isPlainObject(value) {
57
- return value !== null && typeof value === 'object' && !Array.isArray(value);
58
- }
59
-
60
56
  /**
61
57
  * A member set the pen knows, or `JL0101` naming the one it does not.
62
58
  * @param {any} spec
@@ -120,7 +116,7 @@ export function effect(run, props = undefined) {
120
116
  * transition({ state: () => null, effects: [effect('save')] });
121
117
  */
122
118
  export function transition(spec) {
123
- if (!isPlainObject(spec)) {
119
+ if (!isJsonObject(spec)) {
124
120
  throw new LinqBuildError('JL0101',
125
121
  `transition() takes { state?, patch?, effects? }, got ${describeValue(spec)}`);
126
122
  }
@@ -157,7 +153,7 @@ export function action(fn, options = undefined) {
157
153
  `action() takes a callback (s, x) => transition(…), got ${describeValue(fn)}`);
158
154
  }
159
155
  if (options !== undefined) {
160
- if (!isPlainObject(options)) {
156
+ if (!isJsonObject(options)) {
161
157
  throw new LinqBuildError('JL0101',
162
158
  `action() options are { payload?, event? }, got ${describeValue(options)}`);
163
159
  }
@@ -232,7 +228,7 @@ export function bind(name, options = undefined) {
232
228
  }
233
229
  const out = { action: name };
234
230
  if (options !== undefined) {
235
- if (!isPlainObject(options)) {
231
+ if (!isJsonObject(options)) {
236
232
  throw new LinqBuildError('JL0101',
237
233
  'bind() options are { payload?, event?, preventDefault?, stopPropagation? }, got '
238
234
  + describeValue(options));
package/src/app/define.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * click; the pen can see the whole document at once and refuses it.
25
25
  */
26
26
 
27
- import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
27
+ import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
28
28
 
29
29
  import { LinqBuildError } from '../errors.js';
30
30
  import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
@@ -37,11 +37,6 @@ const APP_VERSION = '0.1';
37
37
  /** The members `defineApp()` takes. */
38
38
  const APP_MEMBERS = Object.freeze(['state', 'initial', 'schema', 'view', 'actions', 'subs']);
39
39
 
40
- /** @param {any} value */
41
- function isPlainObject(value) {
42
- return value !== null && typeof value === 'object' && !Array.isArray(value);
43
- }
44
-
45
40
  /** A JSON value, copied: the document is a value of its own. @param {any} v */
46
41
  const copy = (v) => JSON.parse(JSON.stringify(v));
47
42
 
@@ -65,7 +60,7 @@ function initialOf(schema, at, needed) {
65
60
  }
66
61
  if (schema.default !== undefined) return { has: true, value: copy(schema.default) };
67
62
  if (schema.const !== undefined) return { has: true, value: copy(schema.const) };
68
- if (!isPlainObject(schema.properties)) return { has: false };
63
+ if (!isJsonObject(schema.properties)) return { has: false };
69
64
 
70
65
  const required = Array.isArray(schema.required) ? schema.required : [];
71
66
  const out = {};
@@ -114,7 +109,7 @@ function collectBoundActions(node, out) {
114
109
  const name = literalName(value);
115
110
  if (name !== null) out.add(name);
116
111
  }
117
- if (key === 'on' && isPlainObject(value)) {
112
+ if (key === 'on' && isJsonObject(value)) {
118
113
  for (const binding of Object.values(value)) {
119
114
  const name = literalName(binding);
120
115
  if (name !== null) out.add(name);
@@ -137,7 +132,7 @@ function readView(view) {
137
132
  + 'bare rule array; the format requires the member and the runtime refuses an app '
138
133
  + 'without one (JA0002)', '/view');
139
134
  }
140
- if (!Array.isArray(view) && !isPlainObject(view)) {
135
+ if (!Array.isArray(view) && !isJsonObject(view)) {
141
136
  throw new LinqBuildError('JL0101',
142
137
  `defineApp() view is a JSLT stylesheet document or a bare rule array, got ${describeValue(view)}`,
143
138
  '/view');
@@ -165,7 +160,7 @@ function readView(view) {
165
160
  * createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) });
166
161
  */
167
162
  export function defineApp(spec) {
168
- if (!isPlainObject(spec)) {
163
+ if (!isJsonObject(spec)) {
169
164
  throw new LinqBuildError('JL0101',
170
165
  `defineApp() takes { state, initial?, schema?, view, actions?, subs? }, got ${describeValue(spec)}`);
171
166
  }
@@ -199,7 +194,7 @@ export function defineApp(spec) {
199
194
  /** @type {Record<string, any> | undefined} */
200
195
  let actions;
201
196
  if (spec.actions !== undefined) {
202
- if (!isPlainObject(spec.actions)) {
197
+ if (!isJsonObject(spec.actions)) {
203
198
  throw new LinqBuildError('JL0101',
204
199
  `defineApp() actions is an object of named action() declarations, got ${describeValue(spec.actions)}`,
205
200
  '/actions');
@@ -207,7 +202,7 @@ export function defineApp(spec) {
207
202
  requireNameMap(spec.actions, 'defineApp() actions', '/actions');
208
203
  actions = {};
209
204
  for (const [name, declared] of Object.entries(spec.actions)) {
210
- if (!isPlainObject(declared) || declared[ACTION] !== true) {
205
+ if (!isJsonObject(declared) || declared[ACTION] !== true) {
211
206
  throw new LinqBuildError('JL0101',
212
207
  `defineApp() action '${name}' is action((s, x) => transition(…)), got ${describeValue(declared)}`,
213
208
  `/actions/${name}`);
@@ -225,7 +220,7 @@ export function defineApp(spec) {
225
220
  '/subs');
226
221
  }
227
222
  subs = spec.subs.map((declared, i) => {
228
- if (!isPlainObject(declared) || declared[SUB] !== true) {
223
+ if (!isJsonObject(declared) || declared[SUB] !== true) {
229
224
  throw new LinqBuildError('JL0101',
230
225
  `defineApp() subs[${i}] is sub(run, options?), got ${describeValue(declared)}`,
231
226
  `/subs/${i}`);