@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,82 @@
1
+ //@ts-check
2
+ /**
3
+ * @file One capture over a value rooted at `$` with named externals —
4
+ * the shared piece under every pen's query-valued member: a schema
5
+ * `check()` rule (`root`, `path`), a model `compute()` default (no
6
+ * externals), a JSLT rule body (`root`, `path` and the parameters the
7
+ * body declares). The value proxy is rooted at `$`, the instance the
8
+ * query is evaluated at; the externals proxy answers exactly the named
9
+ * expressions and refuses any other name at build time (`JL0104`),
10
+ * earlier than the engine's own error and with the same meaning. One
11
+ * implementation: a pen adds an externals list, never a capture entry
12
+ * point.
13
+ */
14
+
15
+ import { captureExpression } from './expression.js';
16
+ import { LinqBuildError } from './errors.js';
17
+
18
+ /** No parameters are declared for a rule: `p.x` cannot appear. */
19
+ const NO_PARAMS = new Set();
20
+
21
+ /** The indefinite article for a method name (`edge() select` → `an`). */
22
+ const article = (/** @type {string} */ what) =>
23
+ ('aeiou'.includes(what.charAt(0).toLowerCase()) ? 'an' : 'a');
24
+
25
+ /**
26
+ * The externals proxy: exactly the named expressions the capture handed
27
+ * us; anything else has nothing to bind to.
28
+ *
29
+ * The message states only what this file KNOWS — how many externals the
30
+ * evaluator binds and which — because that is all it can know: WHERE the
31
+ * query is evaluated differs per pen (a schema rule over the instance, a
32
+ * model default over the document being written, a flow guard over the
33
+ * step scope at step time), and naming one pen's answer here made the
34
+ * message contradict its own advice for every other pen. A caller that
35
+ * has a scope to name passes it as `advice`.
36
+ * @param {string} what - the method, for the message
37
+ * @param {readonly string[]} names - the externals bound
38
+ * @param {readonly any[]} proxies - one expression proxy per name
39
+ * @param {((name: string) => string) | undefined} advice - the fix, when one exists
40
+ * @param {string} noun - what this pen calls the callback
41
+ */
42
+ function externalsProxy(what, names, proxies, advice, noun) {
43
+ return new Proxy(Object.freeze({}), {
44
+ get(_target, prop) {
45
+ if (typeof prop === 'symbol') return undefined;
46
+ const index = names.indexOf(/** @type {string} */ (prop));
47
+ if (index !== -1) return proxies[index];
48
+ const bound = names.length === 0
49
+ ? 'no externals at all'
50
+ : `exactly ${names.length === 1 ? 'one external' : `${names.length} externals`}, `
51
+ + `${names.map((n) => `'${n}'`).join(' and ')}`;
52
+ throw new LinqBuildError('JL0104',
53
+ `${article(what)} ${what} ${noun} cannot bind '${prop}' — its query evaluates with `
54
+ + `${bound}; anything else has nothing to bind to`
55
+ + (advice === undefined ? '' : advice(prop)));
56
+ },
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Capture one rule into a query document over a value rooted at `$`,
62
+ * with the named externals as the second argument.
63
+ * @param {string} what - the method, for the message
64
+ * @param {readonly string[]} externals - the externals the evaluator binds
65
+ * @param {(value: any, externals: any) => any} rule
66
+ * @param {{ advice?: (name: string) => string, fold?: boolean, noun?: string }} [options] -
67
+ * `advice`: appended to the `JL0104` message — how an unbound name could
68
+ * be declared where it can, or what the callback's own argument already
69
+ * is; `fold`: whether a pure data tree folds into one `$const` (the
70
+ * default) or is spelled as a constructor tree; `noun`: what this pen
71
+ * calls the callback (`'rule'` by default; a pen with no rules passes
72
+ * its own word)
73
+ * @returns {any} the captured query expression (plain JSON)
74
+ */
75
+ export function captureQuery(what, externals, rule, options = {}) {
76
+ const noun = options.noun ?? 'rule';
77
+ return captureExpression(
78
+ (value, ...bound) => rule(value, externalsProxy(what, externals, bound, options.advice, noun)),
79
+ [{ doc: '$', pathable: true }, ...externals],
80
+ NO_PARAMS,
81
+ options.fold !== false);
82
+ }
@@ -1,7 +1,7 @@
1
1
  //@ts-check
2
2
  /**
3
3
  * @file `mapAsync` — the ONE explicit bounded-concurrency boundary
4
- * (LINQ-FORMAT.md §11). Element-wise asynchronous work (an HTTP call, a
4
+ * (QUERY-PEN.md §11). Element-wise asynchronous work (an HTTP call, a
5
5
  * model call, a file read per row) happens here and nowhere else: there
6
6
  * is no parallel universe of `selectAwait`-shaped operators, and a
7
7
  * per-element async *predicate* is `mapAsync` then `where`, by design.
@@ -54,6 +54,18 @@ export async function* applyMapAsync(items, fn, opts) {
54
54
  /** The pipeline's own failure, so source cleanup cannot displace it.
55
55
  * @type {{ reason: any } | null} */
56
56
  let failure = null;
57
+ /** A callback failure may close while a pull is pending; finally awaits
58
+ * that same close, so cancellation reaches the producer exactly once.
59
+ * @type {Promise<any> | null} */
60
+ let closing = null;
61
+ const closeSource = () => {
62
+ if (closing === null) {
63
+ closing = Promise.resolve().then(() => typeof items.return === 'function'
64
+ ? items.return(undefined) : undefined);
65
+ closing.catch(() => {}); // the finally block reports cleanup failures
66
+ }
67
+ return closing;
68
+ };
57
69
 
58
70
  try {
59
71
  if (opts.mode === 'concat' || (opts.mode === 'parallel' && opts.concurrency === 1)) {
@@ -118,25 +130,45 @@ export async function* applyMapAsync(items, fn, opts) {
118
130
  // parallel: a sliding window of `concurrency` in-flight tasks
119
131
  const window = [];
120
132
  let sourceDone = false;
133
+ // the first rejection anywhere in the window aborts every sibling
134
+ // and stops the pull, at once — not when it reaches the head of the
135
+ // ordered window: the rejection itself still surfaces in order
136
+ let rejected = false;
137
+ let stopPull;
138
+ /** @type {Promise<IteratorResult<any>>} */
139
+ const stoppedPull = new Promise((resolve) => {
140
+ stopPull = () => resolve({ done: true, value: undefined });
141
+ });
142
+ const taskFailed = () => {
143
+ if (rejected) return;
144
+ rejected = true;
145
+ stopPull();
146
+ controller.abort();
147
+ closeSource();
148
+ };
149
+ // An idle producer must not hide a callback's failure. The race also
150
+ // observes a pending next() that rejects after cancellation won.
151
+ const next = () => Promise.race([items.next(), stoppedPull]);
121
152
  const pull = async () => {
122
- const step = await items.next();
153
+ const step = await next();
123
154
  if (step.done) { sourceDone = true; return null; }
155
+ if (rejected) return null; // a sibling failed while this pull awaited
124
156
  const promise = Promise.resolve(fn(step.value, signal));
125
157
  // a rejection must wait its turn in the ordered window without
126
158
  // firing unhandledRejection while an earlier task is in flight
127
- promise.catch(() => {});
159
+ promise.catch(taskFailed);
128
160
  return { promise };
129
161
  };
130
162
  if (opts.ordered) {
131
163
  // completion order = source order; the window buffers at most
132
164
  // `concurrency` results (the documented buffering cost)
133
- while (!sourceDone && window.length < opts.concurrency) {
165
+ while (!sourceDone && !rejected && window.length < opts.concurrency) {
134
166
  const task = await pull();
135
167
  if (task !== null) window.push(task.promise);
136
168
  }
137
169
  while (window.length > 0) {
138
170
  const value = await window.shift();
139
- if (!sourceDone) {
171
+ if (!sourceDone && !rejected) {
140
172
  const task = await pull();
141
173
  if (task !== null) window.push(task.promise);
142
174
  }
@@ -148,19 +180,24 @@ export async function* applyMapAsync(items, fn, opts) {
148
180
  let nextId = 0;
149
181
  const inflight = new Map();
150
182
  const start = async () => {
151
- const step = await items.next();
183
+ const step = await next();
152
184
  if (step.done) { sourceDone = true; return; }
185
+ if (rejected) return; // a task failed while this source pull awaited
153
186
  const id = nextId++;
154
- inflight.set(id, Promise.resolve(fn(step.value, signal)).then(
187
+ const promise = Promise.resolve(fn(step.value, signal)).then(
155
188
  (value) => ({ id, value }),
156
189
  // the task's identity rides in an internal ENVELOPE, never on the
157
190
  // rejection value. Stamping the value mutated whatever the handler
158
191
  // threw: a frozen error became a different TypeError, a thrown
159
192
  // string came back boxed, an ordinary error grew a private
160
193
  // property, and a hostile proxy could break normalization outright.
161
- (error) => { throw new TaskFailure(id, error); }));
194
+ (error) => { throw new TaskFailure(id, error); });
195
+ // Observe failures immediately, including while the next source
196
+ // pull is pending or downstream has stopped consuming the window.
197
+ promise.catch(taskFailed);
198
+ inflight.set(id, promise);
162
199
  };
163
- while (!sourceDone && inflight.size < opts.concurrency) await start();
200
+ while (!sourceDone && !rejected && inflight.size < opts.concurrency) await start();
164
201
  while (inflight.size > 0) {
165
202
  let settled;
166
203
  try {
@@ -174,7 +211,7 @@ export async function* applyMapAsync(items, fn, opts) {
174
211
  throw err;
175
212
  }
176
213
  inflight.delete(settled.id);
177
- if (!sourceDone) await start();
214
+ if (!sourceDone && !rejected) await start();
178
215
  yield settled.value;
179
216
  }
180
217
  }
@@ -191,7 +228,7 @@ export async function* applyMapAsync(items, fn, opts) {
191
228
  // itself succeeded. Composed as a rejection the `await` adopts,
192
229
  // because a `throw` here would be the very substitution this
193
230
  // avoids: it discards whatever completion the block was carrying.
194
- await Promise.resolve(items.return(undefined)).then(undefined,
231
+ await closeSource().then(undefined,
195
232
  (cleanupError) => Promise.reject(failure === null
196
233
  ? cleanupError
197
234
  : new AggregateError([failure.reason, cleanupError],
@@ -0,0 +1,282 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `defineContract({ id?, version?, compat? }, operations)` — one
4
+ * `$contract` 0.1 document, deep-frozen, that `compileContract` takes
5
+ * unchanged.
6
+ *
7
+ * Two rules make the pen's document and its own public projection
8
+ * comparable member for member. First, the member ORDER is §12.1's, the
9
+ * normative one the revision hashes: root `$contract, id, version,
10
+ * compat, $defs, operations`; operation `kind, input, output, errors,
11
+ * policy, http, doc`; error `status, schema`; http `method, path, in,
12
+ * body, status, media`; `$defs` in first-reference order. Second, no
13
+ * DEFAULT is written: `describe()` marks a default inferred, and a pen
14
+ * that wrote them would turn every default into a declaration and move
15
+ * the revision for nothing.
16
+ *
17
+ * Every `named()` builder any operation reaches becomes one entry of the
18
+ * contract's own `$defs`, referenced `#/$defs/<name>` — the schema pen's
19
+ * hoisting walk over several roots instead of one. Nothing here imports
20
+ * `@jarenjs/contract`: the compiler stays the only judge of what the
21
+ * document means.
22
+ */
23
+
24
+ import { cloneJson, deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
25
+
26
+ import { LinqBuildError } from '../errors.js';
27
+ import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
28
+ import { isSchemaBuilder } from '../schema/brand.js';
29
+ import { createHoist, emitInto, hoistedDefs } from '../schema/emit.js';
30
+ import { http as httpBinding, isHttpBinding } from './http.js';
31
+ import { checkOperation, emitPolicy, isOperation, KINDS, readErrors } from './operation.js';
32
+
33
+ const CONTRACT_VERSION = '0.1';
34
+ const HEAD_MEMBERS = ['id', 'version', 'compat'];
35
+
36
+ /** `[A-Za-z_][A-Za-z0-9_-]*` — a contract id (§2.1). */
37
+ const ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
38
+
39
+ /**
40
+ * One schema position — `input`, `output`, an error's `schema` — as the
41
+ * document carries it: a builder emitted into the shared `$defs`
42
+ * context, or a JSON Schema written by hand, copied.
43
+ * @param {any} value
44
+ * @param {any} ctx - the hoisting context
45
+ * @param {string} at
46
+ * @returns {any}
47
+ */
48
+ function schemaAt(value, ctx, at) {
49
+ if (isSchemaBuilder(value)) return emitInto(value, ctx, at);
50
+ const json = requireJson(value, `the schema at ${at}`);
51
+ if (typeof json !== 'boolean' && !isJsonObject(json)) {
52
+ throw new LinqBuildError('JL0101',
53
+ `a schema is an object, true or false — got ${describeValue(value)}`, at);
54
+ }
55
+ return cloneJson(json);
56
+ }
57
+
58
+ /**
59
+ * The kind and spec of one declared operation: a `read()`/`command()`/
60
+ * `subscribe()` declaration, or the same members written by hand with a
61
+ * `kind`.
62
+ *
63
+ * The hand-written form is checked HERE by the same predicate the three
64
+ * declaration functions run, against its own position in the document.
65
+ * It is a second door into one emitter, and a door with no check behind
66
+ * it is worse than no door: an unknown member would be dropped in
67
+ * silence (the pen never sees it again, and the compiler never sees it
68
+ * at all), and a non-string `doc` would be written into a document
69
+ * `jaren-contract` refuses.
70
+ * @param {any} declared
71
+ * @param {string} at
72
+ * @returns {{ kind: string, spec: any }}
73
+ */
74
+ function declarationOf(declared, at) {
75
+ if (isOperation(declared)) return declared;
76
+ if (!isJsonObject(declared)) {
77
+ throw new LinqBuildError('JL0101',
78
+ `an operation is read(), command() or subscribe() — got ${describeValue(declared)}`, at);
79
+ }
80
+ if (!KINDS.includes(declared.kind)) {
81
+ throw new LinqBuildError('JL0102',
82
+ `an operation kind is one of ${KINDS.join(', ')} — got ${describeValue(declared.kind)}`,
83
+ `${at}/kind`);
84
+ }
85
+ const { kind, ...spec } = declared;
86
+ checkOperation(kind, spec, at);
87
+ return { kind, spec };
88
+ }
89
+
90
+ /**
91
+ * Emit one operation, in §12.1's member order, declared members only.
92
+ * @param {string} id
93
+ * @param {any} declared
94
+ * @param {any} ctx - the hoisting context
95
+ * @returns {any}
96
+ */
97
+ function emitOperation(id, declared, ctx) {
98
+ const at = `/operations/${id}`;
99
+ const { kind, spec } = declarationOf(declared, at);
100
+ const out = { kind };
101
+ if (spec.input !== undefined) out.input = schemaAt(spec.input, ctx, `${at}/input`);
102
+ out.output = schemaAt(spec.output, ctx, `${at}/output`);
103
+ if (spec.errors !== undefined) {
104
+ const declaredErrors = readErrors(spec.errors, `${at}/errors`);
105
+ const errors = {};
106
+ for (const [code, entry] of declaredErrors) {
107
+ const e = {};
108
+ if (entry.status !== undefined) e.status = entry.status;
109
+ if (entry.schema !== undefined) {
110
+ e.schema = schemaAt(entry.schema, ctx, `${at}/errors/${code}/schema`);
111
+ }
112
+ setObjectMember(errors, code, e);
113
+ }
114
+ out.errors = errors;
115
+ }
116
+ if (spec.policy !== undefined) out.policy = emitPolicy(spec.policy, `${at}/policy`);
117
+ if (spec.http !== undefined) {
118
+ out.http = isHttpBinding(spec.http) ? { ...spec.http } : { ...httpBinding(spec.http) };
119
+ }
120
+ if (spec.doc !== undefined) out.doc = spec.doc;
121
+ return out;
122
+ }
123
+
124
+ /**
125
+ * The contract under construction: frozen on creation, its document
126
+ * assembled once. `document` and `toJSON()` are the same deep-frozen
127
+ * `$contract` 0.1 JSON; the phantom the declarations carry is what
128
+ * `ContractOf<>` reads.
129
+ */
130
+ export class Contract {
131
+ #document;
132
+
133
+ /**
134
+ * @param {any} document - the assembled document
135
+ */
136
+ constructor(document) {
137
+ this.#document = document;
138
+ Object.freeze(this);
139
+ }
140
+
141
+ /** The deep-frozen `$contract` 0.1 document. */
142
+ get document() { return this.#document; }
143
+
144
+ /** The document — what `JSON.stringify` writes. @returns {any} */
145
+ toJSON() { return this.#document; }
146
+ }
147
+
148
+ /**
149
+ * Write a `$contract` 0.1 document.
150
+ *
151
+ * @param {any} meta - `{ id?, version?, compat? }` (§2.1)
152
+ * @param {any} operations - operation id → `read()` / `command()` / `subscribe()`
153
+ * @returns {Contract} the contract, its `document` deep-frozen JSON
154
+ * @throws {LinqBuildError} `JL0101` a value the pen cannot spell;
155
+ * `JL0102` a kind or path template the format reserves; `JL0103` two
156
+ * distinct builders under one `$defs` name, or a `ref()` nothing defines
157
+ * @example
158
+ * const shop = defineContract({ id: 'shop', version: '5' }, {
159
+ * 'catalog.load': read({ output: Catalog, http: http({ method: 'GET', path: '/api/catalog' }) }),
160
+ * });
161
+ * compileContract(shop.document).ids; // ['catalog.load']
162
+ */
163
+ export function defineContract(meta, operations) {
164
+ if (!isJsonObject(meta)) {
165
+ throw new LinqBuildError('JL0101',
166
+ `defineContract() takes ({ id?, version?, compat? }, operations), got `
167
+ + `${describeValue(meta)} as its first argument`);
168
+ }
169
+ for (const key of Object.keys(meta)) {
170
+ if (!HEAD_MEMBERS.includes(key)) {
171
+ throw new LinqBuildError('JL0101',
172
+ `defineContract() does not take '${key}' — the head is ${HEAD_MEMBERS.join(', ')}; `
173
+ + 'everything else is an operation', `/${key}`);
174
+ }
175
+ }
176
+ if (meta.id !== undefined && (typeof meta.id !== 'string' || !ID.test(meta.id))) {
177
+ throw new LinqBuildError('JL0101',
178
+ `defineContract() id matches [A-Za-z_][A-Za-z0-9_-]*, got ${describeValue(meta.id)}`, '/id');
179
+ }
180
+ if (meta.version !== undefined && typeof meta.version !== 'string') {
181
+ throw new LinqBuildError('JL0101',
182
+ `defineContract() version is a string, got ${describeValue(meta.version)}`, '/version');
183
+ }
184
+ if (meta.compat !== undefined
185
+ && (!Array.isArray(meta.compat) || meta.compat.some((v) => typeof v !== 'string'))) {
186
+ throw new LinqBuildError('JL0101',
187
+ 'defineContract() compat is an array of peer version strings', '/compat');
188
+ }
189
+ if (!isJsonObject(operations)) {
190
+ throw new LinqBuildError('JL0101',
191
+ `defineContract() operations is a plain object of id → operation, got `
192
+ + `${describeValue(operations)}`, '/operations');
193
+ }
194
+ requireNameMap(operations, 'defineContract() operations', '/operations');
195
+ const ids = Object.keys(operations);
196
+ if (ids.length === 0) {
197
+ throw new LinqBuildError('JL0101',
198
+ 'defineContract() needs at least one operation', '/operations');
199
+ }
200
+
201
+ const ctx = createHoist();
202
+ const emitted = ids.map((id) => [id, emitOperation(id, operations[id], ctx)]);
203
+ const defs = hoistedDefs(ctx);
204
+
205
+ const out = { $contract: CONTRACT_VERSION };
206
+ if (meta.id !== undefined) out.id = meta.id;
207
+ if (meta.version !== undefined) out.version = meta.version;
208
+ if (meta.compat !== undefined) out.compat = meta.compat.slice();
209
+ if (defs !== null) out.$defs = defs;
210
+ const ops = {};
211
+ for (const [id, op] of emitted) setObjectMember(ops, id, op);
212
+ out.operations = ops;
213
+ return new Contract(deepFreeze(out));
214
+ }
215
+
216
+ /**
217
+ * Bind a contract client to the contract that types it. Identity at
218
+ * runtime: `invoke`, `url` and `subscribe` are narrowed by the pen's
219
+ * phantoms, and nothing is added to the binding.
220
+ * @template C
221
+ * @param {any} client - any contract client (local, http, port)
222
+ * @param {C} contract - the pen's contract; the type argument only
223
+ * @returns {any}
224
+ * @example
225
+ * const api = typedClient(openLocalClient(compileContract(shop.document), handlers), shop);
226
+ * const outcome = await api.invoke('product.save', { id: 1, revision: 4, product });
227
+ */
228
+ export function typedClient(client, contract) {
229
+ void contract;
230
+ return client;
231
+ }
232
+
233
+ /**
234
+ * Bind an HTTP client to the contract that types it: `typedClient` plus
235
+ * `bytes` over the opaque operations. Identity at runtime — a local or
236
+ * port client carries no `bytes` and takes `typedClient`.
237
+ * @template C
238
+ * @template T
239
+ * @param {T} client - an `openHttpClient` client
240
+ * @param {C} contract - the pen's contract; the type argument only
241
+ * @returns {T}
242
+ * @example
243
+ * const api = typedHttpClient(openHttpClient(compileContract(shop.document), { baseUrl }), shop);
244
+ * const outcome = await api.bytes('image.bytes', { id: 3 }); // ok: { status, headers, media, body: ReadableStream }
245
+ */
246
+ export function typedHttpClient(client, contract) {
247
+ void contract;
248
+ return client;
249
+ }
250
+
251
+ /**
252
+ * Bind a handler table to the contract it serves. Identity at runtime:
253
+ * the table is checked against the operation ids and each handler's
254
+ * input and output typed by the pen's phantoms.
255
+ * @template C
256
+ * @template H
257
+ * @param {C} contract - the pen's contract; the type argument only
258
+ * @param {H} handlers - operation id → handler
259
+ * @returns {H}
260
+ * @example
261
+ * serveHttp(compiled, typedHandlers(shop, { 'catalog.load': () => catalog }));
262
+ */
263
+ export function typedHandlers(contract, handlers) {
264
+ void contract;
265
+ return handlers;
266
+ }
267
+
268
+ /**
269
+ * Bind `contractTools`' output to the contract that types it. Identity
270
+ * at runtime: each tool's `name` and `execute` argument are narrowed by
271
+ * the pen's phantoms.
272
+ * @template C
273
+ * @param {any} tools - what `contractTools(compiled, client)` answered
274
+ * @param {C} contract - the pen's contract; the type argument only
275
+ * @returns {any}
276
+ * @example
277
+ * for (const tool of typedTools(contractTools(compiled, client), shop)) toolbox.add(tool);
278
+ */
279
+ export function typedTools(tools, contract) {
280
+ void contract;
281
+ return tools;
282
+ }