@jarenjs/linq 0.56.0 → 0.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/flow/fsm.js CHANGED
@@ -22,7 +22,7 @@
22
22
  * and everything else §5.1 lists.
23
23
  */
24
24
 
25
- import { deepFreeze } from '@jarenjs/core/object';
25
+ import { deepFreeze, isJsonObject } from '@jarenjs/core/object';
26
26
 
27
27
  import { LinqBuildError } from '../errors.js';
28
28
  import { effectDescriptor, readEffects } from '../effect.js';
@@ -47,11 +47,6 @@ const ON_MEMBERS = Object.freeze(['payload']);
47
47
  /** The members `defineFsm()` takes. */
48
48
  const FSM_MEMBERS = Object.freeze(['initial', 'states', 'transitions', 'context']);
49
49
 
50
- /** @param {any} value */
51
- function isPlainObject(value) {
52
- return value !== null && typeof value === 'object' && !Array.isArray(value);
53
- }
54
-
55
50
  /**
56
51
  * A member set the pen knows, or `JL0101` naming the one it does not.
57
52
  * @param {any} spec
@@ -111,7 +106,7 @@ export function effect(run, props = undefined) {
111
106
  export function state(id, options = undefined) {
112
107
  const out = { id: readId(id, 'state()') };
113
108
  if (options !== undefined) {
114
- if (!isPlainObject(options)) {
109
+ if (!isJsonObject(options)) {
115
110
  throw new LinqBuildError('JL0101',
116
111
  `state() options are { entry?, exit?, final? }, got ${describeValue(options)}`);
117
112
  }
@@ -203,7 +198,7 @@ export function on(from, event = null, options = undefined) {
203
198
  entry.event = event;
204
199
  }
205
200
  if (options !== undefined) {
206
- if (!isPlainObject(options)) {
201
+ if (!isJsonObject(options)) {
207
202
  throw new LinqBuildError('JL0101',
208
203
  `on() options are { payload? }, got ${describeValue(options)}`);
209
204
  }
@@ -239,7 +234,7 @@ export function on(from, event = null, options = undefined) {
239
234
  * compileFsm(machine).step('draft', 'submit').state; // 'review'
240
235
  */
241
236
  export function defineFsm(spec) {
242
- if (!isPlainObject(spec)) {
237
+ if (!isJsonObject(spec)) {
243
238
  throw new LinqBuildError('JL0101',
244
239
  `defineFsm() takes { initial, states, transitions, context? }, got ${describeValue(spec)}`);
245
240
  }
@@ -266,7 +261,7 @@ export function defineFsm(spec) {
266
261
  states.push(entry);
267
262
  return;
268
263
  }
269
- if (!isPlainObject(entry) || entry[STATE] !== true) {
264
+ if (!isJsonObject(entry) || entry[STATE] !== true) {
270
265
  throw new LinqBuildError('JL0101',
271
266
  `defineFsm() states[${i}] is an id or state(id, options?), got ${describeValue(entry)}`,
272
267
  `/states/${i}`);
@@ -305,7 +300,7 @@ export function defineFsm(spec) {
305
300
  }
306
301
  const transitions = spec.transitions.map((declaration, i) => {
307
302
  const at = `/transitions/${i}`;
308
- const entry = isPlainObject(declaration) ? declaration[TRANSITION] : undefined;
303
+ const entry = isJsonObject(declaration) ? declaration[TRANSITION] : undefined;
309
304
  if (entry === undefined) {
310
305
  throw new LinqBuildError('JL0101',
311
306
  `defineFsm() transitions[${i}] is on(from, event?).to(state), got `
package/src/index.js CHANGED
@@ -13,4 +13,5 @@
13
13
  export { from, fromDocument, Sequence } from './sequence.js';
14
14
  export { fromAsync, AsyncSequence } from './async.js';
15
15
  export { createPushQueue } from './sources.js';
16
+ export { federate } from './federate.js';
16
17
  export { LinqBuildError, LinqRuntimeError, LINQ_CODES } from './errors.js';
package/src/jslt/rules.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * body's operators, the schema — is the compiler's.
14
14
  */
15
15
 
16
- import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
16
+ import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
17
17
  import { LinqBuildError } from '../errors.js';
18
18
  import { isSchemaBuilder, schemaOf } from '../schema/brand.js';
19
19
  import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
@@ -24,11 +24,6 @@ const DISPOSITIONS = ['share', 'fresh', 'error'];
24
24
  /** A JSON value, copied: the document is a value of its own. @param {any} v */
25
25
  const copy = (v) => JSON.parse(JSON.stringify(v));
26
26
 
27
- /** @param {any} value */
28
- function isPlainObject(value) {
29
- return value !== null && typeof value === 'object' && !Array.isArray(value);
30
- }
31
-
32
27
  /**
33
28
  * The `match` member (§3.1), or `undefined` for the unconditional rule.
34
29
  * @param {any} match
@@ -37,7 +32,7 @@ function isPlainObject(value) {
37
32
  function readMatch(match) {
38
33
  if (match === undefined || match === null) return undefined;
39
34
  if (typeof match === 'string') return match;
40
- if (!isPlainObject(match)) {
35
+ if (!isJsonObject(match)) {
41
36
  throw new LinqBuildError('JL0101',
42
37
  `rule() match is a JSONPath string or { path?, schema? }, got ${describeValue(match)}`,
43
38
  '/match');
@@ -97,7 +92,7 @@ function readBody(value) {
97
92
  export function rule(match, bodyOrFn, options = undefined) {
98
93
  const out = {};
99
94
  if (options !== undefined) {
100
- if (!isPlainObject(options)) {
95
+ if (!isJsonObject(options)) {
101
96
  throw new LinqBuildError('JL0101',
102
97
  `rule() options are { mode?, priority? }, got ${describeValue(options)}`);
103
98
  }
@@ -156,7 +151,7 @@ export function stylesheet(rules, options = undefined) {
156
151
  }
157
152
  const out = { $jslt: '0.1' };
158
153
  if (options !== undefined) {
159
- if (!isPlainObject(options)) {
154
+ if (!isJsonObject(options)) {
160
155
  throw new LinqBuildError('JL0101',
161
156
  `stylesheet() options are { unmatched?, modes? }, got ${describeValue(options)}`);
162
157
  }
@@ -170,7 +165,7 @@ export function stylesheet(rules, options = undefined) {
170
165
  out.unmatched = readDisposition(options.unmatched, 'unmatched');
171
166
  }
172
167
  if (options.modes !== undefined) {
173
- if (!isPlainObject(options.modes)) {
168
+ if (!isJsonObject(options.modes)) {
174
169
  throw new LinqBuildError('JL0101',
175
170
  `stylesheet() modes is { name: { unmatched } }, got ${describeValue(options.modes)}`,
176
171
  '/modes');
@@ -179,7 +174,7 @@ export function stylesheet(rules, options = undefined) {
179
174
  const modes = {};
180
175
  for (const name of Object.keys(options.modes)) {
181
176
  const mode = options.modes[name];
182
- if (!isPlainObject(mode) || Object.keys(mode).length !== 1 || mode.unmatched === undefined) {
177
+ if (!isJsonObject(mode) || Object.keys(mode).length !== 1 || mode.unmatched === undefined) {
183
178
  throw new LinqBuildError('JL0101',
184
179
  `stylesheet() mode '${name}' is { unmatched } and nothing else (JSLT-FORMAT §2.1)`,
185
180
  `/modes/${name}`);
@@ -191,7 +186,7 @@ export function stylesheet(rules, options = undefined) {
191
186
  }
192
187
  }
193
188
  out.rules = rules.map((r, i) => {
194
- if (!isPlainObject(r)) {
189
+ if (!isJsonObject(r)) {
195
190
  throw new LinqBuildError('JL0101',
196
191
  `stylesheet() rule ${i} is an object — rule(match, body) — got ${describeValue(r)}`,
197
192
  `/rules/${i}`);
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { canonicalizeJson } from '@jarenjs/json/canonical';
18
18
  import { hashContent } from '@jarenjs/core/string';
19
- import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
19
+ import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
20
20
 
21
21
  import { LinqBuildError } from '../errors.js';
22
22
  import { describeValue, requireJson } from '../json-boundary.js';
@@ -30,11 +30,6 @@ const HEAD_MEMBERS = ['$migration', 'id', 'from', 'to', 'note', 'steps'];
30
30
  /** A JSON value, copied: the document is a value of its own. @param {any} v */
31
31
  const copy = (v) => JSON.parse(JSON.stringify(v));
32
32
 
33
- /** @param {any} value */
34
- function isPlainObject(value) {
35
- return value !== null && typeof value === 'object' && !Array.isArray(value);
36
- }
37
-
38
33
  /**
39
34
  * The model without its `x-rename` hints. A hint is a PLANNING
40
35
  * instruction, not shape (MIGRATION-FORMAT §3): two models that differ
@@ -47,11 +42,11 @@ function withoutRenameHints(model) {
47
42
  for (const key of Object.keys(model)) setObjectMember(out, key, model[key]);
48
43
  for (const member of ['collections', 'entities']) {
49
44
  const declared = model[member];
50
- if (!isPlainObject(declared)) continue;
45
+ if (!isJsonObject(declared)) continue;
51
46
  const stripped = {};
52
47
  for (const name of Object.keys(declared)) {
53
48
  const spec = declared[name];
54
- if (isPlainObject(spec) && Object.hasOwn(spec, 'x-rename')) {
49
+ if (isJsonObject(spec) && Object.hasOwn(spec, 'x-rename')) {
55
50
  const { 'x-rename': _hint, ...rest } = spec;
56
51
  setObjectMember(stripped, name, rest);
57
52
  }
@@ -82,10 +77,10 @@ function shapeHashOf(model) {
82
77
  */
83
78
  function requireModel(model, what) {
84
79
  const doc = requireJson(model, what);
85
- if (!isPlainObject(doc) || doc.$model !== '0.1') {
80
+ if (!isJsonObject(doc) || doc.$model !== '0.1') {
86
81
  throw new LinqBuildError('JL0101',
87
82
  `${what} is a $model 0.1 document (defineModel(…), or its JSON), got `
88
- + `${isPlainObject(doc) ? 'an object without $model: \'0.1\'' : describeValue(model)}`);
83
+ + `${isJsonObject(doc) ? 'an object without $model: \'0.1\'' : describeValue(model)}`);
89
84
  }
90
85
  return doc;
91
86
  }
@@ -98,7 +93,7 @@ function requireModel(model, what) {
98
93
  function declaredNames(model) {
99
94
  const names = [];
100
95
  for (const member of ['entities', 'collections']) {
101
- if (isPlainObject(model[member])) names.push(...Object.keys(model[member]));
96
+ if (isJsonObject(model[member])) names.push(...Object.keys(model[member]));
102
97
  }
103
98
  return names;
104
99
  }
@@ -241,7 +236,7 @@ export class Migration {
241
236
  * @returns {Migration}
242
237
  */
243
238
  export function defineMigration(spec) {
244
- if (!isPlainObject(spec)) {
239
+ if (!isJsonObject(spec)) {
245
240
  throw new LinqBuildError('JL0101', 'defineMigration() takes { id, from, to, note? }');
246
241
  }
247
242
  for (const key of Object.keys(spec)) {
@@ -277,7 +272,7 @@ export function defineMigration(spec) {
277
272
  */
278
273
  export function fromPlanned(document, options = undefined) {
279
274
  const doc = copy(requireJson(document, 'fromPlanned() document'));
280
- if (!isPlainObject(doc) || doc.$migration !== MIGRATION_VERSION
275
+ if (!isJsonObject(doc) || doc.$migration !== MIGRATION_VERSION
281
276
  || typeof doc.id !== 'string' || doc.id === ''
282
277
  || typeof doc.from !== 'string' || typeof doc.to !== 'string' || !Array.isArray(doc.steps)) {
283
278
  throw new LinqBuildError('JL0101',
@@ -293,7 +288,7 @@ export function fromPlanned(document, options = undefined) {
293
288
  }
294
289
  let names = null;
295
290
  if (options !== undefined) {
296
- if (!isPlainObject(options)) {
291
+ if (!isJsonObject(options)) {
297
292
  throw new LinqBuildError('JL0101', `fromPlanned() options are { from?, to? }, got ${describeValue(options)}`);
298
293
  }
299
294
  for (const key of Object.keys(options)) {
@@ -13,6 +13,7 @@
13
13
  * `$it` inside the `$for` the format's own example writes.
14
14
  */
15
15
 
16
+ import { isJsonObject } from '@jarenjs/core/object';
16
17
  import { captureExpression } from '../expression.js';
17
18
  import { body } from '../jslt/body.js';
18
19
  import { describeValue, requireJson } from '../json-boundary.js';
@@ -28,11 +29,6 @@ const NO_PARAMS = new Set();
28
29
  /** A JSON value, copied: the document is a value of its own. @param {any} v */
29
30
  const copy = (v) => JSON.parse(JSON.stringify(v));
30
31
 
31
- /** @param {any} value */
32
- function isPlainObject(value) {
33
- return value !== null && typeof value === 'object' && !Array.isArray(value);
34
- }
35
-
36
32
  /**
37
33
  * A table name: an identifier, as the artifact's pattern admits.
38
34
  * @param {any} name
@@ -117,7 +113,7 @@ export function transformStep(name, spelling) {
117
113
  else if (Array.isArray(spelling)) {
118
114
  stylesheet = copy(requireJson(spelling, 'transform() rules'));
119
115
  }
120
- else if (isPlainObject(spelling) && spelling.$jslt === '0.1') {
116
+ else if (isJsonObject(spelling) && spelling.$jslt === '0.1') {
121
117
  for (const key of Object.keys(spelling)) {
122
118
  if (key !== '$jslt' && key !== 'rules') {
123
119
  throw new LinqBuildError('JL0102',
@@ -155,7 +151,7 @@ export function assertStep(name, spelling, options = undefined) {
155
151
  requireName(name, 'assert()');
156
152
  let expect;
157
153
  if (options !== undefined) {
158
- if (!isPlainObject(options)) {
154
+ if (!isJsonObject(options)) {
159
155
  throw new LinqBuildError('JL0101', `assert() options are { expect? }, got ${describeValue(options)}`);
160
156
  }
161
157
  for (const key of Object.keys(options)) {
@@ -211,10 +207,10 @@ export function deriveStep(name, columns) {
211
207
  */
212
208
  export function rawStep(step) {
213
209
  const raw = copy(requireJson(step, 'step()'));
214
- if (!isPlainObject(raw) || !STEP_KINDS.includes(raw.kind)) {
210
+ if (!isJsonObject(raw) || !STEP_KINDS.includes(raw.kind)) {
215
211
  throw new LinqBuildError('JL0101',
216
212
  `step() takes a migration step with a recognised kind (${STEP_KINDS.join(', ')}), got `
217
- + `${isPlainObject(raw) ? `kind ${describeValue(raw.kind)}` : describeValue(step)}`);
213
+ + `${isJsonObject(raw) ? `kind ${describeValue(raw.kind)}` : describeValue(step)}`);
218
214
  }
219
215
  const need = (member, ok) => {
220
216
  if (!ok) {
@@ -18,6 +18,10 @@ import { DOCUMENT_SCOPE, refuseStrandedRename } from './entity.js';
18
18
  /** The index options the grammar names beside `name` and `path`. */
19
19
  const INDEX_OPTIONS = new Set(['name', 'unique', 'derive', 'precision', 'dims', 'physical']);
20
20
 
21
+ /** The options an EXPRESSION index takes: it names the members it reads
22
+ * itself, so nothing that describes a member's storage belongs here. */
23
+ const EXPRESSION_OPTIONS = new Set(['name', 'unique']);
24
+
21
25
  /** The marker a collection spec carries so `defineModel` can tell it apart. */
22
26
  export const COLLECTION = Symbol.for('@jarenjs/linq/model-collection');
23
27
 
@@ -108,6 +112,104 @@ export function index(path, options = {}) {
108
112
  return Object.freeze(out);
109
113
  }
110
114
 
115
+ /**
116
+ * One node of an index expression, checked here so a pen mistake is a
117
+ * build error rather than a store refusal at open. The vocabulary is the
118
+ * model's, closed: a member path (a lambda or a JSONPath string), a JSON
119
+ * scalar, or a call to a function the HOST declares — this pen resolves
120
+ * no name, because arity and determinism are the store's to check
121
+ * against the declarations it was given.
122
+ * @param {any} node
123
+ * @param {string} what
124
+ * @param {number} depth
125
+ * @returns {any}
126
+ */
127
+ function expressionNode(node, what, depth) {
128
+ if (depth > 8)
129
+ throw new LinqBuildError('JL0101', `${what} nests deeper than 8`);
130
+ if (typeof node === 'function') return { member: capturePath(node, what) };
131
+ if (typeof node === 'string' && node.charCodeAt(0) === 0x24) return { member: node };
132
+ if (typeof node === 'number' || typeof node === 'boolean') return { value: node };
133
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) {
134
+ throw new LinqBuildError('JL0101',
135
+ `${what} takes a path lambda, a JSONPath string, a JSON scalar, or `
136
+ + '{ call, args } — an index expression is never SQL text');
137
+ }
138
+ if (node.member !== undefined) {
139
+ if (typeof node.member === 'function')
140
+ return { member: capturePath(node.member, `${what}.member`) };
141
+ if (typeof node.member !== 'string' || node.member.length === 0)
142
+ throw new LinqBuildError('JL0101', `${what}.member takes a path lambda or a JSONPath string`);
143
+ return { member: node.member };
144
+ }
145
+ if (node.value !== undefined) {
146
+ if (typeof node.value !== 'string' && typeof node.value !== 'number'
147
+ && typeof node.value !== 'boolean') {
148
+ throw new LinqBuildError('JL0101',
149
+ `${what}.value takes a JSON string, number or boolean`);
150
+ }
151
+ return { value: node.value };
152
+ }
153
+ if (typeof node.call !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(node.call))
154
+ throw new LinqBuildError('JL0101', `${what}.call names a function by identifier`);
155
+ const args = node.args ?? [];
156
+ if (!Array.isArray(args))
157
+ throw new LinqBuildError('JL0101', `${what}.args is an array`);
158
+ return {
159
+ call: node.call,
160
+ args: args.map((argument, i) => expressionNode(argument, `${what}.args[${i}]`, depth + 1)),
161
+ };
162
+ }
163
+
164
+ /**
165
+ * An index over a computed value: a closed expression over declared
166
+ * members, JSON scalars and functions the HOST declares deterministic.
167
+ *
168
+ * The function is resolved where the declarations are — at
169
+ * `openStore({ expressions })` — so a name this pen has never heard of
170
+ * is not an error here; a wrong ARITY and a missing declaration are
171
+ * `JD0004` at open, before any DDL. What this pen decides is the shape.
172
+ * @param {any} expression - a `{ call, args }` node, a path lambda, a
173
+ * JSONPath string, or a JSON scalar
174
+ * @param {{ name?: string, unique?: boolean }} [options]
175
+ * @returns {any} the frozen index document
176
+ */
177
+ export function expressionIndex(expression, options = {}) {
178
+ if (options === null || typeof options !== 'object' || Array.isArray(options))
179
+ throw new LinqBuildError('JL0101', 'expressionIndex() takes an options object');
180
+ for (const key of Object.keys(options)) {
181
+ if (!EXPRESSION_OPTIONS.has(key)) {
182
+ throw new LinqBuildError('JL0101',
183
+ `expressionIndex() does not take '${key}' — the options are name, unique`);
184
+ }
185
+ }
186
+ const resolved = expressionNode(expression, 'expressionIndex()', 0);
187
+ const out = {
188
+ name: options.name === undefined
189
+ ? expressionName(resolved)
190
+ : requireName(options.name, 'expressionIndex() name'),
191
+ expression: resolved,
192
+ };
193
+ if (options.unique !== undefined)
194
+ out.unique = requireJson(options.unique, 'expressionIndex() unique');
195
+ return Object.freeze(out);
196
+ }
197
+
198
+ /** The default name: `by_` + the outermost call and the members it
199
+ * reads, identifier-safe — the same stem the store's column takes.
200
+ * @param {any} node */
201
+ function expressionName(node) {
202
+ const members = [];
203
+ const walk = (current) => {
204
+ if (current.member !== undefined) members.push(current.member);
205
+ else if (current.call !== undefined) current.args.forEach(walk);
206
+ };
207
+ walk(node);
208
+ const parts = [node.call ?? 'x', ...members.map((path) =>
209
+ path.replace(/^\$\.?/, '').replace(/[^A-Za-z0-9]+/g, '_'))];
210
+ return `by_${parts.join('_').replace(/^_+|_+$/g, '')}`;
211
+ }
212
+
111
213
  /**
112
214
  * The key declaration: a pointer string, a captured member path, or
113
215
  * `null` (the store allocates; `identity` says how).
@@ -41,7 +41,7 @@ export const {
41
41
  /** The relation members: `rel.hasMany`, `rel.hasOne`, `rel.belongsToMany`. */
42
42
  export const rel = createRelations(EntityBuilder);
43
43
 
44
- export { collection, index } from './collection.js';
44
+ export { collection, index, expressionIndex } from './collection.js';
45
45
  export { defineModel } from './define.js';
46
46
  export { withEntity } from './entity.js';
47
47
  export { isSchemaBuilder, schemaOf, SCHEMA_BUILDER } from '../schema/brand.js';
@@ -7,8 +7,9 @@
7
7
  * the `Infer<>` of its output, the union of its declared error codes,
8
8
  * and whether its media makes it opaque. Every one of those is a
9
9
  * compile-time reading of the SAME builders the emitted document was
10
- * written from (D2), so the three wrappers below can type a client, a
11
- * handler table and an AI toolbox with no `generate` step.
10
+ * written from (D2), so the wrappers below can type a client (an HTTP
11
+ * one with its byte method), a handler table and an AI toolbox with no
12
+ * `generate` step.
12
13
  *
13
14
  * The agreement is a gate: `test/consumer/linq-contract.ts` proves
14
15
  * `ContractOf<>`'s members EQUAL to what
@@ -215,6 +216,13 @@ export type SubscribableOf<C> = Simplify<{
215
216
  ContractOf<C>[K]
216
217
  }>;
217
218
 
219
+ /** The opaque operations of a contract: what an HTTP client's `bytes`
220
+ * reaches — exactly the set §12.3's `ByteOperations` declares. */
221
+ export type OpaqueOf<C> = Simplify<{
222
+ [K in keyof ContractOf<C> as ContractOf<C>[K] extends { opaque: true } ? K : never]:
223
+ ContractOf<C>[K]
224
+ }>;
225
+
218
226
  // ——— the fixed outcome shapes (§10.1, rendered by §12.3) ———
219
227
 
220
228
  /** The correlation members of every outcome. A member a binding cannot
@@ -249,36 +257,104 @@ export type Failure = {
249
257
  retryable: boolean | null;
250
258
  };
251
259
 
252
- /** The per-request context a server binding hands a handler. */
253
- export interface HandlerContext {
260
+ /** The binding a handler context comes from. */
261
+ export type CarrierName = 'http' | 'port' | 'local';
262
+
263
+ /** The members every carrier's context shares; `host` is the host
264
+ * lifecycle's acquired value (§7.7), `null` by default. */
265
+ export interface HandlerContextBase<Host = null> {
254
266
  op: unknown;
255
267
  trace: string;
256
- method: string;
257
- path: string;
258
- params: Readonly<Record<string, string>>;
268
+ host: Host;
259
269
  headers: Readonly<Record<string, string>>;
260
- body: string | Uint8Array | null;
261
270
  signal: AbortSignal | null;
262
- idempotency: Readonly<{ key: string; scope: string }> | null;
263
271
  fail(code: string, params?: Record<string, unknown>, details?: unknown,
264
272
  options?: { retryable?: boolean }): Failure;
273
+ }
274
+
275
+ /** The HTTP binding's context: the request line, the raw body of an
276
+ * opaque operation, the idempotency key, and the entity-tag and status
277
+ * arms. */
278
+ export interface HttpHandlerContext<Host = null> extends HandlerContextBase<Host> {
279
+ carrier: 'http';
280
+ method: string;
281
+ path: string;
282
+ params: Readonly<Record<string, string>>;
283
+ body: string | Uint8Array | AsyncIterable<Uint8Array> | null;
284
+ idempotency: Readonly<{ key: string; scope: string }> | null;
265
285
  etag(tag: string, options?: { strong?: boolean }): void;
266
286
  status(status: number): void;
267
287
  }
268
288
 
269
- /** What `client.subscribe` takes (§19); every callback is optional. */
289
+ /** The port and local bindings' context: no request line, no body, no
290
+ * key, and no callable `etag` or `status` — spelled `null`, never
291
+ * omitted (§15, §16). */
292
+ export interface ChannelHandlerContext<Host = null, Carrier extends 'port' | 'local' = 'port' | 'local'>
293
+ extends HandlerContextBase<Host> {
294
+ carrier: Carrier;
295
+ method: null;
296
+ path: null;
297
+ params: null;
298
+ body: null;
299
+ idempotency: null;
300
+ etag: null;
301
+ status: null;
302
+ }
303
+
304
+ /** The per-request context a server binding hands a handler, selected
305
+ * by carrier: the HTTP context by default (`HandlerContext` is the
306
+ * shape it always was, plus `carrier` and `host`); a carrier union is a
307
+ * discriminated union — narrow on `carrier` before an HTTP-only
308
+ * member. */
309
+ export type HandlerContext<Host = null, Carrier extends CarrierName = 'http'> =
310
+ Extract<HttpHandlerContext<Host> | ChannelHandlerContext<Host, 'port'> | ChannelHandlerContext<Host, 'local'>, { carrier: Carrier }>;
311
+
312
+ /** Per-call options of `bytes` (§10.6): the members of `InvokeContext`
313
+ * that apply to an opaque call, plus the request body to send. */
314
+ export interface ByteContext {
315
+ signal?: AbortSignal; attempt?: unknown; headers?: Record<string, string>;
316
+ ifNoneMatch?: string; ifMatch?: string;
317
+ body?: string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> | null;
318
+ }
319
+
320
+ /** The success value of `bytes`: the status, the lowercase response
321
+ * headers, the response media and the LIVE body — a stream the caller
322
+ * reads; `null` when the response carries none. */
323
+ export type ByteResponse = {
324
+ status: number; headers: Record<string, string>; media: string | null;
325
+ body: ReadableStream<Uint8Array> | null;
326
+ };
327
+
328
+ /** The info beside every snapshot (§19): the event's seq, whether the
329
+ * stream resumed, whether the snapshot re-seeds a consumer whose cursor
330
+ * fell behind the server's retention (`reset`), and the server log's
331
+ * watermarks when it reported them. */
332
+ export interface SnapshotInfo {
333
+ seq: number; resumed: boolean; reset: boolean;
334
+ earliestAvailable: number | null; highWatermark: number | null;
335
+ }
336
+
337
+ /** What `client.subscribe` takes (§19); every callback is optional.
338
+ * `reconnect` opts the HTTP client into re-establishing the stream
339
+ * after a network loss, up to `max` further attempts from the last
340
+ * delivered seq; the budget spent, `onError` gets one `JC2097`. The
341
+ * port client has no network loss to reconnect from and ignores it. */
270
342
  export interface SubscribeHandlers<T> {
271
- onSnapshot?(value: T, info: { seq: number; resumed: boolean }): void;
343
+ onSnapshot?(value: T, info: SnapshotInfo): void;
272
344
  onPatch?(emission: { patch: readonly Json[]; seq: number }): void;
273
345
  onError?(outcome: Outcome<never>): void;
274
346
  onEnd?(info: { reason: string }): void;
275
347
  signal?: AbortSignal;
276
348
  lastSeq?: number;
349
+ reconnect?: { max: number };
277
350
  }
278
351
 
279
- /** A live subscription: `stop()` releases it (§19). */
352
+ /** A live subscription: `stop()` releases it; `lastSeq` is the last
353
+ * delivered seq (`null` before the first, the passed `lastSeq` until an
354
+ * event moves it) — what a re-entered `subscribe` passes (§19). */
280
355
  export interface Subscription {
281
356
  stop(): void;
357
+ readonly lastSeq: number | null;
282
358
  }
283
359
 
284
360
  // ——— the three identity wrappers ———
@@ -303,11 +379,25 @@ export interface TypedClient<C> {
303
379
  close(): void;
304
380
  }
305
381
 
306
- /** The handler table of a server binding, one handler per invokable operation. */
307
- export type TypedHandlerTable<C> = {
382
+ /** An HTTP client typed by one contract's operations: `TypedClient` plus
383
+ * `bytes` over the opaque ones, whose success owns a live stream. */
384
+ export interface TypedHttpClient<C> extends TypedClient<C> {
385
+ bytes<K extends keyof OpaqueOf<C>>(
386
+ op: K,
387
+ input: OpaqueOf<C>[K] extends { input: infer I } ? I : never,
388
+ ctx?: ByteContext,
389
+ ): Promise<Outcome<ByteResponse>>;
390
+ }
391
+
392
+ /** The handler table of a server binding, one handler per invokable
393
+ * operation. `Host` is what the host lifecycle's `acquire` hands the
394
+ * handler as `ctx.host`; `Carrier` selects the context — `'http'` by
395
+ * default, a union for a table several bindings share (narrow on
396
+ * `ctx.carrier` before an HTTP-only member). */
397
+ export type TypedHandlerTable<C, Host = null, Carrier extends CarrierName = 'http'> = {
308
398
  [K in keyof InvokableOf<C>]: (
309
399
  input: InvokableOf<C>[K] extends { input: infer I } ? I : never,
310
- ctx: HandlerContext,
400
+ ctx: HandlerContext<Host, Carrier>,
311
401
  ) => (InvokableOf<C>[K] extends { output: infer O } ? O : never)
312
402
  | Failure
313
403
  | Promise<(InvokableOf<C>[K] extends { output: infer O } ? O : never) | Failure>;
@@ -360,10 +450,17 @@ export function defineContract<O extends Record<string, AnyOperation | ({
360
450
  /** Bind a contract client to the contract that types it. Identity at runtime. */
361
451
  export function typedClient<C extends Contract<any>>(client: unknown, contract: C): TypedClient<C>;
362
452
 
453
+ /** Bind an HTTP client (`openHttpClient`) to the contract that types it:
454
+ * `typedClient` plus `bytes` over the opaque operations. Identity at
455
+ * runtime; a local or port client has no `bytes` and takes `typedClient`. */
456
+ export function typedHttpClient<C extends Contract<any>>(client: unknown, contract: C): TypedHttpClient<C>;
457
+
363
458
  /** Bind a handler table to the contract it serves. Identity at runtime;
364
- * a missing or misspelled operation is a type error. */
365
- export function typedHandlers<C extends Contract<any>>(
366
- contract: C, handlers: TypedHandlerTable<C>): TypedHandlerTable<C>;
459
+ * a missing or misspelled operation is a type error. Name `Host` and
460
+ * `Carrier` explicitly for a table whose context is not the HTTP default:
461
+ * `typedHandlers<typeof Shop, { db: Client }, 'http' | 'port'>(Shop, …)`. */
462
+ export function typedHandlers<C extends Contract<any>, Host = null, Carrier extends CarrierName = 'http'>(
463
+ contract: C, handlers: TypedHandlerTable<C, Host, Carrier>): TypedHandlerTable<C, Host, Carrier>;
367
464
 
368
465
  /** Bind `contractTools`' output to the contract that types it. Identity
369
466
  * at runtime. */