@jarenjs/linq 0.49.2 → 0.66.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +227 -0
  2. package/README.md +650 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1221 -0
  5. package/docs/DB-CLIENT.md +882 -0
  6. package/docs/FLOW-PEN.md +1033 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +778 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1092 -0
  12. package/docs/QUERY-PEN.md +1724 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +251 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +255 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +377 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +48 -11
  24. package/src/contract/define.js +282 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +338 -0
  28. package/src/db/handle.js +89 -0
  29. package/src/db/include.js +351 -0
  30. package/src/db/index.js +24 -0
  31. package/src/db/ledger.js +195 -0
  32. package/src/db/live.js +43 -0
  33. package/src/db/membership.js +37 -0
  34. package/src/db/open.js +130 -0
  35. package/src/document.js +143 -13
  36. package/src/effect.js +65 -0
  37. package/src/errors.js +78 -6
  38. package/src/expression.js +463 -36
  39. package/src/federate.js +531 -0
  40. package/src/flow/capture.js +33 -0
  41. package/src/flow/dag.js +316 -0
  42. package/src/flow/fsm.js +323 -0
  43. package/src/flow/index.js +22 -0
  44. package/src/forms/index.js +43 -0
  45. package/src/forms/rules.js +170 -0
  46. package/src/forms/submit.js +177 -0
  47. package/src/index.js +5 -2
  48. package/src/jslt/body.js +226 -0
  49. package/src/jslt/index.js +18 -0
  50. package/src/jslt/rules.js +202 -0
  51. package/src/json-boundary.js +90 -0
  52. package/src/migration/define.js +318 -0
  53. package/src/migration/index.js +15 -0
  54. package/src/migration/steps.js +244 -0
  55. package/src/model/collection.js +273 -0
  56. package/src/model/define.js +125 -0
  57. package/src/model/entity.js +307 -0
  58. package/src/model/index.js +47 -0
  59. package/src/model/relation.js +85 -0
  60. package/src/provider.js +137 -20
  61. package/src/schema/brand.js +31 -0
  62. package/src/schema/builders.js +526 -0
  63. package/src/schema/check.js +29 -0
  64. package/src/schema/emit.js +394 -0
  65. package/src/schema/factories.js +239 -0
  66. package/src/schema/index.js +37 -0
  67. package/src/schema-of.js +24 -0
  68. package/src/sequence.js +233 -103
  69. package/src/sources.js +10 -3
  70. package/types/app.d.ts +293 -0
  71. package/types/contract.d.ts +468 -0
  72. package/types/db.d.ts +359 -0
  73. package/types/flow.d.ts +285 -0
  74. package/types/forms.d.ts +253 -0
  75. package/types/index.d.ts +296 -26
  76. package/types/jslt.d.ts +193 -0
  77. package/types/migration.d.ts +201 -0
  78. package/types/model.d.ts +526 -0
  79. package/types/schema.d.ts +494 -0
@@ -0,0 +1,247 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `http()` — the REST binding of one operation (CONTRACT-FORMAT
4
+ * §4), written as the format spells it and checked as far as the pen can
5
+ * see. The path template scan mirrors the compiler's own parser: every
6
+ * form §4.2 reserves is refused by name with `JL0102`, at build time,
7
+ * before `compileContract` would answer `JC0008` with the same meaning.
8
+ * Nothing is canonicalized here — a `:name` template is written as the
9
+ * author declared it, and the projections are what show the `{name}`
10
+ * form.
11
+ */
12
+
13
+ import { LinqBuildError } from '../errors.js';
14
+ import { describeValue, requireJson } from '../json-boundary.js';
15
+
16
+ /** The binding brand: how `defineContract` tells a checked binding apart. */
17
+ export const HTTP_BINDING = Symbol.for('@jarenjs/linq/contract-http');
18
+
19
+ /** The members `http` accepts, in the order §12.1 fixes. */
20
+ export const HTTP_MEMBERS = Object.freeze(['method', 'path', 'in', 'body', 'status', 'media']);
21
+
22
+ /** The uppercase tokens §4's table lists. */
23
+ export const HTTP_METHODS = Object.freeze(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']);
24
+
25
+ /** The four places an input member can travel. */
26
+ export const LOCATIONS = Object.freeze(['path', 'query', 'header', 'body']);
27
+
28
+ /** `[A-Za-z_][A-Za-z0-9_]*` — a path variable's name. */
29
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
30
+
31
+ /** The RFC 6570 operators a `{…}` expression may open with; all reserved. */
32
+ const OPERATORS = '+#./;?&=';
33
+
34
+ /**
35
+ * One static segment: any character but the structural ones (`/ { } : *
36
+ * ? #`), whitespace and controls; a `%` must open a well-formed escape.
37
+ * @param {string} segment
38
+ * @param {string} at
39
+ */
40
+ function checkStatic(segment, at) {
41
+ for (let i = 0; i < segment.length; i++) {
42
+ const ch = segment[i];
43
+ if (ch === '{' || ch === '}') {
44
+ throw new LinqBuildError('JL0102',
45
+ `a variable must be a whole segment ("{name}"), found "${segment}" — the format `
46
+ + 'reserves a variable that is only part of a segment', at);
47
+ }
48
+ if (ch === ':') {
49
+ throw new LinqBuildError('JL0102',
50
+ `":" is reserved for a variable segment (":name"), found "${segment}"`, at);
51
+ }
52
+ if (ch === '*') {
53
+ throw new LinqBuildError('JL0102',
54
+ `"*" is a reserved wildcard form; $contract 0.1 has no wildcards, found "${segment}"`, at);
55
+ }
56
+ if (ch === '?' || ch === '#') {
57
+ throw new LinqBuildError('JL0102',
58
+ `"${ch}" cannot appear in a path template (the query and fragment are not part of `
59
+ + 'the path)', at);
60
+ }
61
+ const code = segment.charCodeAt(i);
62
+ if (code <= 0x20 || code === 0x7f) {
63
+ throw new LinqBuildError('JL0102',
64
+ `whitespace or a control character in segment "${segment}"`, at);
65
+ }
66
+ if (ch === '%') {
67
+ if (!/^[0-9A-Fa-f]{2}$/.test(segment.slice(i + 1, i + 3))) {
68
+ throw new LinqBuildError('JL0102',
69
+ `a malformed percent-escape in segment "${segment}"`, at);
70
+ }
71
+ i += 2;
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * The variables a template declares, refusing every reserved form by
78
+ * name (§4.2). Mirrors the compiler's parser; nothing is rewritten.
79
+ * @param {any} source
80
+ * @param {string} at
81
+ * @returns {string[]} the variable names, in order
82
+ */
83
+ export function pathVariables(source, at) {
84
+ if (typeof source !== 'string') {
85
+ throw new LinqBuildError('JL0102',
86
+ `http() path is a path template string, got ${describeValue(source)}`, at);
87
+ }
88
+ if (source.length === 0 || source[0] !== '/') {
89
+ throw new LinqBuildError('JL0102', 'a path template must start with "/"', at);
90
+ }
91
+ /** @type {string[]} */
92
+ const variables = [];
93
+ if (source === '/') return variables;
94
+ const segments = source.slice(1).split('/');
95
+ for (let i = 0; i < segments.length; i++) {
96
+ const segment = segments[i];
97
+ if (segment.length === 0) {
98
+ throw new LinqBuildError('JL0102', i === segments.length - 1
99
+ ? 'a trailing "/" declares an empty segment; the root template "/" is the only empty path'
100
+ : 'an empty segment ("//")', at);
101
+ }
102
+ /** @type {string | null} */
103
+ let name = null;
104
+ if (segment[0] === '{') {
105
+ if (segment[segment.length - 1] !== '}') {
106
+ throw new LinqBuildError('JL0102',
107
+ `a variable must be a whole segment ("{name}"), found "${segment}" — the format `
108
+ + 'reserves a variable that is only part of a segment', at);
109
+ }
110
+ name = segment.slice(1, -1);
111
+ if (name.length > 0 && OPERATORS.includes(name[0])) {
112
+ throw new LinqBuildError('JL0102',
113
+ `"{${name}}" uses the reserved RFC 6570 operator "${name[0]}"; $contract 0.1 `
114
+ + 'supports only "{name}"', at);
115
+ }
116
+ const last = name[name.length - 1];
117
+ if (last === '+' || last === '*') {
118
+ throw new LinqBuildError('JL0102',
119
+ `"{${name}}" uses the reserved "${last}" expansion modifier; $contract 0.1 has no `
120
+ + 'wildcards', at);
121
+ }
122
+ if (name.includes(',') || name.includes(':')) {
123
+ throw new LinqBuildError('JL0102',
124
+ `"{${name}}" uses a reserved RFC 6570 list or prefix form; $contract 0.1 supports `
125
+ + 'only "{name}"', at);
126
+ }
127
+ }
128
+ else if (segment[0] === ':') {
129
+ name = segment.slice(1);
130
+ if (name.length === 0 || name.includes('{') || name.includes('}')) {
131
+ throw new LinqBuildError('JL0102',
132
+ `":name" must be a whole segment with an identifier name, found "${segment}"`, at);
133
+ }
134
+ const last = name[name.length - 1];
135
+ if (last === '*' || last === '+' || last === '?') {
136
+ throw new LinqBuildError('JL0102',
137
+ `":${name}" uses a reserved "${last}" modifier; $contract 0.1 has no wildcards or `
138
+ + 'optional segments', at);
139
+ }
140
+ }
141
+ if (name === null) {
142
+ checkStatic(segment, at);
143
+ continue;
144
+ }
145
+ if (!IDENT.test(name)) {
146
+ throw new LinqBuildError('JL0102',
147
+ `a variable name must match [A-Za-z_][A-Za-z0-9_]*, found "${name}"`, at);
148
+ }
149
+ if (variables.includes(name)) {
150
+ throw new LinqBuildError('JL0102', `the variable "${name}" is declared twice`, at);
151
+ }
152
+ variables.push(name);
153
+ }
154
+ return variables;
155
+ }
156
+
157
+ /**
158
+ * One operation's HTTP binding, checked and branded. The members are
159
+ * written in §12.1's order — `method`, `path`, `in`, `body`, `status`,
160
+ * `media` — and only the ones declared: a default is the compiler's to
161
+ * materialize, never the pen's to write.
162
+ *
163
+ * @param {any} spec - `{ method, path, in?, body?, status?, media? }`
164
+ * @returns {any} the binding, frozen and branded
165
+ * @throws {LinqBuildError} `JL0101` a member that is not what it takes;
166
+ * `JL0102` a path template form the format reserves
167
+ * @example
168
+ * http({ method: 'PUT', path: '/api/products/{id}', in: { revision: 'body' } });
169
+ */
170
+ export function http(spec) {
171
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
172
+ throw new LinqBuildError('JL0101',
173
+ `http() takes { method, path, in?, body?, status?, media? }, got ${describeValue(spec)}`);
174
+ }
175
+ for (const key of Object.keys(spec)) {
176
+ if (!HTTP_MEMBERS.includes(key)) {
177
+ throw new LinqBuildError('JL0101',
178
+ `http() does not take '${key}' — the binding is ${HTTP_MEMBERS.join(', ')} `
179
+ + '(CONTRACT-FORMAT §4)', `/${key}`);
180
+ }
181
+ }
182
+ if (!HTTP_METHODS.includes(spec.method)) {
183
+ throw new LinqBuildError('JL0101',
184
+ `http() method is one uppercase token of ${HTTP_METHODS.join(' ')}, got `
185
+ + `${describeValue(spec.method)}`, '/method');
186
+ }
187
+ const variables = pathVariables(spec.path, '/path');
188
+
189
+ const out = { method: spec.method, path: spec.path };
190
+ if (spec.in !== undefined) {
191
+ const locations = requireJson(spec.in, 'http() in');
192
+ if (locations === null || typeof locations !== 'object' || Array.isArray(locations)) {
193
+ throw new LinqBuildError('JL0101',
194
+ 'http() in is a plain object of input member → path | query | header | body', '/in');
195
+ }
196
+ const placed = {};
197
+ for (const member of Object.keys(locations)) {
198
+ const where = locations[member];
199
+ if (!LOCATIONS.includes(where)) {
200
+ throw new LinqBuildError('JL0101',
201
+ `http() in.${member} is one of ${LOCATIONS.join(', ')}, got ${describeValue(where)}`,
202
+ `/in/${member}`);
203
+ }
204
+ if (where === 'path' && !variables.includes(member)) {
205
+ throw new LinqBuildError('JL0102',
206
+ `http() maps '${member}' to path, but the template declares no {${member}} — a `
207
+ + 'path member is named by the template itself', `/in/${member}`);
208
+ }
209
+ placed[member] = where;
210
+ }
211
+ out.in = placed;
212
+ }
213
+ if (spec.body !== undefined) {
214
+ if (typeof spec.body !== 'string' || spec.body.length === 0) {
215
+ throw new LinqBuildError('JL0101',
216
+ `http() body names the input member whose value IS the request body, got `
217
+ + `${describeValue(spec.body)}`, '/body');
218
+ }
219
+ out.body = spec.body;
220
+ }
221
+ if (spec.status !== undefined) {
222
+ if (!Number.isInteger(spec.status) || spec.status < 200 || spec.status > 299) {
223
+ throw new LinqBuildError('JL0101',
224
+ `http() status is an integer in 200–299, got ${describeValue(spec.status)}`, '/status');
225
+ }
226
+ out.status = spec.status;
227
+ }
228
+ if (spec.media !== undefined) {
229
+ if (typeof spec.media !== 'string' || spec.media.length === 0) {
230
+ throw new LinqBuildError('JL0101',
231
+ `http() media is a media type, got ${describeValue(spec.media)}`, '/media');
232
+ }
233
+ out.media = spec.media;
234
+ }
235
+ Object.defineProperty(out, HTTP_BINDING, { value: true, enumerable: false });
236
+ return Object.freeze(out);
237
+ }
238
+
239
+ /**
240
+ * Whether a value is an `http()` binding (as opposed to the same members
241
+ * written by hand, which `defineContract` accepts and checks the same way).
242
+ * @param {any} value
243
+ * @returns {boolean}
244
+ */
245
+ export function isHttpBinding(value) {
246
+ return value !== null && typeof value === 'object' && value[HTTP_BINDING] === true;
247
+ }
@@ -0,0 +1,23 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/contract` — `$contract` 0.1 documents by code.
4
+ * `defineContract({ id?, version?, compat? }, operations)` writes the
5
+ * document `compileContract` takes, with every `named()` schema builder
6
+ * an operation reaches hoisted into the contract's own `$defs` and every
7
+ * member in the order CONTRACT-FORMAT §12.1 fixes, so a pen document and
8
+ * its own public projection differ by nothing but the defaults the
9
+ * compiler materializes. `read`/`command`/`subscribe` declare the three
10
+ * kinds, `http()` the REST binding (its path template checked here,
11
+ * earlier than the compiler's `JC0008`), `error()` one entry of an
12
+ * operation's `errors`.
13
+ *
14
+ * The identity wrappers — `typedClient`, `typedHttpClient`,
15
+ * `typedHandlers`, `typedTools` — carry the inferred `Operations` type
16
+ * onto a client (an HTTP one with its byte method), a handler table and
17
+ * an AI toolbox without running the TypeScript projection. The document is the deliverable; nothing here imports
18
+ * `@jarenjs/contract` or an engine.
19
+ */
20
+
21
+ export { defineContract, typedClient, typedHttpClient, typedHandlers, typedTools, Contract } from './define.js';
22
+ export { read, command, subscribe, error } from './operation.js';
23
+ export { http } from './http.js';
@@ -0,0 +1,338 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The operation declarations of the contract pen — `read()`,
4
+ * `command()`, `subscribe()` (CONTRACT-FORMAT §3's three kinds) and
5
+ * `error()` (an entry of an operation's `errors` map) — plus the policy
6
+ * vocabulary §3.1 fixes.
7
+ *
8
+ * These write no document: they carry a checked spec that
9
+ * `defineContract` lowers, because the schemas an operation names are
10
+ * hoisted into the contract's own `$defs` and only the whole document
11
+ * knows them. The pen refuses its own surface (a member it does not
12
+ * know, a policy value outside its declared set) and leaves the
13
+ * format's cross-member rules — a read that declares idempotency, a
14
+ * body-located member on a GET, two operations sharing a route shape —
15
+ * to `compileContract`, which is the only judge of them.
16
+ *
17
+ * No default is ever written: §3.1's defaults are materialized by the
18
+ * compiler and marked inferred by `describe()`. A pen that wrote them
19
+ * would turn every default into a declaration and move the revision for
20
+ * nothing.
21
+ */
22
+
23
+ import { isJsonObject } from '@jarenjs/core/object';
24
+ import { LinqBuildError } from '../errors.js';
25
+ import { describeValue } from '../json-boundary.js';
26
+
27
+ /** The operation brand: how `defineContract` tells a declaration apart. */
28
+ export const OPERATION = Symbol.for('@jarenjs/linq/contract-operation');
29
+
30
+ /** The error-declaration brand. */
31
+ export const ERROR_DECLARATION = Symbol.for('@jarenjs/linq/contract-error');
32
+
33
+ /** The three kinds §3 declares. */
34
+ export const KINDS = Object.freeze(['read', 'command', 'subscribe']);
35
+
36
+ /** The members an operation spec accepts, in the order §12.1 fixes. */
37
+ export const OPERATION_MEMBERS = Object.freeze(['input', 'output', 'errors', 'policy', 'http', 'doc']);
38
+
39
+ /**
40
+ * The policy members, in the order the pen writes them: §12.1's public
41
+ * order with the two server-side knobs (`limits`, `errors`) in their
42
+ * §3.1 positions — the projection drops those two, so their place is
43
+ * the source document's own.
44
+ */
45
+ export const POLICY_MEMBERS = Object.freeze([
46
+ 'task', 'idempotency', 'revision', 'cache', 'limits', 'errors', 'retry', 'stream', 'audience',
47
+ ]);
48
+
49
+ /** The value sets §3.1's table declares, by member. */
50
+ export const POLICY_VALUES = Object.freeze({
51
+ __proto__: null,
52
+ task: ['switch', 'exhaust', 'concat', 'parallel'],
53
+ idempotency: ['none', 'optional', 'required'],
54
+ cache: ['none', 'revision'],
55
+ audience: ['public', 'server'],
56
+ });
57
+
58
+ /** An error code: `^[a-z][a-z0-9-]*$` (§3's table). */
59
+ const CODE = /^[a-z][a-z0-9-]*$/;
60
+
61
+ /**
62
+ * A member set the pen knows, or `JL0101` naming the one it does not.
63
+ * @param {any} spec
64
+ * @param {readonly string[]} members
65
+ * @param {string} what
66
+ * @param {string} at
67
+ */
68
+ function closedTo(spec, members, what, at) {
69
+ for (const key of Object.keys(spec)) {
70
+ if (!members.includes(key)) {
71
+ throw new LinqBuildError('JL0101',
72
+ `${what} does not take '${key}' — it takes ${members.join(', ')}`, `${at}/${key}`);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * One entry of `policy.retry`/`policy.stream`/`policy.limits`/
79
+ * `policy.errors`, checked against §3.1's table.
80
+ * @param {string} member
81
+ * @param {any} value
82
+ * @param {string} at
83
+ * @returns {any} the member's emitted value
84
+ */
85
+ function policyMember(member, value, at) {
86
+ const set = POLICY_VALUES[member];
87
+ if (set !== undefined) {
88
+ if (!set.includes(value)) {
89
+ throw new LinqBuildError('JL0101',
90
+ `policy.${member} is one of ${set.join(', ')}, got ${describeValue(value)}`, at);
91
+ }
92
+ return value;
93
+ }
94
+ if (member === 'revision') {
95
+ if (typeof value !== 'string' || !value.startsWith('input:')) {
96
+ throw new LinqBuildError('JL0101',
97
+ 'policy.revision is "input:<json-pointer>" — where in the input the revision a '
98
+ + `command asserts lives, got ${describeValue(value)}`, at);
99
+ }
100
+ return value;
101
+ }
102
+ if (member === 'limits') {
103
+ if (!isJsonObject(value)) {
104
+ throw new LinqBuildError('JL0101', 'policy.limits is { maxBodyBytes }', at);
105
+ }
106
+ closedTo(value, ['maxBodyBytes'], 'policy.limits', at);
107
+ if (!Number.isInteger(value.maxBodyBytes) || value.maxBodyBytes <= 0) {
108
+ throw new LinqBuildError('JL0101',
109
+ `policy.limits.maxBodyBytes is a positive integer, got ${describeValue(value.maxBodyBytes)}`,
110
+ `${at}/maxBodyBytes`);
111
+ }
112
+ return { maxBodyBytes: value.maxBodyBytes };
113
+ }
114
+ if (member === 'errors') {
115
+ if (!isJsonObject(value)) {
116
+ throw new LinqBuildError('JL0101', 'policy.errors is { details }', at);
117
+ }
118
+ closedTo(value, ['details'], 'policy.errors', at);
119
+ if (!['none', 'paths', 'full'].includes(value.details)) {
120
+ throw new LinqBuildError('JL0101',
121
+ `policy.errors.details is one of none, paths, full, got ${describeValue(value.details)}`,
122
+ `${at}/details`);
123
+ }
124
+ return { details: value.details };
125
+ }
126
+ if (member === 'retry') {
127
+ if (!isJsonObject(value)) {
128
+ throw new LinqBuildError('JL0101', 'policy.retry is { max, on }', at);
129
+ }
130
+ closedTo(value, ['max', 'on'], 'policy.retry', at);
131
+ if (!Number.isInteger(value.max) || value.max < 0) {
132
+ throw new LinqBuildError('JL0101',
133
+ `policy.retry.max is an integer ≥ 0, got ${describeValue(value.max)}`, `${at}/max`);
134
+ }
135
+ if (!Array.isArray(value.on) || value.on.some((code) => typeof code !== 'string')) {
136
+ throw new LinqBuildError('JL0101',
137
+ 'policy.retry.on is an array of error codes (declared codes, or JC2xxx taxonomy codes)',
138
+ `${at}/on`);
139
+ }
140
+ return { max: value.max, on: value.on.slice() };
141
+ }
142
+ // stream
143
+ if (!isJsonObject(value)) {
144
+ throw new LinqBuildError('JL0101', 'policy.stream is { resume?, heartbeatMs?, maxPatchBytes? }', at);
145
+ }
146
+ closedTo(value, ['resume', 'heartbeatMs', 'maxPatchBytes'], 'policy.stream', at);
147
+ const stream = {};
148
+ if (value.resume !== undefined) {
149
+ if (value.resume !== 'snapshot' && value.resume !== 'replay') {
150
+ throw new LinqBuildError('JL0101',
151
+ `policy.stream.resume is snapshot or replay, got ${describeValue(value.resume)}`,
152
+ `${at}/resume`);
153
+ }
154
+ stream.resume = value.resume;
155
+ }
156
+ if (value.heartbeatMs !== undefined) {
157
+ if (!Number.isInteger(value.heartbeatMs) || value.heartbeatMs < 1000) {
158
+ throw new LinqBuildError('JL0101',
159
+ `policy.stream.heartbeatMs is an integer ≥ 1000, got ${describeValue(value.heartbeatMs)}`,
160
+ `${at}/heartbeatMs`);
161
+ }
162
+ stream.heartbeatMs = value.heartbeatMs;
163
+ }
164
+ if (value.maxPatchBytes !== undefined) {
165
+ if (!Number.isInteger(value.maxPatchBytes) || value.maxPatchBytes <= 0) {
166
+ throw new LinqBuildError('JL0101',
167
+ 'policy.stream.maxPatchBytes is a positive integer, got '
168
+ + `${describeValue(value.maxPatchBytes)}`, `${at}/maxPatchBytes`);
169
+ }
170
+ stream.maxPatchBytes = value.maxPatchBytes;
171
+ }
172
+ return stream;
173
+ }
174
+
175
+ /**
176
+ * One operation's `policy`, in the pen's member order, declared members
177
+ * only.
178
+ * @param {any} policy
179
+ * @param {string} at
180
+ * @returns {any}
181
+ */
182
+ export function emitPolicy(policy, at) {
183
+ if (!isJsonObject(policy)) {
184
+ throw new LinqBuildError('JL0101',
185
+ `policy is a plain object of the members CONTRACT-FORMAT §3.1 declares, got `
186
+ + `${describeValue(policy)}`, at);
187
+ }
188
+ closedTo(policy, POLICY_MEMBERS, 'policy', at);
189
+ const out = {};
190
+ for (const member of POLICY_MEMBERS) {
191
+ if (policy[member] === undefined) continue;
192
+ out[member] = policyMember(member, policy[member], `${at}/${member}`);
193
+ }
194
+ return out;
195
+ }
196
+
197
+ /**
198
+ * One entry of an operation's `errors` map — `{ status?, schema? }`.
199
+ * The `schema` may be a schema-pen builder (hoisted into the contract's
200
+ * `$defs` like any other) or a JSON Schema written by hand.
201
+ *
202
+ * @param {any} [spec] - `{ status?, schema? }`
203
+ * @returns {any} the declaration, frozen and branded
204
+ * @throws {LinqBuildError} `JL0101` a member the declaration does not take
205
+ * @example
206
+ * error({ status: 409, schema: Conflict });
207
+ */
208
+ export function error(spec = {}) {
209
+ if (!isJsonObject(spec)) {
210
+ throw new LinqBuildError('JL0101',
211
+ `error() takes { status?, schema? }, got ${describeValue(spec)}`);
212
+ }
213
+ closedTo(spec, ['status', 'schema'], 'error()', '');
214
+ if (spec.status !== undefined
215
+ && (!Number.isInteger(spec.status) || spec.status < 100 || spec.status > 599)) {
216
+ throw new LinqBuildError('JL0101',
217
+ `error() status is an integer in 100–599, got ${describeValue(spec.status)}`, '/status');
218
+ }
219
+ const out = {};
220
+ if (spec.status !== undefined) out.status = spec.status;
221
+ if (spec.schema !== undefined) out.schema = spec.schema;
222
+ Object.defineProperty(out, ERROR_DECLARATION, { value: true, enumerable: false });
223
+ return Object.freeze(out);
224
+ }
225
+
226
+ /**
227
+ * The `errors` map of an operation, checked: codes match §3's pattern,
228
+ * every entry is an `error()` declaration or the same members by hand.
229
+ * @param {any} errors
230
+ * @param {string} at
231
+ * @returns {[string, any][]} code → `{ status?, schema? }`, in declaration order
232
+ */
233
+ export function readErrors(errors, at) {
234
+ if (!isJsonObject(errors)) {
235
+ throw new LinqBuildError('JL0101',
236
+ `errors is a plain object of code → error(), got ${describeValue(errors)}`, at);
237
+ }
238
+ return Object.keys(errors).map((code) => {
239
+ if (!CODE.test(code)) {
240
+ throw new LinqBuildError('JL0101',
241
+ `an error code matches ^[a-z][a-z0-9-]*$, got '${code}'`, `${at}/${code}`);
242
+ }
243
+ const declared = errors[code];
244
+ if (!isJsonObject(declared)) {
245
+ throw new LinqBuildError('JL0101',
246
+ `errors.${code} is error({ status?, schema? }), got ${describeValue(declared)}`,
247
+ `${at}/${code}`);
248
+ }
249
+ return [code, declared[ERROR_DECLARATION] === true ? declared : error(declared)];
250
+ });
251
+ }
252
+
253
+ /**
254
+ * One operation spec, checked against §3's member set. There are two
255
+ * doors into `defineContract`'s emitter and both run this: a
256
+ * `read()`/`command()`/`subscribe()` declaration, which has no position
257
+ * in the document yet, and an operation written by hand as `{ kind,
258
+ * …members }`, which does. A member this pen does not know must not
259
+ * reach the document whichever door it came through — the pen emits only
260
+ * what it was given, so an unchecked member is either dropped in silence
261
+ * or written into a document the grammar refuses.
262
+ * @param {string} kind
263
+ * @param {any} spec
264
+ * @param {string} [at] - the docPath of the operation being assembled;
265
+ * absent at declaration, where the operation has no id yet
266
+ */
267
+ export function checkOperation(kind, spec, at) {
268
+ const base = at ?? '';
269
+ if (!isJsonObject(spec)) {
270
+ throw new LinqBuildError('JL0101',
271
+ `${kind}() takes { input?, output, errors?, policy?, http?, doc? }, got `
272
+ + `${describeValue(spec)}`, at);
273
+ }
274
+ closedTo(spec, OPERATION_MEMBERS, `${kind}()`, base);
275
+ if (spec.output === undefined) {
276
+ throw new LinqBuildError('JL0101',
277
+ `${kind}() needs an output — every operation declares one (true for "any value")`,
278
+ `${base}/output`);
279
+ }
280
+ if (spec.doc !== undefined && typeof spec.doc !== 'string') {
281
+ throw new LinqBuildError('JL0101',
282
+ `${kind}() doc is a string, got ${describeValue(spec.doc)}`, `${base}/doc`);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * One operation declaration of `kind`.
288
+ * @param {string} kind
289
+ * @param {any} spec
290
+ * @returns {any}
291
+ */
292
+ function operation(kind, spec) {
293
+ checkOperation(kind, spec);
294
+ const out = { kind, spec };
295
+ Object.defineProperty(out, OPERATION, { value: true, enumerable: false });
296
+ return Object.freeze(out);
297
+ }
298
+
299
+ /**
300
+ * A `read` operation: a query whose result may be cached and whose
301
+ * input members default to the query string.
302
+ * @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
303
+ * @returns {any}
304
+ * @example
305
+ * read({ output: Catalog, http: http({ method: 'GET', path: '/api/catalog' }) });
306
+ */
307
+ export function read(spec) { return operation('read', spec); }
308
+
309
+ /**
310
+ * A `command` operation: a state change whose input members default to
311
+ * the request body.
312
+ * @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
313
+ * @returns {any}
314
+ * @example
315
+ * command({ input: SaveInput, output: Product, errors: { conflict: error({ status: 409 }) } });
316
+ */
317
+ export function command(spec) { return operation('command', spec); }
318
+
319
+ /**
320
+ * A `subscribe` operation (§17): its `output` is the snapshot schema
321
+ * and its emissions travel the stream binding. The compiler enforces
322
+ * the shape the wire requires (`GET`, `task: switch`, `idempotency:
323
+ * none`, the forced media); the pen writes what is declared.
324
+ * @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
325
+ * @returns {any}
326
+ * @example
327
+ * subscribe({ output: Board, policy: { stream: { resume: 'replay' } } });
328
+ */
329
+ export function subscribe(spec) { return operation('subscribe', spec); }
330
+
331
+ /**
332
+ * Whether a value is an operation declaration.
333
+ * @param {any} value
334
+ * @returns {boolean}
335
+ */
336
+ export function isOperation(value) {
337
+ return value !== null && typeof value === 'object' && value[OPERATION] === true;
338
+ }