@prisma-next/framework-components 0.5.0-dev.6 → 0.5.0-dev.61

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 (92) hide show
  1. package/README.md +63 -3
  2. package/dist/authoring.d.mts +2 -2
  3. package/dist/authoring.mjs +2 -121
  4. package/dist/codec-BUBJeOk4.d.mts +168 -0
  5. package/dist/codec-BUBJeOk4.d.mts.map +1 -0
  6. package/dist/codec.d.mts +49 -2
  7. package/dist/codec.d.mts.map +1 -0
  8. package/dist/codec.mjs +68 -3
  9. package/dist/codec.mjs.map +1 -1
  10. package/dist/components.d.mts +3 -1
  11. package/dist/components.mjs +1 -1
  12. package/dist/control.d.mts +110 -71
  13. package/dist/control.d.mts.map +1 -1
  14. package/dist/control.mjs +47 -45
  15. package/dist/control.mjs.map +1 -1
  16. package/dist/emission-types-DzNgwiFC.d.mts +39 -0
  17. package/dist/emission-types-DzNgwiFC.d.mts.map +1 -0
  18. package/dist/emission.d.mts +2 -2
  19. package/dist/execution.d.mts +7 -5
  20. package/dist/execution.d.mts.map +1 -1
  21. package/dist/execution.mjs +3 -3
  22. package/dist/execution.mjs.map +1 -1
  23. package/dist/{framework-authoring-D1-JZ37B.d.mts → framework-authoring-DJbiXhmf.d.mts} +41 -12
  24. package/dist/framework-authoring-DJbiXhmf.d.mts.map +1 -0
  25. package/dist/framework-authoring-gi_BJlNO.mjs +206 -0
  26. package/dist/framework-authoring-gi_BJlNO.mjs.map +1 -0
  27. package/dist/{framework-components-EJXe-pum.d.mts → framework-components-BVqm1I48.d.mts} +45 -55
  28. package/dist/framework-components-BVqm1I48.d.mts.map +1 -0
  29. package/dist/{framework-components-C8ZhSwXe.mjs → framework-components-BsWST1Rn.mjs} +2 -2
  30. package/dist/framework-components-BsWST1Rn.mjs.map +1 -0
  31. package/dist/psl-ast-BlDveJZ4.d.mts +159 -0
  32. package/dist/psl-ast-BlDveJZ4.d.mts.map +1 -0
  33. package/dist/psl-ast.d.mts +2 -0
  34. package/dist/psl-ast.mjs +1 -0
  35. package/dist/runtime.d.mts +346 -19
  36. package/dist/runtime.d.mts.map +1 -1
  37. package/dist/runtime.mjs +256 -4
  38. package/dist/runtime.mjs.map +1 -1
  39. package/dist/{types-import-spec-C4sc7wbb.d.mts → types-import-spec-CPhrNJIV.d.mts} +2 -2
  40. package/dist/types-import-spec-CPhrNJIV.d.mts.map +1 -0
  41. package/package.json +7 -4
  42. package/src/control/control-capabilities.ts +71 -0
  43. package/src/{control-descriptors.ts → control/control-descriptors.ts} +7 -7
  44. package/src/{control-instances.ts → control/control-instances.ts} +6 -6
  45. package/src/{control-migration-types.ts → control/control-migration-types.ts} +57 -60
  46. package/src/control/control-operation-preview.ts +23 -0
  47. package/src/{control-stack.ts → control/control-stack.ts} +77 -94
  48. package/src/control/emission-types.ts +49 -0
  49. package/src/control/psl-ast.ts +193 -0
  50. package/src/{execution-descriptors.ts → execution/execution-descriptors.ts} +7 -7
  51. package/src/{execution-instances.ts → execution/execution-instances.ts} +1 -1
  52. package/src/{execution-requirements.ts → execution/execution-requirements.ts} +1 -1
  53. package/src/execution/query-plan.ts +53 -0
  54. package/src/execution/race-against-abort.ts +85 -0
  55. package/src/execution/run-with-middleware.ts +132 -0
  56. package/src/execution/runtime-core.ts +133 -0
  57. package/src/execution/runtime-error.ts +83 -0
  58. package/src/execution/runtime-middleware.ts +182 -0
  59. package/src/exports/authoring.ts +5 -2
  60. package/src/exports/codec.ts +27 -2
  61. package/src/exports/components.ts +2 -2
  62. package/src/exports/control.ts +26 -13
  63. package/src/exports/emission.ts +2 -2
  64. package/src/exports/execution.ts +5 -5
  65. package/src/exports/psl-ast.ts +1 -0
  66. package/src/exports/runtime.ts +17 -5
  67. package/src/shared/codec-descriptor.ts +87 -0
  68. package/src/shared/codec-types.ts +79 -0
  69. package/src/shared/codec.ts +80 -0
  70. package/src/shared/column-spec.ts +83 -0
  71. package/src/{framework-authoring.ts → shared/framework-authoring.ts} +202 -23
  72. package/src/{framework-components.ts → shared/framework-components.ts} +22 -48
  73. package/src/{mutation-default-types.ts → shared/mutation-default-types.ts} +22 -2
  74. package/dist/authoring.mjs.map +0 -1
  75. package/dist/codec-types-B58nCJiu.d.mts +0 -40
  76. package/dist/codec-types-B58nCJiu.d.mts.map +0 -1
  77. package/dist/emission-types-BPAALJbF.d.mts +0 -24
  78. package/dist/emission-types-BPAALJbF.d.mts.map +0 -1
  79. package/dist/framework-authoring-D1-JZ37B.d.mts.map +0 -1
  80. package/dist/framework-components-C8ZhSwXe.mjs.map +0 -1
  81. package/dist/framework-components-EJXe-pum.d.mts.map +0 -1
  82. package/dist/types-import-spec-C4sc7wbb.d.mts.map +0 -1
  83. package/src/codec-types.ts +0 -46
  84. package/src/control-capabilities.ts +0 -34
  85. package/src/emission-types.ts +0 -28
  86. package/src/runtime-error.ts +0 -39
  87. package/src/runtime-middleware.ts +0 -83
  88. /package/src/{control-result-types.ts → control/control-result-types.ts} +0 -0
  89. /package/src/{control-schema-view.ts → control/control-schema-view.ts} +0 -0
  90. /package/src/{async-iterable-result.ts → execution/async-iterable-result.ts} +0 -0
  91. /package/src/{execution-stack.ts → execution/execution-stack.ts} +0 -0
  92. /package/src/{types-import-spec.ts → shared/types-import-spec.ts} +0 -0
@@ -1,3 +1,10 @@
1
+ import type {
2
+ AdapterDescriptor,
3
+ DriverDescriptor,
4
+ ExtensionDescriptor,
5
+ FamilyDescriptor,
6
+ TargetDescriptor,
7
+ } from '../shared/framework-components';
1
8
  import type {
2
9
  RuntimeAdapterInstance,
3
10
  RuntimeDriverInstance,
@@ -6,13 +13,6 @@ import type {
6
13
  RuntimeTargetInstance,
7
14
  } from './execution-instances';
8
15
  import type { ExecutionStack } from './execution-stack';
9
- import type {
10
- AdapterDescriptor,
11
- DriverDescriptor,
12
- ExtensionDescriptor,
13
- FamilyDescriptor,
14
- TargetDescriptor,
15
- } from './framework-components';
16
16
 
17
17
  export interface RuntimeFamilyDescriptor<
18
18
  TFamilyId extends string,
@@ -4,7 +4,7 @@ import type {
4
4
  ExtensionInstance,
5
5
  FamilyInstance,
6
6
  TargetInstance,
7
- } from './framework-components';
7
+ } from '../shared/framework-components';
8
8
 
9
9
  export interface RuntimeFamilyInstance<TFamilyId extends string>
10
10
  extends FamilyInstance<TFamilyId> {}
@@ -1,10 +1,10 @@
1
+ import { checkContractComponentRequirements } from '../shared/framework-components';
1
2
  import type {
2
3
  RuntimeAdapterDescriptor,
3
4
  RuntimeExtensionDescriptor,
4
5
  RuntimeFamilyDescriptor,
5
6
  RuntimeTargetDescriptor,
6
7
  } from './execution-descriptors';
7
- import { checkContractComponentRequirements } from './framework-components';
8
8
 
9
9
  export function assertRuntimeContractRequirementsSatisfied<
10
10
  TFamilyId extends string,
@@ -0,0 +1,53 @@
1
+ import type { PlanMeta } from '@prisma-next/contract/types';
2
+
3
+ /**
4
+ * Family-agnostic plan marker.
5
+ *
6
+ * Carries only `meta` (the family-agnostic plan metadata) and the optional
7
+ * phantom `_row` parameter that lets type-level utilities recover the row
8
+ * type from a plan value. SQL and Mongo extend this marker with their own
9
+ * concrete shapes (`SqlQueryPlan`, `MongoQueryPlan`).
10
+ *
11
+ * `QueryPlan` is the *pre-lowering* marker — i.e. the surface a builder
12
+ * produces before family-specific lowering turns it into an executable
13
+ * plan (`ExecutionPlan`).
14
+ */
15
+ export interface QueryPlan<Row = unknown> {
16
+ readonly meta: PlanMeta;
17
+ /**
18
+ * Phantom property to carry the Row generic for type-level utilities.
19
+ * Not set at runtime; used only for `ResultType` extraction.
20
+ */
21
+ readonly _row?: Row;
22
+ }
23
+
24
+ /**
25
+ * Family-agnostic execution-plan marker.
26
+ *
27
+ * Extends `QueryPlan` with no additional structural fields — the marker
28
+ * exists to nominally distinguish executable plans from pre-lowering plans
29
+ * in the type system. Family-specific execution plans (`SqlExecutionPlan`,
30
+ * `MongoExecutionPlan`) extend this marker with their concrete shapes
31
+ * (e.g. `sql + params` for SQL, `wireCommand` for Mongo).
32
+ */
33
+ export interface ExecutionPlan<Row = unknown> extends QueryPlan<Row> {}
34
+
35
+ /**
36
+ * Extracts the `Row` type from a plan via the phantom `_row` property.
37
+ *
38
+ * Works with any plan that extends `QueryPlan<Row>` — including
39
+ * `ExecutionPlan<Row>`, `SqlQueryPlan<Row>`, `SqlExecutionPlan<Row>`,
40
+ * `MongoQueryPlan<Row>`, and `MongoExecutionPlan<Row>`.
41
+ *
42
+ * The `_row` property must be present in the plan's static type for the
43
+ * conditional to bind `R`; objects whose type lacks `_row` resolve to
44
+ * `never`. Without the `keyof` guard, `extends { _row?: infer R }` would
45
+ * silently match any object and infer `unknown`.
46
+ *
47
+ * Example: `type Row = ResultType<typeof plan>`.
48
+ */
49
+ export type ResultType<P> = '_row' extends keyof P
50
+ ? P extends { readonly _row?: infer R }
51
+ ? R
52
+ : never
53
+ : never;
@@ -0,0 +1,85 @@
1
+ import type { CodecCallContext } from '../shared/codec-types';
2
+ import type { RuntimeAbortedPhase } from './runtime-error';
3
+ import { runtimeAborted } from './runtime-error';
4
+
5
+ /**
6
+ * Throw a phase-tagged `RUNTIME.ABORTED` envelope if the supplied
7
+ * codec-call context is already aborted at the precheck site. Centralises
8
+ * the `if (ctx.signal?.aborted) throw runtimeAborted(...)` pattern that
9
+ * every codec dispatch site repeats.
10
+ */
11
+ export function checkAborted(ctx: CodecCallContext, phase: RuntimeAbortedPhase): void {
12
+ if (ctx.signal?.aborted) {
13
+ throw runtimeAborted(phase, ctx.signal.reason);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Race a per-cell `Promise.all` (or any other in-flight work promise) against
19
+ * the supplied abort signal so the runtime returns `RUNTIME.ABORTED` promptly
20
+ * even when codec bodies ignore the signal. In-flight bodies that ignore the
21
+ * signal are abandoned and run to completion in the background — the
22
+ * cooperative-cancellation contract documented in ADR 204.
23
+ *
24
+ * Call sites still SHOULD pre-check `signal.aborted` and short-circuit with
25
+ * a phase-tagged `RUNTIME.ABORTED` envelope before invoking this helper —
26
+ * that path is the canonical "aborted at entry" surface and avoids
27
+ * scheduling the work promise. As a defensive belt-and-braces, this helper
28
+ * also handles the already-aborted case internally: `AbortSignal` does not
29
+ * replay past abort events to listeners registered after the abort, so we
30
+ * inspect `signal.aborted` synchronously and reject with the sentinel
31
+ * before installing the listener. The rejection is still attributed to the
32
+ * abort path via the sentinel-identity check.
33
+ *
34
+ * Distinguishing the rejection source is load-bearing for AC-ERR4
35
+ * (`RUNTIME.ENCODE_FAILED` / `RUNTIME.DECODE_FAILED` pass through unchanged).
36
+ * The semantically equivalent `abortable(signal)` helper in
37
+ * `@prisma-next/utils` rejects with `signal.reason ?? new DOMException(...)`,
38
+ * which is not stably distinguishable from a codec-thrown error by identity
39
+ * alone (a fresh fallback DOMException is allocated per call). We instead
40
+ * track abort attribution with a unique sentinel: only the `onAbort` listener
41
+ * installed here ever rejects with the sentinel, so an `error === sentinel`
42
+ * identity check after the race is unambiguous.
43
+ *
44
+ * Lives in `framework-components` (rather than the SQL family, where it
45
+ * originated in m2) so every family runtime that needs cooperative
46
+ * cancellation around a codec-dispatch `Promise.all` (SQL encode + decode
47
+ * today, Mongo encode in m3) shares the same attribution logic.
48
+ */
49
+ export async function raceAgainstAbort<T>(
50
+ work: Promise<T>,
51
+ signal: AbortSignal | undefined,
52
+ phase: RuntimeAbortedPhase,
53
+ ): Promise<T> {
54
+ if (signal === undefined) {
55
+ return await work;
56
+ }
57
+ const sentinel: { reason: unknown } = { reason: undefined };
58
+ let onAbort: (() => void) | undefined;
59
+
60
+ const abortPromise = new Promise<never>((_, reject) => {
61
+ if (signal.aborted) {
62
+ sentinel.reason = signal.reason;
63
+ reject(sentinel);
64
+ return;
65
+ }
66
+ onAbort = () => {
67
+ sentinel.reason = signal.reason;
68
+ reject(sentinel);
69
+ };
70
+ signal.addEventListener('abort', onAbort, { once: true });
71
+ });
72
+
73
+ try {
74
+ return await Promise.race([work, abortPromise]);
75
+ } catch (error) {
76
+ if (error === sentinel) {
77
+ throw runtimeAborted(phase, sentinel.reason);
78
+ }
79
+ throw error;
80
+ } finally {
81
+ if (onAbort) {
82
+ signal.removeEventListener('abort', onAbort);
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,132 @@
1
+ import { AsyncIterableResult } from './async-iterable-result';
2
+ import type { ExecutionPlan } from './query-plan';
3
+ import type { RuntimeMiddleware, RuntimeMiddlewareContext } from './runtime-middleware';
4
+
5
+ /**
6
+ * Drives a single execution of `runDriver()` through the middleware lifecycle.
7
+ *
8
+ * Lifecycle, in order:
9
+ * 1. For each middleware in registration order: `intercept(exec, ctx)`. The
10
+ * first non-`undefined` result wins; subsequent middleware's `intercept`
11
+ * does not fire. On a hit, the runtime emits a `middleware.intercept`
12
+ * debug event naming the winning middleware, switches the row source to
13
+ * the intercepted rows, and proceeds with `source: 'middleware'`. On
14
+ * all-passthrough (every `intercept` returns `undefined` or is omitted),
15
+ * `source: 'driver'` is used and the row source is `runDriver()`.
16
+ * 2. If `source === 'driver'`: for each middleware in registration order,
17
+ * `beforeExecute(exec, ctx)`. Skipped on the intercepted hit path —
18
+ * `beforeExecute` semantically means "about to hit the driver".
19
+ * 3. Iterate the row source. On the driver path, for each row, for each
20
+ * middleware in registration order: `onRow(row, exec, ctx)`; then yield
21
+ * the row. On the intercepted hit path, `onRow` is skipped — intercepted
22
+ * rows did not originate from a driver row stream — but rows are still
23
+ * yielded to the consumer in order.
24
+ * 4. On successful completion: for each middleware in registration order:
25
+ * `afterExecute(exec, { rowCount, latencyMs, completed: true, source },
26
+ * ctx)`.
27
+ * 5. On any error thrown during steps 1–3: for each middleware in
28
+ * registration order: `afterExecute(exec, { rowCount, latencyMs,
29
+ * completed: false, source }, ctx)`. Errors thrown by `afterExecute`
30
+ * during the error path are swallowed so they do not mask the original
31
+ * error. The original error is then rethrown.
32
+ *
33
+ * The `source` field on `AfterExecuteResult` lets observers (telemetry,
34
+ * lints, budgets) distinguish driver-served from middleware-served
35
+ * executions without needing their own out-of-band signal.
36
+ *
37
+ * This helper is the single canonical implementation of the middleware
38
+ * orchestration loop; family runtimes should not reimplement it.
39
+ */
40
+ export function runWithMiddleware<TExec extends ExecutionPlan, Row>(
41
+ exec: TExec,
42
+ middleware: ReadonlyArray<RuntimeMiddleware<TExec>>,
43
+ ctx: RuntimeMiddlewareContext,
44
+ runDriver: () => AsyncIterable<Row>,
45
+ ): AsyncIterableResult<Row> {
46
+ const iterator = async function* (): AsyncGenerator<Row, void, unknown> {
47
+ const startedAt = Date.now();
48
+ let rowCount = 0;
49
+ let completed = false;
50
+ let source: 'driver' | 'middleware' = 'driver';
51
+ // Deferred so a winning interceptor can skip `runDriver()` entirely.
52
+ // For factories that lazily produce async generators this is a no-op,
53
+ // but factories that do eager work (e.g. acquiring a connection,
54
+ // sending a query) must not run on the intercepted hit path.
55
+ let rowSource: AsyncIterable<Row> | Iterable<Row> | undefined;
56
+
57
+ try {
58
+ for (const mw of middleware) {
59
+ if (!mw.intercept) {
60
+ continue;
61
+ }
62
+ // Mark the lifecycle as middleware-driven *before* awaiting the
63
+ // hook. If `intercept` throws, the catch block reports the failure
64
+ // as `source: 'middleware'` — the failure originated in the
65
+ // intercept chain, not in the driver. If `intercept` returns
66
+ // `undefined` (passthrough), we revert to `'driver'` and continue.
67
+ source = 'middleware';
68
+ const result = await mw.intercept(exec, ctx);
69
+ if (result === undefined) {
70
+ source = 'driver';
71
+ continue;
72
+ }
73
+ ctx.log.debug?.({ event: 'middleware.intercept', middleware: mw.name });
74
+ // The intercepted rows are typed as `Record<string, unknown>` at
75
+ // the SPI level; the consumer's `Row` type parameter is enforced by
76
+ // the caller (via the plan's phantom `_row`) the same way driver
77
+ // rows are. Cast through unknown to bridge the SPI shape to the
78
+ // caller-supplied Row.
79
+ rowSource = result.rows as unknown as AsyncIterable<Row> | Iterable<Row>;
80
+ break;
81
+ }
82
+
83
+ if (source === 'driver') {
84
+ for (const mw of middleware) {
85
+ if (mw.beforeExecute) {
86
+ await mw.beforeExecute(exec, ctx);
87
+ }
88
+ }
89
+ rowSource = runDriver();
90
+ }
91
+
92
+ // `rowSource` is always assigned by this point: either the intercepted
93
+ // rows (on a hit) or `runDriver()` (on the driver path).
94
+ for await (const row of rowSource as AsyncIterable<Row> | Iterable<Row>) {
95
+ if (source === 'driver') {
96
+ for (const mw of middleware) {
97
+ if (mw.onRow) {
98
+ await mw.onRow(row as Record<string, unknown>, exec, ctx);
99
+ }
100
+ }
101
+ }
102
+ rowCount++;
103
+ yield row;
104
+ }
105
+
106
+ completed = true;
107
+ } catch (error) {
108
+ const latencyMs = Date.now() - startedAt;
109
+ for (const mw of middleware) {
110
+ if (mw.afterExecute) {
111
+ try {
112
+ await mw.afterExecute(exec, { rowCount, latencyMs, completed, source }, ctx);
113
+ } catch {
114
+ // Swallow afterExecute errors during the error path so they do not
115
+ // mask the original error.
116
+ }
117
+ }
118
+ }
119
+
120
+ throw error;
121
+ }
122
+
123
+ const latencyMs = Date.now() - startedAt;
124
+ for (const mw of middleware) {
125
+ if (mw.afterExecute) {
126
+ await mw.afterExecute(exec, { rowCount, latencyMs, completed, source }, ctx);
127
+ }
128
+ }
129
+ };
130
+
131
+ return new AsyncIterableResult(iterator());
132
+ }
@@ -0,0 +1,133 @@
1
+ import type { CodecCallContext } from '../shared/codec-types';
2
+ import { AsyncIterableResult } from './async-iterable-result';
3
+ import type { ExecutionPlan, QueryPlan } from './query-plan';
4
+ import { checkAborted } from './race-against-abort';
5
+ import { runWithMiddleware } from './run-with-middleware';
6
+ import type {
7
+ RuntimeExecuteOptions,
8
+ RuntimeExecutor,
9
+ RuntimeMiddleware,
10
+ RuntimeMiddlewareContext,
11
+ } from './runtime-middleware';
12
+
13
+ /**
14
+ * Constructor options shared by every concrete `RuntimeCore` subclass.
15
+ *
16
+ * Family runtimes typically build the middleware list and the
17
+ * `RuntimeMiddlewareContext` themselves (running compatibility checks,
18
+ * narrowing the context's `contract` field, etc.) before calling `super`.
19
+ */
20
+ export interface RuntimeCoreOptions<TMiddleware extends RuntimeMiddleware<ExecutionPlan>> {
21
+ readonly middleware: ReadonlyArray<TMiddleware>;
22
+ readonly ctx: RuntimeMiddlewareContext;
23
+ }
24
+
25
+ /**
26
+ * Family-agnostic abstract runtime base.
27
+ *
28
+ * Defines the entire `execute(plan)` template in one place:
29
+ *
30
+ * 1. `runBeforeCompile(plan)` — concrete; defaults to identity. SQL overrides
31
+ * this to run its `beforeCompile` middleware-hook chain.
32
+ * 2. `lower(plan)` — abstract. Each family produces its `*ExecutionPlan`
33
+ * (SQL via `lowerSqlPlan`, Mongo via `adapter.lower`).
34
+ * 3. `runWithMiddleware(exec, this.middleware, this.ctx,
35
+ * () => runDriver(exec))` — concrete; lifts the middleware lifecycle
36
+ * out of the family runtimes into the canonical helper.
37
+ *
38
+ * Concrete subclasses must implement `lower`, `runDriver`, and `close`.
39
+ *
40
+ * The class is generic over:
41
+ * - `TPlan` — the family's pre-lowering plan type.
42
+ * - `TExec` — the family's post-lowering (executable) plan type.
43
+ * - `TMiddleware` — the family's middleware type. Constrained to
44
+ * `RuntimeMiddleware<TExec>` because `runWithMiddleware` invokes the
45
+ * `beforeExecute` / `onRow` / `afterExecute` hooks with the lowered
46
+ * `TExec`. (The spec/plan wording "RuntimeMiddleware<TPlan>" is
47
+ * tightened to `<TExec>` here so the helper call typechecks; the
48
+ * intent is unchanged — middleware sees the post-lowering plan.)
49
+ */
50
+ export abstract class RuntimeCore<
51
+ TPlan extends QueryPlan,
52
+ TExec extends ExecutionPlan,
53
+ TMiddleware extends RuntimeMiddleware<TExec>,
54
+ > implements RuntimeExecutor<TPlan>
55
+ {
56
+ protected readonly middleware: ReadonlyArray<TMiddleware>;
57
+ protected readonly ctx: RuntimeMiddlewareContext;
58
+
59
+ constructor(options: RuntimeCoreOptions<TMiddleware>) {
60
+ this.middleware = options.middleware;
61
+ this.ctx = options.ctx;
62
+ }
63
+
64
+ /**
65
+ * Pre-lowering hook for plan rewriting. Defaults to identity. Subclasses
66
+ * may override to run a `beforeCompile` middleware chain (SQL does this
67
+ * to support typed AST rewrites — see `before-compile-chain.ts`).
68
+ */
69
+ protected runBeforeCompile(plan: TPlan): TPlan | Promise<TPlan> {
70
+ return plan;
71
+ }
72
+
73
+ /**
74
+ * Lower a pre-lowering `TPlan` into the family's executable `TExec`.
75
+ * Family-specific: SQL produces `{ sql, params, ast?, ... }`; Mongo
76
+ * produces `{ command, ... }`.
77
+ *
78
+ * `ctx` carries per-query cancellation (and any future fields on
79
+ * `CodecCallContext`); concrete subclasses forward it to the
80
+ * encode-side codec dispatch site (e.g. SQL's `encodeParams` in m2,
81
+ * Mongo's `resolveValue` in m3). The runtime allocates one ctx per
82
+ * `execute()` call and threads the same reference everywhere; the
83
+ * `signal` field inside may be `undefined`, but the ctx object itself
84
+ * is always present.
85
+ */
86
+ protected abstract lower(plan: TPlan, ctx: CodecCallContext): TExec | Promise<TExec>;
87
+
88
+ /**
89
+ * Drive the underlying transport for a lowered `TExec`. Yields raw rows
90
+ * directly from the driver as `Record<string, unknown>`; codec decoding
91
+ * (if any) is the subclass's responsibility, applied by wrapping
92
+ * `execute()` rather than living inside this hook.
93
+ *
94
+ * The `Row` type parameter on `execute()` is satisfied by the caller via
95
+ * the plan's phantom `_row`; the runtime treats rows as opaque records
96
+ * here and trusts the caller's row typing.
97
+ */
98
+ protected abstract runDriver(exec: TExec): AsyncIterable<Record<string, unknown>>;
99
+
100
+ abstract close(): Promise<void>;
101
+
102
+ execute<Row>(
103
+ plan: TPlan & { readonly _row?: Row },
104
+ options?: RuntimeExecuteOptions,
105
+ ): AsyncIterableResult<Row> {
106
+ const self = this;
107
+ const signal = options?.signal;
108
+ // One ctx per execute() call. The ctx object is always allocated; the
109
+ // `signal` field is only included when a signal was supplied (required
110
+ // under exactOptionalPropertyTypes — `{ signal: undefined }` would not
111
+ // satisfy `signal?: AbortSignal`).
112
+ const codecCtx: CodecCallContext = signal === undefined ? {} : { signal };
113
+
114
+ async function* generator(): AsyncGenerator<Row, void, unknown> {
115
+ // Pre-check the signal at entry so an already-aborted caller observes
116
+ // RUNTIME.ABORTED on the first `next()` without any work being done.
117
+ checkAborted(codecCtx, 'stream');
118
+
119
+ const compiled = await self.runBeforeCompile(plan);
120
+ const exec = await self.lower(compiled, codecCtx);
121
+ // The driver yields raw `Record<string, unknown>`; we cast to `Row` here.
122
+ // The Row contract is enforced by the caller via `plan._row`.
123
+ yield* runWithMiddleware<TExec, Row>(
124
+ exec,
125
+ self.middleware,
126
+ self.ctx,
127
+ () => self.runDriver(exec) as AsyncIterable<Row>,
128
+ );
129
+ }
130
+
131
+ return new AsyncIterableResult(generator());
132
+ }
133
+ }
@@ -0,0 +1,83 @@
1
+ export interface RuntimeErrorEnvelope extends Error {
2
+ readonly code: string;
3
+ readonly category: 'PLAN' | 'CONTRACT' | 'LINT' | 'BUDGET' | 'RUNTIME';
4
+ readonly severity: 'error';
5
+ readonly details?: Record<string, unknown>;
6
+ }
7
+
8
+ /**
9
+ * Stable code emitted by the runtime when an in-flight `execute()`
10
+ * is cancelled via the per-query `AbortSignal`. The envelope's
11
+ * `details.phase` distinguishes where the abort was observed:
12
+ *
13
+ * - `'encode'` — abort fired during `encodeParams` (SQL) or
14
+ * `resolveValue` (Mongo).
15
+ * - `'decode'` — abort fired during `decodeRow` / `decodeField`.
16
+ * - `'stream'` — abort fired between rows or before any codec call
17
+ * (already-aborted at entry).
18
+ */
19
+ export const RUNTIME_ABORTED = 'RUNTIME.ABORTED' as const;
20
+
21
+ /** Discriminator placed in `details.phase` of a `RUNTIME.ABORTED` envelope. */
22
+ export type RuntimeAbortedPhase = 'encode' | 'decode' | 'stream';
23
+
24
+ /**
25
+ * Type guard for the runtime-error envelope produced by `runtimeError`.
26
+ *
27
+ * Prefer this over duck-typing on `error.code` directly so consumers stay
28
+ * insulated from the envelope's internal shape.
29
+ */
30
+ export function isRuntimeError(error: unknown): error is RuntimeErrorEnvelope {
31
+ return (
32
+ error instanceof Error &&
33
+ 'code' in error &&
34
+ typeof (error as { code?: unknown }).code === 'string' &&
35
+ 'category' in error &&
36
+ 'severity' in error
37
+ );
38
+ }
39
+
40
+ export function runtimeError(
41
+ code: string,
42
+ message: string,
43
+ details?: Record<string, unknown>,
44
+ ): RuntimeErrorEnvelope {
45
+ const error = new Error(message) as RuntimeErrorEnvelope;
46
+ Object.defineProperty(error, 'name', {
47
+ value: 'RuntimeError',
48
+ configurable: true,
49
+ });
50
+
51
+ return Object.assign(error, {
52
+ code,
53
+ category: resolveCategory(code),
54
+ severity: 'error' as const,
55
+ message,
56
+ details,
57
+ });
58
+ }
59
+
60
+ function resolveCategory(code: string): RuntimeErrorEnvelope['category'] {
61
+ const prefix = code.split('.')[0] ?? 'RUNTIME';
62
+ switch (prefix) {
63
+ case 'PLAN':
64
+ case 'CONTRACT':
65
+ case 'LINT':
66
+ case 'BUDGET':
67
+ return prefix;
68
+ default:
69
+ return 'RUNTIME';
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Construct a `RUNTIME.ABORTED` envelope. Phase distinguishes where the
75
+ * abort was observed (encode / decode / stream); cause carries `signal.reason`
76
+ * verbatim from the platform — native abort produces a `DOMException`,
77
+ * explicit `controller.abort(reason)` produces whatever the caller passed.
78
+ * No synthesis happens here.
79
+ */
80
+ export function runtimeAborted(phase: RuntimeAbortedPhase, cause?: unknown): RuntimeErrorEnvelope {
81
+ const envelope = runtimeError(RUNTIME_ABORTED, `Operation aborted during ${phase}`, { phase });
82
+ return Object.assign(envelope, { cause });
83
+ }