@prisma-next/framework-components 0.5.0-dev.4 → 0.5.0-dev.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -0
- package/dist/authoring.d.mts +1 -1
- package/dist/authoring.mjs +1 -1
- package/dist/authoring.mjs.map +1 -1
- package/dist/codec-types-CB0jWeHU.d.mts +207 -0
- package/dist/codec-types-CB0jWeHU.d.mts.map +1 -0
- package/dist/codec.d.mts +2 -2
- package/dist/codec.mjs +38 -2
- package/dist/codec.mjs.map +1 -1
- package/dist/components.d.mts +1 -1
- package/dist/components.mjs +1 -1
- package/dist/control.d.mts +101 -35
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +11 -5
- package/dist/control.mjs.map +1 -1
- package/dist/emission-types-D6t3_a0x.d.mts +39 -0
- package/dist/emission-types-D6t3_a0x.d.mts.map +1 -0
- package/dist/emission.d.mts +2 -2
- package/dist/execution.d.mts +5 -5
- package/dist/execution.d.mts.map +1 -1
- package/dist/execution.mjs +3 -3
- package/dist/execution.mjs.map +1 -1
- package/dist/{framework-authoring-D1-JZ37B.d.mts → framework-authoring-BdrFDx4x.d.mts} +2 -2
- package/dist/framework-authoring-BdrFDx4x.d.mts.map +1 -0
- package/dist/{framework-components-EJXe-pum.d.mts → framework-components-AHI6V96G.d.mts} +6 -6
- package/dist/framework-components-AHI6V96G.d.mts.map +1 -0
- package/dist/{framework-components-C8ZhSwXe.mjs → framework-components-BsWST1Rn.mjs} +2 -2
- package/dist/framework-components-BsWST1Rn.mjs.map +1 -0
- package/dist/psl-ast-9X5rwo98.d.mts +159 -0
- package/dist/psl-ast-9X5rwo98.d.mts.map +1 -0
- package/dist/psl-ast.d.mts +2 -0
- package/dist/psl-ast.mjs +1 -0
- package/dist/runtime.d.mts +251 -19
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +215 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-import-spec-C4sc7wbb.d.mts → types-import-spec-D-O6GotH.d.mts} +2 -2
- package/dist/types-import-spec-D-O6GotH.d.mts.map +1 -0
- package/package.json +8 -6
- package/src/control/control-capabilities.ts +71 -0
- package/src/{control-descriptors.ts → control/control-descriptors.ts} +7 -7
- package/src/{control-instances.ts → control/control-instances.ts} +6 -6
- package/src/{control-migration-types.ts → control/control-migration-types.ts} +48 -23
- package/src/control/control-operation-preview.ts +23 -0
- package/src/{control-stack.ts → control/control-stack.ts} +13 -13
- package/src/control/emission-types.ts +49 -0
- package/src/control/psl-ast.ts +193 -0
- package/src/{execution-descriptors.ts → execution/execution-descriptors.ts} +7 -7
- package/src/{execution-instances.ts → execution/execution-instances.ts} +1 -1
- package/src/{execution-requirements.ts → execution/execution-requirements.ts} +1 -1
- package/src/execution/query-plan.ts +53 -0
- package/src/execution/race-against-abort.ts +85 -0
- package/src/execution/run-with-middleware.ts +77 -0
- package/src/execution/runtime-core.ts +133 -0
- package/src/execution/runtime-error.ts +83 -0
- package/src/{runtime-middleware.ts → execution/runtime-middleware.ts} +32 -12
- package/src/exports/authoring.ts +2 -2
- package/src/exports/codec.ts +14 -2
- package/src/exports/components.ts +2 -2
- package/src/exports/control.ts +26 -12
- package/src/exports/emission.ts +2 -2
- package/src/exports/execution.ts +5 -5
- package/src/exports/psl-ast.ts +1 -0
- package/src/exports/runtime.ts +16 -5
- package/src/shared/codec-types.ts +261 -0
- package/dist/codec-types-B58nCJiu.d.mts +0 -40
- package/dist/codec-types-B58nCJiu.d.mts.map +0 -1
- package/dist/emission-types-BPAALJbF.d.mts +0 -24
- package/dist/emission-types-BPAALJbF.d.mts.map +0 -1
- package/dist/framework-authoring-D1-JZ37B.d.mts.map +0 -1
- package/dist/framework-components-C8ZhSwXe.mjs.map +0 -1
- package/dist/framework-components-EJXe-pum.d.mts.map +0 -1
- package/dist/types-import-spec-C4sc7wbb.d.mts.map +0 -1
- package/src/codec-types.ts +0 -46
- package/src/control-capabilities.ts +0 -34
- package/src/emission-types.ts +0 -28
- package/src/runtime-error.ts +0 -39
- /package/src/{control-result-types.ts → control/control-result-types.ts} +0 -0
- /package/src/{control-schema-view.ts → control/control-schema-view.ts} +0 -0
- /package/src/{async-iterable-result.ts → execution/async-iterable-result.ts} +0 -0
- /package/src/{execution-stack.ts → execution/execution-stack.ts} +0 -0
- /package/src/{framework-authoring.ts → shared/framework-authoring.ts} +0 -0
- /package/src/{framework-components.ts → shared/framework-components.ts} +0 -0
- /package/src/{mutation-default-types.ts → shared/mutation-default-types.ts} +0 -0
- /package/src/{types-import-spec.ts → shared/types-import-spec.ts} +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export interface PslPosition {
|
|
2
|
+
readonly offset: number;
|
|
3
|
+
readonly line: number;
|
|
4
|
+
readonly column: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PslSpan {
|
|
8
|
+
readonly start: PslPosition;
|
|
9
|
+
readonly end: PslPosition;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type PslDiagnosticCode =
|
|
13
|
+
| 'PSL_UNTERMINATED_BLOCK'
|
|
14
|
+
| 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK'
|
|
15
|
+
| 'PSL_INVALID_ATTRIBUTE_SYNTAX'
|
|
16
|
+
| 'PSL_INVALID_MODEL_MEMBER'
|
|
17
|
+
| 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE'
|
|
18
|
+
| 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE'
|
|
19
|
+
| 'PSL_INVALID_RELATION_ATTRIBUTE'
|
|
20
|
+
| 'PSL_INVALID_REFERENTIAL_ACTION'
|
|
21
|
+
| 'PSL_INVALID_DEFAULT_VALUE'
|
|
22
|
+
| 'PSL_INVALID_ENUM_MEMBER'
|
|
23
|
+
| 'PSL_INVALID_TYPES_MEMBER';
|
|
24
|
+
|
|
25
|
+
export interface PslDiagnostic {
|
|
26
|
+
readonly code: PslDiagnosticCode;
|
|
27
|
+
readonly message: string;
|
|
28
|
+
readonly sourceId: string;
|
|
29
|
+
readonly span: PslSpan;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PslDefaultFunctionValue {
|
|
33
|
+
readonly kind: 'function';
|
|
34
|
+
readonly name: 'autoincrement' | 'now';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PslDefaultLiteralValue {
|
|
38
|
+
readonly kind: 'literal';
|
|
39
|
+
readonly value: string | number | boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type PslDefaultValue = PslDefaultFunctionValue | PslDefaultLiteralValue;
|
|
43
|
+
|
|
44
|
+
export type PslAttributeTarget = 'field' | 'model' | 'enum' | 'namedType';
|
|
45
|
+
|
|
46
|
+
export interface PslAttributePositionalArgument {
|
|
47
|
+
readonly kind: 'positional';
|
|
48
|
+
readonly value: string;
|
|
49
|
+
readonly span: PslSpan;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PslAttributeNamedArgument {
|
|
53
|
+
readonly kind: 'named';
|
|
54
|
+
readonly name: string;
|
|
55
|
+
readonly value: string;
|
|
56
|
+
readonly span: PslSpan;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type PslAttributeArgument = PslAttributePositionalArgument | PslAttributeNamedArgument;
|
|
60
|
+
|
|
61
|
+
export interface PslTypeConstructorCall {
|
|
62
|
+
readonly kind: 'typeConstructor';
|
|
63
|
+
readonly path: readonly string[];
|
|
64
|
+
readonly args: readonly PslAttributeArgument[];
|
|
65
|
+
readonly span: PslSpan;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PslAttribute {
|
|
69
|
+
readonly kind: 'attribute';
|
|
70
|
+
readonly target: PslAttributeTarget;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly args: readonly PslAttributeArgument[];
|
|
73
|
+
readonly span: PslSpan;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type PslReferentialAction = string;
|
|
77
|
+
|
|
78
|
+
export type PslFieldAttribute = PslAttribute;
|
|
79
|
+
|
|
80
|
+
export interface PslField {
|
|
81
|
+
readonly kind: 'field';
|
|
82
|
+
readonly name: string;
|
|
83
|
+
readonly typeName: string;
|
|
84
|
+
readonly typeConstructor?: PslTypeConstructorCall;
|
|
85
|
+
readonly optional: boolean;
|
|
86
|
+
readonly list: boolean;
|
|
87
|
+
readonly typeRef?: string;
|
|
88
|
+
readonly attributes: readonly PslFieldAttribute[];
|
|
89
|
+
readonly span: PslSpan;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface PslUniqueConstraint {
|
|
93
|
+
readonly kind: 'unique';
|
|
94
|
+
readonly fields: readonly string[];
|
|
95
|
+
readonly span: PslSpan;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface PslIndexConstraint {
|
|
99
|
+
readonly kind: 'index';
|
|
100
|
+
readonly fields: readonly string[];
|
|
101
|
+
readonly span: PslSpan;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type PslModelAttribute = PslAttribute;
|
|
105
|
+
|
|
106
|
+
export interface PslModel {
|
|
107
|
+
readonly kind: 'model';
|
|
108
|
+
readonly name: string;
|
|
109
|
+
readonly fields: readonly PslField[];
|
|
110
|
+
readonly attributes: readonly PslModelAttribute[];
|
|
111
|
+
readonly span: PslSpan;
|
|
112
|
+
/**
|
|
113
|
+
* Optional leading comment line emitted above the `model` keyword by the
|
|
114
|
+
* printer. Producers (e.g. `sqlSchemaIrToPslAst`) attach introspection
|
|
115
|
+
* advisories such as "// WARNING: This table has no primary key in the
|
|
116
|
+
* database" here. The parser leaves this field unset; round-tripping a
|
|
117
|
+
* parsed schema does not re-attach comments.
|
|
118
|
+
*/
|
|
119
|
+
readonly comment?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface PslEnumValue {
|
|
123
|
+
readonly kind: 'enumValue';
|
|
124
|
+
readonly name: string;
|
|
125
|
+
/**
|
|
126
|
+
* Optional storage label for the enum member, captured from a trailing
|
|
127
|
+
* `@map("...")` attribute on the member line. The parser populates this
|
|
128
|
+
* when the source PSL carries an explicit `@map`. Producers (e.g.
|
|
129
|
+
* `sqlSchemaIrToPslAst`) leave it unset; the printer emits `@map(...)`
|
|
130
|
+
* automatically when normalisation would change the printed member name
|
|
131
|
+
* (so an enum value `'in-progress'` becomes `inProgress @map("in-progress")`
|
|
132
|
+
* in PSL, preserving the round-trip).
|
|
133
|
+
*/
|
|
134
|
+
readonly mapName?: string;
|
|
135
|
+
readonly span: PslSpan;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface PslEnum {
|
|
139
|
+
readonly kind: 'enum';
|
|
140
|
+
readonly name: string;
|
|
141
|
+
readonly values: readonly PslEnumValue[];
|
|
142
|
+
readonly attributes: readonly PslAttribute[];
|
|
143
|
+
readonly span: PslSpan;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface PslCompositeType {
|
|
147
|
+
readonly kind: 'compositeType';
|
|
148
|
+
readonly name: string;
|
|
149
|
+
readonly fields: readonly PslField[];
|
|
150
|
+
readonly attributes: readonly PslAttribute[];
|
|
151
|
+
readonly span: PslSpan;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface PslNamedTypeDeclaration {
|
|
155
|
+
readonly kind: 'namedType';
|
|
156
|
+
readonly name: string;
|
|
157
|
+
/**
|
|
158
|
+
* Parser invariant: exactly one of `baseType` and `typeConstructor` is set.
|
|
159
|
+
* Expressing this as a discriminated union trips TypeScript narrowing when
|
|
160
|
+
* the declaration flows through helpers that accept the full union.
|
|
161
|
+
*/
|
|
162
|
+
readonly baseType?: string;
|
|
163
|
+
readonly typeConstructor?: PslTypeConstructorCall;
|
|
164
|
+
readonly attributes: readonly PslAttribute[];
|
|
165
|
+
readonly span: PslSpan;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface PslTypesBlock {
|
|
169
|
+
readonly kind: 'types';
|
|
170
|
+
readonly declarations: readonly PslNamedTypeDeclaration[];
|
|
171
|
+
readonly span: PslSpan;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface PslDocumentAst {
|
|
175
|
+
readonly kind: 'document';
|
|
176
|
+
readonly sourceId: string;
|
|
177
|
+
readonly models: readonly PslModel[];
|
|
178
|
+
readonly enums: readonly PslEnum[];
|
|
179
|
+
readonly compositeTypes: readonly PslCompositeType[];
|
|
180
|
+
readonly types?: PslTypesBlock;
|
|
181
|
+
readonly span: PslSpan;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface ParsePslDocumentInput {
|
|
185
|
+
readonly schema: string;
|
|
186
|
+
readonly sourceId: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ParsePslDocumentResult {
|
|
190
|
+
readonly ast: PslDocumentAst;
|
|
191
|
+
readonly diagnostics: readonly PslDiagnostic[];
|
|
192
|
+
readonly ok: boolean;
|
|
193
|
+
}
|
|
@@ -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,
|
|
@@ -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,77 @@
|
|
|
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: `beforeExecute(exec, ctx)`.
|
|
10
|
+
* 2. For each row yielded by `runDriver()`: for each middleware in registration
|
|
11
|
+
* order: `onRow(row, exec, ctx)`; then yield the row to the consumer.
|
|
12
|
+
* 3. On successful completion: for each middleware in registration order:
|
|
13
|
+
* `afterExecute(exec, { rowCount, latencyMs, completed: true }, ctx)`.
|
|
14
|
+
* 4. On any error thrown by the driver loop: for each middleware in
|
|
15
|
+
* registration order: `afterExecute(exec, { rowCount, latencyMs,
|
|
16
|
+
* completed: false }, ctx)`. Errors thrown by `afterExecute` during the
|
|
17
|
+
* error path are swallowed so they do not mask the original driver error.
|
|
18
|
+
* The original error is then rethrown.
|
|
19
|
+
*
|
|
20
|
+
* This helper is the single canonical implementation of the middleware
|
|
21
|
+
* orchestration loop; family runtimes should not reimplement it.
|
|
22
|
+
*/
|
|
23
|
+
export function runWithMiddleware<TExec extends ExecutionPlan, Row>(
|
|
24
|
+
exec: TExec,
|
|
25
|
+
middleware: ReadonlyArray<RuntimeMiddleware<TExec>>,
|
|
26
|
+
ctx: RuntimeMiddlewareContext,
|
|
27
|
+
runDriver: () => AsyncIterable<Row>,
|
|
28
|
+
): AsyncIterableResult<Row> {
|
|
29
|
+
const iterator = async function* (): AsyncGenerator<Row, void, unknown> {
|
|
30
|
+
const startedAt = Date.now();
|
|
31
|
+
let rowCount = 0;
|
|
32
|
+
let completed = false;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
for (const mw of middleware) {
|
|
36
|
+
if (mw.beforeExecute) {
|
|
37
|
+
await mw.beforeExecute(exec, ctx);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for await (const row of runDriver()) {
|
|
42
|
+
for (const mw of middleware) {
|
|
43
|
+
if (mw.onRow) {
|
|
44
|
+
await mw.onRow(row as Record<string, unknown>, exec, ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
rowCount++;
|
|
48
|
+
yield row;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
completed = true;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const latencyMs = Date.now() - startedAt;
|
|
54
|
+
for (const mw of middleware) {
|
|
55
|
+
if (mw.afterExecute) {
|
|
56
|
+
try {
|
|
57
|
+
await mw.afterExecute(exec, { rowCount, latencyMs, completed }, ctx);
|
|
58
|
+
} catch {
|
|
59
|
+
// Swallow afterExecute errors during the error path so they do not
|
|
60
|
+
// mask the original driver error.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const latencyMs = Date.now() - startedAt;
|
|
69
|
+
for (const mw of middleware) {
|
|
70
|
+
if (mw.afterExecute) {
|
|
71
|
+
await mw.afterExecute(exec, { rowCount, latencyMs, completed }, ctx);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return new AsyncIterableResult(iterator());
|
|
77
|
+
}
|
|
@@ -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
|
+
}
|