@jarenjs/linq 0.75.0 → 0.83.2
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 +8 -1
- package/docs/AI-PEN.md +1 -1
- package/docs/APP-PEN.md +3 -3
- package/docs/CHARTS-PEN.md +1 -1
- package/docs/CONTRACT-PEN.md +4 -4
- package/docs/DB-CLIENT.md +200 -8
- package/docs/FLOW-PEN.md +3 -3
- package/docs/FORMS-PEN.md +2 -2
- package/docs/FORMULA-PEN.md +41 -0
- package/docs/JSLT-PEN.md +4 -4
- package/docs/JTLT-PEN.md +1 -1
- package/docs/LINQ-FORMAT.md +60 -43
- package/docs/MESSAGES-PEN.md +1 -1
- package/docs/MIGRATION-PEN.md +3 -3
- package/docs/MODEL-PEN.md +27 -3
- package/docs/PROJECT-PEN.md +10 -4
- package/docs/QUERY-PEN.md +25 -8
- package/docs/SCHEMA-PEN.md +2 -2
- package/package.json +10 -6
- package/src/db/effects.js +148 -0
- package/src/db/handle.js +2 -0
- package/src/db/index.js +6 -0
- package/src/db/ingest.js +153 -0
- package/src/db/open.js +3 -0
- package/src/db/range.js +261 -0
- package/src/db/receipts.js +151 -0
- package/src/db/records.js +61 -0
- package/src/db/runs.js +164 -0
- package/src/db/search.js +86 -0
- package/src/errors.js +3 -1
- package/src/expression.js +5 -2
- package/src/formula/index.js +15 -0
- package/src/model/define.js +2 -0
- package/src/model/entity.js +15 -0
- package/src/project/index.js +6 -5
- package/types/db.d.ts +101 -0
- package/types/formula.d.ts +9 -0
- package/types/index.d.ts +4 -0
- package/types/model.d.ts +18 -0
- package/types/project.d.ts +9 -3
package/docs/LINQ-FORMAT.md
CHANGED
|
@@ -16,9 +16,9 @@ A **pen** is a by-code front-end to one of the suite's document formats:
|
|
|
16
16
|
named functions that build a standard document — a JSON Schema, a
|
|
17
17
|
`$model`, a `$jslt` stylesheet — the way the chain builds a query
|
|
18
18
|
document. `@jarenjs/linq` exports each pen under its own subpath
|
|
19
|
-
(<!--fact:coverage.subpaths-->`./ai`, `./app`, `./charts`, `./contract`, `./db`, `./flow`, `./forms`, `./jslt`, `./jtlt`, `./messages`, `./migration`, `./model`, `./project`, `./schema`<!--/fact-->); `.` stays the chain.
|
|
19
|
+
(<!--fact:coverage.subpaths-->`./ai`, `./app`, `./charts`, `./contract`, `./db`, `./flow`, `./forms`, `./formula`, `./jslt`, `./jtlt`, `./messages`, `./migration`, `./model`, `./project`, `./schema`<!--/fact-->); `.` stays the chain.
|
|
20
20
|
|
|
21
|
-
Coverage: <!--fact:coverage.pens-->
|
|
21
|
+
Coverage: <!--fact:coverage.pens-->15 public pen/client subpaths beside the chain; 69/69 owned schema keywords have dedicated emission routes.<!--/fact-->
|
|
22
22
|
|
|
23
23
|
**This document is the family's normative reference**: §1 states the
|
|
24
24
|
rules every pen keeps, §1.3 the error codes they share, and §4–§7 the
|
|
@@ -36,10 +36,10 @@ is the index of those guides, and it is how a reader reaches any of them.
|
|
|
36
36
|
<!--fact:pens.index-->
|
|
37
37
|
| Document | Lines | What it writes, and when to open it |
|
|
38
38
|
|---|---:|---|
|
|
39
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) |
|
|
40
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | 1,
|
|
39
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | 965 | 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 guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the documents, or one place to look up a method without knowing which pen owns it |
|
|
40
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | 1,752 | 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 |
|
|
41
41
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,205 | `./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 |
|
|
42
|
-
| [MODEL-PEN.md](MODEL-PEN.md) | 1,
|
|
42
|
+
| [MODEL-PEN.md](MODEL-PEN.md) | 1,116 | `./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 |
|
|
43
43
|
| [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 |
|
|
44
44
|
| [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 |
|
|
45
45
|
| [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 |
|
|
@@ -49,9 +49,10 @@ is the index of those guides, and it is how a reader reaches any of them.
|
|
|
49
49
|
| [AI-PEN.md](AI-PEN.md) | 98 | `./ai` — the public action program over environment slots. **Read it when** you want typed fixtures or host-authored programs without a model client. |
|
|
50
50
|
| [MESSAGES-PEN.md](MESSAGES-PEN.md) | 105 | `./messages` — JSON message catalogs and message references. **Read it when** you want checked translation keys, placeholders and explicit completeness. |
|
|
51
51
|
| [JTLT-PEN.md](JTLT-PEN.md) | 83 | `./jtlt` — text templates with JSLT dispatch and query expressions. **Read it when** you want to author Markdown, XML or source text as portable JSON. |
|
|
52
|
-
| [PROJECT-PEN.md](PROJECT-PEN.md) |
|
|
52
|
+
| [PROJECT-PEN.md](PROJECT-PEN.md) | 81 | `./project` — Studio projects with named, typed files. **Read it when** you want a portable editor workspace containing documents written by several pens. |
|
|
53
53
|
| [CHARTS-PEN.md](CHARTS-PEN.md) | 94 | `./charts` — chart-definition documents for every chart kind. **Read it when** you want typed chart data and presentation options that `compileChart` consumes. |
|
|
54
|
-
| [DB-CLIENT.md](DB-CLIENT.md) |
|
|
54
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | 1,085 | `./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` |
|
|
55
|
+
| [FORMULA-PEN.md](FORMULA-PEN.md) | 41 | `./formula` — author versioned saved JSON Query profiles without executing them. |
|
|
55
56
|
<!--/fact-->
|
|
56
57
|
|
|
57
58
|
Every row of that table is derived, and none of it is written here: the
|
|
@@ -204,23 +205,24 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
204
205
|
<!--fact:pens.census-->
|
|
205
206
|
| Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
|
|
206
207
|
|---|---|---:|---:|---:|---:|---:|
|
|
207
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — |
|
|
208
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,
|
|
209
|
-
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 |
|
|
210
|
-
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,
|
|
211
|
-
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 |
|
|
212
|
-
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 |
|
|
213
|
-
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 |
|
|
214
|
-
| [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 |
|
|
215
|
-
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 |
|
|
216
|
-
| [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 |
|
|
217
|
-
| [AI-PEN.md](AI-PEN.md) | `./ai` | 98 | 12 | 1 | 3 |
|
|
218
|
-
| [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 |
|
|
219
|
-
| [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 |
|
|
220
|
-
| [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` |
|
|
221
|
-
| [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 |
|
|
222
|
-
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` |
|
|
223
|
-
|
|
|
208
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 965 | — | — | — | — |
|
|
209
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,752 | 34 | 8 | 15 | 175,221 B |
|
|
210
|
+
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,205 | 82 | 10 | 4 | 35,285 B |
|
|
211
|
+
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,116 | 30 | 6 | 3 | 44,143 B |
|
|
212
|
+
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 18,424 B |
|
|
213
|
+
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 22,827 B |
|
|
214
|
+
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 47,427 B |
|
|
215
|
+
| [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 18,478 B |
|
|
216
|
+
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 49,574 B |
|
|
217
|
+
| [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 39,441 B |
|
|
218
|
+
| [AI-PEN.md](AI-PEN.md) | `./ai` | 98 | 12 | 1 | 3 | 15,472 B |
|
|
219
|
+
| [MESSAGES-PEN.md](MESSAGES-PEN.md) | `./messages` | 105 | 9 | 2 | 1 | 16,931 B |
|
|
220
|
+
| [JTLT-PEN.md](JTLT-PEN.md) | `./jtlt` | 83 | 13 | 1 | 2 | 15,293 B |
|
|
221
|
+
| [PROJECT-PEN.md](PROJECT-PEN.md) | `./project` | 81 | 9 | 1 | 1 | 13,793 B |
|
|
222
|
+
| [CHARTS-PEN.md](CHARTS-PEN.md) | `./charts` | 94 | 21 | 1 | 1 | 15,582 B |
|
|
223
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 1,085 | 46 | 4 | 2 | 675,752 B |
|
|
224
|
+
| [FORMULA-PEN.md](FORMULA-PEN.md) | `./formula` | 41 | 2 | — | — | 14,850 B |
|
|
225
|
+
| **17 documents** | | **12,698** | **380** | **73** | | |
|
|
224
226
|
<!--/fact-->
|
|
225
227
|
|
|
226
228
|
A pen whose mapping rows are far below its worked examples is a pen
|
|
@@ -262,21 +264,22 @@ it and each document publishes it. The rounded column is what
|
|
|
262
264
|
<!--fact:pens.cost-->
|
|
263
265
|
| Subpath | Document | Bundle | Rounded |
|
|
264
266
|
|---|---|---:|---:|
|
|
265
|
-
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) |
|
|
266
|
-
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) |
|
|
267
|
-
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) |
|
|
268
|
-
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) |
|
|
269
|
-
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) |
|
|
270
|
-
| `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) |
|
|
271
|
-
| `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) |
|
|
272
|
-
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) |
|
|
273
|
-
| `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) |
|
|
274
|
-
| `@jarenjs/linq/ai` | [AI-PEN.md](AI-PEN.md) |
|
|
275
|
-
| `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) |
|
|
276
|
-
| `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) |
|
|
277
|
-
| `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) |
|
|
278
|
-
| `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) |
|
|
279
|
-
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) |
|
|
267
|
+
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 175,221 B | 175 kB |
|
|
268
|
+
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 35,285 B | 35 kB |
|
|
269
|
+
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 44,143 B | 44 kB |
|
|
270
|
+
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 18,424 B | 18 kB |
|
|
271
|
+
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 22,827 B | 23 kB |
|
|
272
|
+
| `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 47,427 B | 47 kB |
|
|
273
|
+
| `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 18,478 B | 18 kB |
|
|
274
|
+
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 49,574 B | 50 kB |
|
|
275
|
+
| `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 39,441 B | 39 kB |
|
|
276
|
+
| `@jarenjs/linq/ai` | [AI-PEN.md](AI-PEN.md) | 15,472 B | 15 kB |
|
|
277
|
+
| `@jarenjs/linq/messages` | [MESSAGES-PEN.md](MESSAGES-PEN.md) | 16,931 B | 17 kB |
|
|
278
|
+
| `@jarenjs/linq/jtlt` | [JTLT-PEN.md](JTLT-PEN.md) | 15,293 B | 15 kB |
|
|
279
|
+
| `@jarenjs/linq/project` | [PROJECT-PEN.md](PROJECT-PEN.md) | 13,793 B | 14 kB |
|
|
280
|
+
| `@jarenjs/linq/charts` | [CHARTS-PEN.md](CHARTS-PEN.md) | 15,582 B | 16 kB |
|
|
281
|
+
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 675,752 B | 676 kB |
|
|
282
|
+
| `@jarenjs/linq/formula` | [FORMULA-PEN.md](FORMULA-PEN.md) | 14,850 B | 15 kB |
|
|
280
283
|
<!--/fact-->
|
|
281
284
|
|
|
282
285
|
Read these as prices, not as scores. `./db` is the largest by an order of
|
|
@@ -524,6 +527,8 @@ it says.
|
|
|
524
527
|
| `.updated()` | `default: 'updated'` — a stamp on insert AND on every update | marks it `generated` | native |
|
|
525
528
|
| `.fill(value)` | `default: { value }` — a literal, filled when absent; the value crosses the JSON boundary (`requireJson`) | marks it `generated` | native; a value that is not JSON is `JL0101` |
|
|
526
529
|
| `.compute(fn)`, `.compute(query)` | `default: { query }` — captured over the document being written (`$`), or a query document verbatim | marks it `generated` | native; a captured rule that binds ANY external is `JL0104` |
|
|
530
|
+
| `.physical(layout)` | `physical` on the entity declaration | preserves entity types; explicit column codecs | native on an object builder |
|
|
531
|
+
| `.invariants(rules)` | `invariants` on the entity declaration | explicit writer qualification | native on an object builder |
|
|
527
532
|
| `.renamedFrom(name)` | `x-rename: name` on the ENTITY (or collection) declaration — a planning hint the migration planner reads, never part of the shape (MIGRATION-FORMAT §3) | — | native on the declaration's own builder; on a member, `JL0102` (the document has no place for one) |
|
|
528
533
|
| `.meta(annotations)` | as the schema pen ([SCHEMA-PEN.md](SCHEMA-PEN.md#28-annotations-and-messages)), minus one key | — | refused (`JL0104`) for `x-entity`: the pen owns that keyword |
|
|
529
534
|
| `.entity(patch)` | the patch, merged into `x-entity` — the primitive every row above is written in terms of, and the way to spell a member of the vocabulary that has no method of its own | — (it sets no flag; the named methods do — §5.2) | native; a member outside the closed vocabulary, `JL0102` |
|
|
@@ -844,8 +849,8 @@ it says.
|
|
|
844
849
|
|
|
845
850
|
| Method | Emits | Type | Status |
|
|
846
851
|
|---|---|---|---|
|
|
847
|
-
| `file(name, kind, text)` | `{ name, kind, text }
|
|
848
|
-
| `jsonFile(name, kind, document)` | same file, with serialized JSON text | literal name/kind | native |
|
|
852
|
+
| `file(name, kind, text, options?)` | `{ name, kind, text }` and optional routing/import members, preserving text | literal name/kind | native |
|
|
853
|
+
| `jsonFile(name, kind, document, options?)` | same file, with serialized JSON text | literal name/kind | native |
|
|
849
854
|
| `defineProject(files?, options?)` | version, files, optional active/layout | names from files | native |
|
|
850
855
|
| `.files(files)` | replacement file list | replaces known names | native |
|
|
851
856
|
| `.file(file)` | appended file | adds its name | native |
|
|
@@ -903,17 +908,22 @@ it says.
|
|
|
903
908
|
| `defineReplication(header)` | a logical replication document builder — §2.7 | `ReplicationPen` |
|
|
904
909
|
| `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
|
|
905
910
|
| `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` |
|
|
911
|
+
| `createDbReceipts(client, options)` | permanent receipts and independent fenced leases; see durable mapped records | structural receipt repository |
|
|
912
|
+
| `createDbEffectStore(client, options)` | reviewed external intent and reconciliation under job fences | structural effect store |
|
|
913
|
+
| `createDbRunStore(client, options)` | mapped workflow checkpoints and bounded revision event pages | structural run store |
|
|
914
|
+
| `createDbIngestionStore(client, options)` | atomic page/checkpoint staging and complete snapshot publication; see [complete ingestion store](DB-CLIENT.md#complete-ingestion-store) | structural ingestion store |
|
|
915
|
+
| `createDbRangeProvider(store, entity, spec, options)` | a bounded structural range source over keyset pages and committed capture; also `handle.range(spec, options)` — [COLLECTION-PROVIDER.md](../../app/docs/COLLECTION-PROVIDER.md) | `Promise<DbRangeProvider>` |
|
|
906
916
|
|
|
907
917
|
**The entity handle**
|
|
908
918
|
|
|
909
919
|
| Group | Members |
|
|
910
920
|
|---|---|
|
|
911
|
-
| the unit of work | `create` `get` `update` `delete` `add` `put` `remove` `discard` `asNoTracking` |
|
|
921
|
+
| the unit of work | `create` `get` `update` `delete` `add` `put` `remove` `discard` `asNoTracking`; `mutate` executes an untracked [native column mutation](../../db/docs/NATIVE-PLANS.md) |
|
|
912
922
|
| the store's reads | `load` `explainLoad` `execute` |
|
|
913
923
|
| the provider seam | `root` `scope` `relations` |
|
|
914
924
|
| membership | `link` `unlink` — the store's, behind §4.2's check |
|
|
915
925
|
| the chain | every `AsyncSequence` operator and terminal: `where` `select` `selectMany` `orderBy` `orderByDescending` `thenBy` `thenByDescending` `groupBy` `aggregate` `join` `groupJoin` `skip` `take` `distinct` `reverse` `concat` `defaultIfEmpty` `ofType` `cast` `zip` `mapAsync` `params` `toDocument` `toArray` `first` `firstOrDefault` `single` `singleOrDefault` `last` `lastOrDefault` `elementAt` `elementAtOrDefault` `count` `sum` `average` `min` `max` `any` `all`, and `Symbol.asyncIterator` |
|
|
916
|
-
| the client's own | `include` (§2.4) and `
|
|
926
|
+
| the client's own | `include` (§2.4), `live`, and `range(spec, options)` |
|
|
917
927
|
| in both | `explain` |
|
|
918
928
|
|
|
919
929
|
**The graph**
|
|
@@ -945,4 +955,11 @@ it says.
|
|
|
945
955
|
| `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` |
|
|
946
956
|
| `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
|
|
947
957
|
| anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
|
|
958
|
+
|
|
959
|
+
### Formula document authoring — [FORMULA-PEN.md §2](FORMULA-PEN.md)
|
|
960
|
+
|
|
961
|
+
| Call / option | Emitted member |
|
|
962
|
+
|---|---|
|
|
963
|
+
| `defineFormula(id, expression)` | `$formula`, `id`, `expression`, default `revision` |
|
|
964
|
+
| `revision`, `bindings`, schema/helper references, result mode | Same named profile members |
|
|
948
965
|
<!--/fact-->
|
package/docs/MESSAGES-PEN.md
CHANGED
|
@@ -100,6 +100,6 @@ gate compares generated files and all locale parameter sets with their sources.
|
|
|
100
100
|
|
|
101
101
|
## 7. Cost
|
|
102
102
|
|
|
103
|
-
The isolated messages pen costs **<!--fact:bundle.messages-->
|
|
103
|
+
The isolated messages pen costs **<!--fact:bundle.messages-->16,931<!--/fact--> bytes**.
|
|
104
104
|
It carries the shared template compiler and generated vocabulary, with no locale,
|
|
105
105
|
validator, forms or contract engine and no query chain.
|
package/docs/MIGRATION-PEN.md
CHANGED
|
@@ -745,10 +745,10 @@ 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-->
|
|
748
|
+
`@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->22,827<!--/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
|
-
(<!--fact:bundle.migration.kb-->
|
|
751
|
+
(<!--fact:bundle.migration.kb-->23<!--/fact--> kB) beside the other nine subpath prices in
|
|
752
752
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
753
753
|
|
|
754
754
|
The probe is a gate, not a report: building a two-step migration as a
|
|
@@ -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-->
|
|
781
|
+
`./model`'s <!--fact:bundle.model-->44,143<!--/fact--> bytes for it.
|
package/docs/MODEL-PEN.md
CHANGED
|
@@ -135,6 +135,8 @@ leaves `key` where it was and replaces `default`.
|
|
|
135
135
|
| `.updated()` | `default: 'updated'` — a stamp on insert AND on every update | marks it `generated` | native |
|
|
136
136
|
| `.fill(value)` | `default: { value }` — a literal, filled when absent; the value crosses the JSON boundary (`requireJson`) | marks it `generated` | native; a value that is not JSON is `JL0101` |
|
|
137
137
|
| `.compute(fn)`, `.compute(query)` | `default: { query }` — captured over the document being written (`$`), or a query document verbatim | marks it `generated` | native; a captured rule that binds ANY external is `JL0104` |
|
|
138
|
+
| `.physical(layout)` | `physical` on the entity declaration | preserves entity types; explicit column codecs | native on an object builder |
|
|
139
|
+
| `.invariants(rules)` | `invariants` on the entity declaration | explicit writer qualification | native on an object builder |
|
|
138
140
|
| `.renamedFrom(name)` | `x-rename: name` on the ENTITY (or collection) declaration — a planning hint the migration planner reads, never part of the shape (MIGRATION-FORMAT §3) | — | native on the declaration's own builder; on a member, `JL0102` (the document has no place for one) |
|
|
139
141
|
| `.meta(annotations)` | as the schema pen ([SCHEMA-PEN.md](SCHEMA-PEN.md#28-annotations-and-messages)), minus one key | — | refused (`JL0104`) for `x-entity`: the pen owns that keyword |
|
|
140
142
|
| `.entity(patch)` | the patch, merged into `x-entity` — the primitive every row above is written in terms of, and the way to spell a member of the vocabulary that has no method of its own | — (it sets no flag; the named methods do — §5.2) | native; a member outside the closed vocabulary, `JL0102` |
|
|
@@ -1062,10 +1064,10 @@ catch them:
|
|
|
1062
1064
|
|
|
1063
1065
|
## 7. Cost
|
|
1064
1066
|
|
|
1065
|
-
`@jarenjs/linq/model` builds to **<!--fact:bundle.model-->
|
|
1067
|
+
`@jarenjs/linq/model` builds to **<!--fact:bundle.model-->44,143<!--/fact--> bytes** as a minified,
|
|
1066
1068
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
1067
1069
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
1068
|
-
(<!--fact:bundle.model.kb-->
|
|
1070
|
+
(<!--fact:bundle.model.kb-->44<!--/fact--> kB) beside the other nine subpath prices in
|
|
1069
1071
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
1070
1072
|
|
|
1071
1073
|
The probe is a gate, not a report: building a two-member model as a
|
|
@@ -1083,10 +1085,32 @@ them:
|
|
|
1083
1085
|
byte of `packages/linq/src/model/`, because the subclasses are built by
|
|
1084
1086
|
this subpath rather than patched onto the base classes.
|
|
1085
1087
|
|
|
1086
|
-
The price above the schema pen's <!--fact:bundle.schema-->
|
|
1088
|
+
The price above the schema pen's <!--fact:bundle.schema-->35,285<!--/fact--> is about 8 kB: the mixin, the
|
|
1087
1089
|
three relation factories, `collection()`/`index()` with their capture,
|
|
1088
1090
|
`defineModel()` — and the refusal MESSAGES, which are most of what §4
|
|
1089
1091
|
costs. That is a deliberate trade: naming the rule and the spelling that
|
|
1090
1092
|
works is why a mapping mistake is a `JL0102` at build rather than a
|
|
1091
1093
|
`JD0005` at `openStore`, so the ceiling is raised with the reason and the
|
|
1092
1094
|
text is not shaved.
|
|
1095
|
+
|
|
1096
|
+
## Existing column layouts and persistence rules
|
|
1097
|
+
|
|
1098
|
+
An entity object builder's `.physical({ table, kind?, keys?, columns })` writes
|
|
1099
|
+
an explicit column-only layout onto the entity declaration. Each column names
|
|
1100
|
+
its physical identifier, codec, SQL NULL policy, and optional database default
|
|
1101
|
+
or generated ownership. `.invariants(rules)` writes application-declared predicates
|
|
1102
|
+
with explicit database/store enforcement. The pen emits declarations; the database
|
|
1103
|
+
package owns validation, codecs, SQL lowering and preservation planning. See
|
|
1104
|
+
[MODEL-FORMAT](../../db/docs/MODEL-FORMAT.md#12-existing-column-layouts).
|
|
1105
|
+
|
|
1106
|
+
```js
|
|
1107
|
+
import * as m from '@jarenjs/linq/model';
|
|
1108
|
+
const model = m.defineModel({ entities: {
|
|
1109
|
+
Setting: m.object({ id: m.string().key(), value: m.string() }).physical({
|
|
1110
|
+
table: 'app_settings', columns: {
|
|
1111
|
+
id: { name: 'key', codec: 'text', null: 'reject' },
|
|
1112
|
+
value: { name: 'value', codec: 'text', null: 'reject' }
|
|
1113
|
+
}
|
|
1114
|
+
})
|
|
1115
|
+
} });
|
|
1116
|
+
```
|
package/docs/PROJECT-PEN.md
CHANGED
|
@@ -17,8 +17,8 @@ layout defaults. `validateFile()` checks each file through its own engine.
|
|
|
17
17
|
|
|
18
18
|
| Method | Emits | Type | Status |
|
|
19
19
|
|---|---|---|---|
|
|
20
|
-
| `file(name, kind, text)` | `{ name, kind, text }
|
|
21
|
-
| `jsonFile(name, kind, document)` | same file, with serialized JSON text | literal name/kind | native |
|
|
20
|
+
| `file(name, kind, text, options?)` | `{ name, kind, text }` and optional routing/import members, preserving text | literal name/kind | native |
|
|
21
|
+
| `jsonFile(name, kind, document, options?)` | same file, with serialized JSON text | literal name/kind | native |
|
|
22
22
|
| `defineProject(files?, options?)` | version, files, optional active/layout | names from files | native |
|
|
23
23
|
| `.files(files)` | replacement file list | replaces known names | native |
|
|
24
24
|
| `.file(file)` | appended file | adds its name | native |
|
|
@@ -42,6 +42,12 @@ Call `parseProject(project.schema)` from `@jarenjs/studio`. A parsed nonempty
|
|
|
42
42
|
project round-trips through JSON and the parser unchanged. Empty projects are
|
|
43
43
|
valid; the parser represents their absent active file as `null` internally.
|
|
44
44
|
|
|
45
|
+
File options preserve `imports`, `input`, `model` and `collection`. For example,
|
|
46
|
+
`jsonFile('app', 'app', { view: [] }, { imports: { state: 'seed' } })` supplies
|
|
47
|
+
state from another file; `file('q', 'query', '"$"', { model: 'store', collection:
|
|
48
|
+
'notes' })` routes a query to a store. The pen snapshots these values without
|
|
49
|
+
resolving them. Studio checks missing references, cycles and execution kinds.
|
|
50
|
+
|
|
45
51
|
## 4. Refusals
|
|
46
52
|
|
|
47
53
|
| Code | Condition |
|
|
@@ -57,7 +63,7 @@ pen preserves these behaviors and does not carry a second project validator.
|
|
|
57
63
|
|
|
58
64
|
`ProjectBuilder<Names>` tracks file names only in declarations. `FILE_KINDS`
|
|
59
65
|
contains the schema's file-kind vocabulary, checked against Studio's own set.
|
|
60
|
-
`ProjectFile<Name, Kind>`, `FileKind`, `ProjectLayout` and `ProjectDocument` are
|
|
66
|
+
`ProjectFile<Name, Kind>`, `ProjectFileOptions`, `FileKind`, `ProjectLayout` and `ProjectDocument` are
|
|
61
67
|
types. `JsonInput` accepts readonly structural documents and leaves unknown
|
|
62
68
|
schema extension values to the runtime JSON check. `.files()` replaces the name union and `.file()` widens it. `.active()`
|
|
63
69
|
refuses an undeclared literal name in TypeScript. `from()` deliberately keeps
|
|
@@ -71,5 +77,5 @@ app rendering and contract checks remain the file engines' responsibilities.
|
|
|
71
77
|
|
|
72
78
|
## 7. Cost
|
|
73
79
|
|
|
74
|
-
The isolated project pen costs **<!--fact:bundle.project-->
|
|
80
|
+
The isolated project pen costs **<!--fact:bundle.project-->13,793<!--/fact--> bytes**.
|
|
75
81
|
Its tree probe excludes Studio, other target engines and the query chain.
|
package/docs/QUERY-PEN.md
CHANGED
|
@@ -566,6 +566,7 @@ Runtime errors (`LinqRuntimeError`):
|
|
|
566
566
|
| `JL2005` | a push queue was fed after it ended |
|
|
567
567
|
| `JL2006` | a provider answered an element terminal with something other than one array |
|
|
568
568
|
| `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)) |
|
|
569
|
+
| `JL2009` | a durable mapped record violated identity, retention or fencing |
|
|
569
570
|
| `JL2008` | a federated fetch reached its row or byte budget (§12.1) |
|
|
570
571
|
|
|
571
572
|
Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
|
|
@@ -1324,7 +1325,7 @@ that preserves it, so there is no spelling that is allowed to.
|
|
|
1324
1325
|
| `from(rows).join([], …)` | `join takes another sequence as its inner side` | `from(sameSource)` |
|
|
1325
1326
|
| `from(a).join(from(b), …)` | `join's other side must derive from the same source, or from two providers sharing one scope (one store's entity sets) — a query document reads one input; load both collections under one root, or join two entity sets of one store` | one source, or one store's two entity sets (§13.2) |
|
|
1326
1327
|
| `from(rows).concat(42)` | `concat takes a sequence or a constant array` | a sequence over the same source, or an array |
|
|
1327
|
-
| `select((r) => r.v.all().rolling(spec))` where `spec` reads the row | `rolling() takes a plain literal spec object
|
|
1328
|
+
| `select((r) => r.v.all().rolling(spec))` where `spec` reads the row | `rolling() takes a plain literal spec object, not an expression or captured value` | a literal spec |
|
|
1328
1329
|
|
|
1329
1330
|
**A provider or a document that is not shaped as the contract says.**
|
|
1330
1331
|
|
|
@@ -1682,14 +1683,14 @@ are shorter:
|
|
|
1682
1683
|
## 17. Cost
|
|
1683
1684
|
|
|
1684
1685
|
A consumer importing `from` from `@jarenjs/linq` and calling one
|
|
1685
|
-
terminal bundles **<!--fact:bundle.chain-->
|
|
1686
|
+
terminal bundles **<!--fact:bundle.chain-->175,221<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
|
|
1686
1687
|
`platform: 'neutral'`). The figure is measured by
|
|
1687
1688
|
`scripts/check-tree-shaking.js`'s chain probe and compared with this
|
|
1688
1689
|
section on every `npm run test:tree-shaking`: it is derived, never typed,
|
|
1689
1690
|
and a stale one is red here rather than wrong in a document somebody
|
|
1690
1691
|
reads.
|
|
1691
1692
|
|
|
1692
|
-
Of that, **<!--fact:bundle.chain.own-->
|
|
1693
|
+
Of that, **<!--fact:bundle.chain.own-->38,701<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
|
|
1693
1694
|
`async.js`, `expression.js`, `document.js`, `provider.js`,
|
|
1694
1695
|
`concurrency.js`, `errors.js` and `schema-of.js`. The remaining ~134 kB
|
|
1695
1696
|
is the query ENGINE and the core it stands on: a chain's document has to
|
|
@@ -1710,13 +1711,13 @@ making:
|
|
|
1710
1711
|
`@jarenjs/formats`** — the client's optional peers. A consumer of the
|
|
1711
1712
|
chain alone installs nothing new;
|
|
1712
1713
|
- **no pen bytes at all**, in either direction: the pens carry no chain
|
|
1713
|
-
module either, which is what keeps a <!--fact:bundle.jslt.kb-->
|
|
1714
|
-
pen <!--fact:bundle.jslt.kb-->
|
|
1714
|
+
module either, which is what keeps a <!--fact:bundle.jslt.kb-->18<!--/fact--> kB JSLT
|
|
1715
|
+
pen <!--fact:bundle.jslt.kb-->18<!--/fact--> kB.
|
|
1715
1716
|
|
|
1716
1717
|
`docs/CONSUMING.md` states the rounded price of all ten subpaths in one
|
|
1717
1718
|
table, each figure held equal to the same measurements. Two of its rows
|
|
1718
|
-
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->
|
|
1719
|
-
`./db` at <!--fact:bundle.db.kb-->
|
|
1719
|
+
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->175<!--/fact--> kB and
|
|
1720
|
+
`./db` at <!--fact:bundle.db.kb-->676<!--/fact--> kB.
|
|
1720
1721
|
The client costs what the store costs, by construction, and the chain
|
|
1721
1722
|
costs what running a query costs.
|
|
1722
1723
|
|
|
@@ -1725,7 +1726,7 @@ the reason is worth knowing: a bundler counts a shared module once, and
|
|
|
1725
1726
|
the chain and every pen share the expression capture (`expression.js`)
|
|
1726
1727
|
and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
|
|
1727
1728
|
and object helpers). A consumer importing the chain AND the schema pen
|
|
1728
|
-
bundles **<!--fact:bundle.chain.withSchemaPen-->
|
|
1729
|
+
bundles **<!--fact:bundle.chain.withSchemaPen-->199,375<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->11,131<!--/fact--> bytes** less than the sum of the
|
|
1729
1730
|
figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
|
|
1730
1731
|
what those shared modules weigh. The probe measures that pair too, so
|
|
1731
1732
|
the saving is derived like everything else here. What the chain does NOT
|
|
@@ -1733,3 +1734,19 @@ share with a pen is the pens' own two shared doors, `capture-root.js`
|
|
|
1733
1734
|
and `json-boundary.js`: no chain callback reaches either, and neither is
|
|
1734
1735
|
in the figure above. Every pen document's §7 carries its own
|
|
1735
1736
|
subpath's figure; nothing here restates one.
|
|
1737
|
+
|
|
1738
|
+
## Native column queries and ranges
|
|
1739
|
+
|
|
1740
|
+
Chains and JSON documents share the db planner. Adopted scalar projections, connected joins, restricted correlated counts and proved one-root grouped aggregates have [native plans](../../db/docs/NATIVE-PLANS.md); unsupported shapes retain explanations and strict-mode refusals. The structural [range provider](../../app/docs/COLLECTION-PROVIDER.md) is `handle.range(spec, options)` from the root client, composed over the existing pager and capture.
|
|
1741
|
+
|
|
1742
|
+
|
|
1743
|
+
## Lexical provider authoring
|
|
1744
|
+
|
|
1745
|
+
A string expression's `lexical(provider, request)` method emits
|
|
1746
|
+
`{ $lexical: [provider, textExpression, request] }`. The provider name is a nonempty
|
|
1747
|
+
string; the request is a plain literal JSON object. It records a declaration,
|
|
1748
|
+
never builds an index or runs a second ranker. Compile the emitted document with
|
|
1749
|
+
`compileJsonQuery(document, {lexicalProviders})`, binding the same
|
|
1750
|
+
`createLexicalProvider` capability that a hand-written query uses. The ordinary
|
|
1751
|
+
sequence provider has no implicit search catalog. Filters/facets run before top-k,
|
|
1752
|
+
continuations bind the source snapshot, and `$search` remains regex-based.
|
package/docs/SCHEMA-PEN.md
CHANGED
|
@@ -1167,10 +1167,10 @@ hand, or generate it some other way, when:
|
|
|
1167
1167
|
|
|
1168
1168
|
## 7. Cost
|
|
1169
1169
|
|
|
1170
|
-
`@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->
|
|
1170
|
+
`@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->35,285<!--/fact--> bytes** as a minified,
|
|
1171
1171
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
1172
1172
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
1173
|
-
(<!--fact:bundle.schema.kb-->
|
|
1173
|
+
(<!--fact:bundle.schema.kb-->35<!--/fact--> kB) beside the other nine subpath prices in
|
|
1174
1174
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
1175
1175
|
|
|
1176
1176
|
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.
|
|
4
|
+
"version": "0.83.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -67,6 +67,10 @@
|
|
|
67
67
|
"./ai": {
|
|
68
68
|
"types": "./types/ai.d.ts",
|
|
69
69
|
"default": "./src/ai/index.js"
|
|
70
|
+
},
|
|
71
|
+
"./formula": {
|
|
72
|
+
"types": "./types/formula.d.ts",
|
|
73
|
+
"default": "./src/formula/index.js"
|
|
70
74
|
}
|
|
71
75
|
},
|
|
72
76
|
"files": [
|
|
@@ -104,13 +108,13 @@
|
|
|
104
108
|
"prepack": "npm run build:types"
|
|
105
109
|
},
|
|
106
110
|
"dependencies": {
|
|
107
|
-
"@jarenjs/core": "^0.
|
|
108
|
-
"@jarenjs/json": "^0.
|
|
111
|
+
"@jarenjs/core": "^0.83.2",
|
|
112
|
+
"@jarenjs/json": "^0.83.2"
|
|
109
113
|
},
|
|
110
114
|
"peerDependencies": {
|
|
111
|
-
"@jarenjs/db": "^0.
|
|
112
|
-
"@jarenjs/formats": "^0.
|
|
113
|
-
"@jarenjs/validate": "^0.
|
|
115
|
+
"@jarenjs/db": "^0.83.2",
|
|
116
|
+
"@jarenjs/formats": "^0.83.2",
|
|
117
|
+
"@jarenjs/validate": "^0.83.2"
|
|
114
118
|
},
|
|
115
119
|
"peerDependenciesMeta": {
|
|
116
120
|
"@jarenjs/db": {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/** Durable external intent/evidence in mapped tables, fenced by the existing job engine. */
|
|
3
|
+
import { canonicalizeJson } from '@jarenjs/json/canonical';
|
|
4
|
+
import { mappedRecords, recordTransaction, copyRecord, refuseRecord } from './records.js';
|
|
5
|
+
|
|
6
|
+
const zero = Object.freeze({ changes: 0, writes: 0, revisions: 0 });
|
|
7
|
+
const terminal = (state) => state === 'confirmed' || state === 'rejected';
|
|
8
|
+
/**
|
|
9
|
+
* Preparation/outbox and each settlement commit locally. Remote delivery never
|
|
10
|
+
* runs inside this adapter. Intent is permanent until evidence resolves it.
|
|
11
|
+
* @param {any} client @param {{ operations: any, maxLegs?: number, maxBytes?: number }} options
|
|
12
|
+
*/
|
|
13
|
+
export function createDbEffectStore(client, options) {
|
|
14
|
+
const records = mappedRecords(client, options?.operations);
|
|
15
|
+
const { maxLegs = 64, maxBytes = 262144 } = options;
|
|
16
|
+
for (const limit of [maxLegs, maxBytes]) if (!Number.isSafeInteger(limit) || limit < 1) throw new TypeError('effect limits must be finite positive integers');
|
|
17
|
+
const inside = (fn) => recordTransaction(client, fn);
|
|
18
|
+
const requireRecord = async (tx, id) => {
|
|
19
|
+
const record = await records.get(tx, id);
|
|
20
|
+
if (!record) throw refuseRecord('unknown external operation');
|
|
21
|
+
return record;
|
|
22
|
+
};
|
|
23
|
+
const guard = async (tx, record, revision, lease) => {
|
|
24
|
+
if (record.revision !== revision || lease?.jobId !== record.plan.jobId) throw refuseRecord('stale operation revision or foreign job');
|
|
25
|
+
await tx.jobs.assertLease(lease);
|
|
26
|
+
};
|
|
27
|
+
const legOf = (record, id) => {
|
|
28
|
+
const leg = record.legs.find((value) => value.id === id);
|
|
29
|
+
if (!leg) throw refuseRecord('unknown operation leg');
|
|
30
|
+
return leg;
|
|
31
|
+
};
|
|
32
|
+
const write = async (tx, record) => {
|
|
33
|
+
record.revision++;
|
|
34
|
+
await records.put(tx, record);
|
|
35
|
+
return { record, changes: 1, writes: 1, revisions: 1 };
|
|
36
|
+
};
|
|
37
|
+
return Object.freeze({
|
|
38
|
+
/** Caller authorization must establish review/compensation authority before preparation.
|
|
39
|
+
* @param {any} plan @param {(tx: any) => any} [prepare] */
|
|
40
|
+
prepare(plan, prepare) {
|
|
41
|
+
const frozen = copyRecord(plan);
|
|
42
|
+
if (['id', 'jobId', 'kind', 'actor', 'reason', 'hashVersion'].some((name) => typeof frozen[name] !== 'string' || !frozen[name])
|
|
43
|
+
|| !Array.isArray(frozen.legs) || !frozen.legs.length || frozen.legs.length > maxLegs
|
|
44
|
+
|| new Set(frozen.legs.map((leg) => leg.id)).size !== frozen.legs.length
|
|
45
|
+
|| frozen.legs.some((leg) => typeof leg.id !== 'string' || !leg.id
|
|
46
|
+
|| !['single-send', 'provider-idempotent'].includes(leg.request?.safety)
|
|
47
|
+
|| !Number.isSafeInteger(leg.maxAttempts) || leg.maxAttempts < 1 || leg.maxAttempts > 100
|
|
48
|
+
|| (leg.request.safety === 'single-send' && leg.maxAttempts !== 1)
|
|
49
|
+
|| (leg.request.safety === 'provider-idempotent' && !leg.request.idempotencyKey))
|
|
50
|
+
|| (frozen.compensationOf !== undefined && frozen.compensationAuthorized !== true)
|
|
51
|
+
|| new TextEncoder().encode(canonicalizeJson(frozen)).byteLength > maxBytes)
|
|
52
|
+
throw new TypeError('effect preparation needs bounded reviewed legs, replay safety, attempt budgets and explicit compensation authority');
|
|
53
|
+
const fingerprint = canonicalizeJson(frozen);
|
|
54
|
+
return inside(async (tx) => {
|
|
55
|
+
const prior = await records.get(tx, frozen.id);
|
|
56
|
+
if (prior) {
|
|
57
|
+
if (prior.fingerprint !== fingerprint) throw refuseRecord('reviewed operation payload changed');
|
|
58
|
+
return { record: prior, ...zero };
|
|
59
|
+
}
|
|
60
|
+
if (await tx.jobs.get(frozen.jobId) !== undefined) throw refuseRecord('operation job identity already belongs to another enqueue');
|
|
61
|
+
if (prepare) await prepare(tx);
|
|
62
|
+
const record = { id: frozen.id, plan: frozen, fingerprint, revision: 1,
|
|
63
|
+
legs: frozen.legs.map((leg) => ({ id: leg.id, state: 'prepared', attempts: 0, evidence: null, intent: null })), decisions: [] };
|
|
64
|
+
await records.put(tx, record, true);
|
|
65
|
+
await tx.jobs.enqueue(frozen.kind, { operationId: frozen.id }, { id: frozen.jobId });
|
|
66
|
+
return { record, changes: 1, writes: 1, revisions: 1 };
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
/** @param {string} id */
|
|
70
|
+
get: (id) => inside((tx) => requireRecord(tx, id)),
|
|
71
|
+
/** Persist sending before dispatch. A stale worker cannot start another send.
|
|
72
|
+
* @param {string} id @param {string} legId @param {number} revision @param {any} lease */
|
|
73
|
+
begin(id, legId, revision, lease) {
|
|
74
|
+
return inside(async (tx) => {
|
|
75
|
+
const record = await requireRecord(tx, id);
|
|
76
|
+
await guard(tx, record, revision, lease);
|
|
77
|
+
const leg = legOf(record, legId), plan = record.plan.legs.find((value) => value.id === legId);
|
|
78
|
+
if (leg.state !== 'prepared' && leg.state !== 'retry-approved') return { state: leg.state, record, ...zero };
|
|
79
|
+
if (leg.attempts >= plan.maxAttempts) return { state: 'exhausted', record, ...zero };
|
|
80
|
+
leg.state = 'sending';
|
|
81
|
+
leg.attempts++;
|
|
82
|
+
leg.intent = { generation: lease.generation, attempt: leg.attempts };
|
|
83
|
+
return { state: 'sending', ...(await write(tx, record)) };
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
/** Only the active attempt may persist transport evidence.
|
|
87
|
+
* @param {string} id @param {string} legId @param {number} revision @param {any} lease @param {any} outcome */
|
|
88
|
+
settle(id, legId, revision, lease, outcome) {
|
|
89
|
+
const observation = copyRecord(outcome);
|
|
90
|
+
if (!['confirmed', 'rejected', 'unresolved'].includes(observation.state)
|
|
91
|
+
|| !Object.hasOwn(observation, 'evidence') || new TextEncoder().encode(canonicalizeJson(observation)).byteLength > maxBytes)
|
|
92
|
+
throw refuseRecord('external settlement needs bounded confirmed/rejected/unresolved evidence');
|
|
93
|
+
return inside(async (tx) => {
|
|
94
|
+
const record = await requireRecord(tx, id);
|
|
95
|
+
await guard(tx, record, revision, lease);
|
|
96
|
+
const leg = legOf(record, legId);
|
|
97
|
+
if (leg.state !== 'sending' || leg.intent.generation !== lease.generation) throw refuseRecord('no sending intent for this attempt');
|
|
98
|
+
leg.state = observation.state;
|
|
99
|
+
leg.evidence = observation.evidence;
|
|
100
|
+
return write(tx, record);
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
/** Lost workers leave uncertainty, never proof of non-application.
|
|
104
|
+
* @param {string} id @param {number} revision @param {any} lease */
|
|
105
|
+
recover(id, revision, lease) {
|
|
106
|
+
return inside(async (tx) => {
|
|
107
|
+
const record = await requireRecord(tx, id);
|
|
108
|
+
await guard(tx, record, revision, lease);
|
|
109
|
+
let changed = false;
|
|
110
|
+
for (const leg of record.legs) if (leg.state === 'sending') { leg.state = 'unresolved'; changed = true; }
|
|
111
|
+
return changed ? write(tx, record) : { record, ...zero };
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
/** Explicit read-back or operator decisions; absence is evidence only under
|
|
115
|
+
* the declared authoritative non-application guarantee. Idempotent retries
|
|
116
|
+
* retain the reviewed request/key and the original durable attempt budget.
|
|
117
|
+
* @param {string} id @param {string} legId @param {number} revision @param {any} lease @param {any} decision */
|
|
118
|
+
reconcile(id, legId, revision, lease, decision) {
|
|
119
|
+
const proof = copyRecord(decision);
|
|
120
|
+
if (['id', 'actor', 'reason'].some((key) => typeof proof[key] !== 'string' || !proof[key])
|
|
121
|
+
|| !['confirm', 'reject', 'retry'].includes(proof.action) || !Object.hasOwn(proof, 'evidence')) throw refuseRecord('reconciliation needs actor/reason and evidence');
|
|
122
|
+
return inside(async (tx) => {
|
|
123
|
+
const record = await requireRecord(tx, id);
|
|
124
|
+
if (lease?.jobId !== record.plan.jobId) throw refuseRecord('foreign reconciliation job');
|
|
125
|
+
await tx.jobs.assertLease(lease);
|
|
126
|
+
const entry = { ...proof, legId };
|
|
127
|
+
const prior = record.decisions.find((value) => value.id === proof.id);
|
|
128
|
+
if (prior) {
|
|
129
|
+
if (canonicalizeJson(prior) !== canonicalizeJson(entry)) throw refuseRecord('reconciliation decision collision');
|
|
130
|
+
return { record, ...zero };
|
|
131
|
+
}
|
|
132
|
+
await guard(tx, record, revision, lease);
|
|
133
|
+
const leg = legOf(record, legId), plan = record.plan.legs.find((value) => value.id === legId);
|
|
134
|
+
if (terminal(leg.state) || !['sending', 'unresolved'].includes(leg.state)) throw refuseRecord('only unresolved intent can be reconciled');
|
|
135
|
+
if (proof.action === 'retry' && (plan.request.safety !== 'provider-idempotent' && proof.guarantee !== 'authoritative-non-application'))
|
|
136
|
+
throw refuseRecord('absence is not proof of non-application');
|
|
137
|
+
// Single-send remains single-send: an authoritative negative result can
|
|
138
|
+
// be recorded as rejected, followed by a separately reviewed operation.
|
|
139
|
+
if (proof.action === 'retry' && leg.attempts >= plan.maxAttempts) throw refuseRecord('durable attempt budget exhausted');
|
|
140
|
+
if (record.decisions.length >= 128 || new TextEncoder().encode(canonicalizeJson(entry)).byteLength > maxBytes) throw refuseRecord('reconciliation evidence limit');
|
|
141
|
+
leg.state = proof.action === 'confirm' ? 'confirmed' : proof.action === 'reject' ? 'rejected' : 'retry-approved';
|
|
142
|
+
leg.evidence = proof.evidence;
|
|
143
|
+
record.decisions.push(entry);
|
|
144
|
+
return write(tx, record);
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
package/src/db/handle.js
CHANGED
|
@@ -18,6 +18,7 @@ import { fromAsync, AsyncSequence } from '../async.js';
|
|
|
18
18
|
import { Graph } from './include.js';
|
|
19
19
|
import { requireMembership } from './membership.js';
|
|
20
20
|
import { registerLive } from './live.js';
|
|
21
|
+
import { createDbRangeProvider } from './range.js';
|
|
21
22
|
|
|
22
23
|
/** The chain surface, read once from the class: every public operator
|
|
23
24
|
* and terminal, `explain` set aside for its overload. */
|
|
@@ -59,6 +60,7 @@ export function createEntityHandle(store, name) {
|
|
|
59
60
|
// the graph with nothing included: the root clauses, the keyset and
|
|
60
61
|
// the page over the rows alone
|
|
61
62
|
members.graph = () => new Graph(set, name);
|
|
63
|
+
members.range = (spec, options) => createDbRangeProvider(store, name, spec, options);
|
|
62
64
|
members.link = (own, member, target) => {
|
|
63
65
|
requireMembership(set.relations, name, member, 'link');
|
|
64
66
|
set.link(own, member, target);
|
package/src/db/index.js
CHANGED
|
@@ -22,4 +22,10 @@
|
|
|
22
22
|
|
|
23
23
|
export { open, defaultValidator } from './open.js';
|
|
24
24
|
export { createDbLedger } from './ledger.js';
|
|
25
|
+
export { createDbIngestionStore } from './ingest.js';
|
|
26
|
+
export { createDbRangeProvider } from './range.js';
|
|
27
|
+
export { createLexicalRangeProvider } from './search.js';
|
|
25
28
|
export { defineReplication } from './replication.js';
|
|
29
|
+
export { createDbReceipts } from './receipts.js';
|
|
30
|
+
export { createDbEffectStore } from './effects.js';
|
|
31
|
+
export { createDbRunStore } from './runs.js';
|