@aventara/core 1.0.0-rc.10 → 1.0.0-rc.11
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/CHANGELOG.md +79 -0
- package/ERRORS.md +82 -82
- package/README.md +57 -1
- package/dist/compiler/contract-compiler-diagnostic.guidance.js +1 -1
- package/dist/compiler/contract.compiler.js +56 -9
- package/dist/compiler/create-requirements.projector.js +1 -3
- package/dist/compiler/field-validator.compiler.js +1 -3
- package/dist/compiler/managed-elsewhere.projector.js +5 -5
- package/dist/config/cache-stage.factory.d.ts +24 -0
- package/dist/config/cache-stage.factory.js +3 -0
- package/dist/config/config.resolver.js +1 -7
- package/dist/config/config.validator.js +61 -13
- package/dist/config/exact-framework-config.d.ts +6 -0
- package/dist/config/field-compute.resolver.js +1 -3
- package/dist/config/pipeline-config.d.ts +16 -0
- package/dist/config/pipeline-tree.resolver.js +2 -3
- package/dist/contracts/canonical-json.writer.d.ts +7 -0
- package/dist/contracts/canonical-json.writer.js +55 -0
- package/dist/contracts/client-contract-structure.validator.d.ts +64 -0
- package/dist/contracts/client-contract-structure.validator.js +12 -0
- package/dist/contracts/contract-canonicalizer.js +5 -0
- package/dist/contracts/contract-limits.d.ts +6 -0
- package/dist/contracts/contract-limits.js +2 -0
- package/dist/contracts/contract.d.ts +15 -0
- package/dist/contracts/json-schema.projector.js +1 -3
- package/dist/framework/access-facade.js +1 -3
- package/dist/framework/framework.factory.js +42 -13
- package/dist/framework/operation-facade.d.ts +67 -6
- package/dist/framework/operation-facade.js +7 -0
- package/dist/framework/operation-set.runner.d.ts +47 -0
- package/dist/framework/operation-set.runner.js +168 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internal.d.ts +2 -0
- package/dist/internal.js +2 -0
- package/dist/operations/operation-validator-core.js +2 -4
- package/dist/operations/structural-limits.walker.js +1 -3
- package/dist/policy/access-question.runtime.js +1 -3
- package/dist/policy/policy-admission.js +1 -3
- package/dist/policy/policy-after-update.runtime.js +1 -3
- package/dist/policy/policy-nested-write.runtime.js +3 -12
- package/dist/policy/policy-read.runtime.js +1 -3
- package/dist/policy/policy-reference-write.runtime.js +2 -11
- package/dist/policy/policy-request.d.ts +31 -0
- package/dist/policy/policy-request.js +69 -17
- package/dist/policy/policy-session.js +1 -3
- package/dist/policy/policy-write.runtime.js +1 -3
- package/dist/prebuilt/core-version.generated.d.ts +1 -1
- package/dist/prebuilt/core-version.generated.js +1 -1
- package/dist/prebuilt/prebuilt-changes.js +1 -3
- package/dist/prebuilt/prebuilt-contracts.loader.js +1 -3
- package/dist/prebuilt/prebuilt-inputs.fingerprint.js +1 -3
- package/dist/protocol/absent-route.responder.d.ts +69 -16
- package/dist/protocol/absent-route.responder.js +75 -31
- package/dist/protocol/contract-document.responder.d.ts +7 -0
- package/dist/protocol/contract-document.responder.js +3 -2
- package/dist/protocol/http-request.decoder.d.ts +33 -0
- package/dist/protocol/http-request.decoder.js +23 -7
- package/dist/protocol/http-request.reader.d.ts +4 -0
- package/dist/protocol/http-request.reader.js +11 -2
- package/dist/protocol/http-response.encoder.d.ts +12 -0
- package/dist/protocol/http-response.encoder.js +15 -1
- package/dist/protocol/operation-response.guard.js +1 -3
- package/dist/protocol/operation-set.decoder.d.ts +48 -0
- package/dist/protocol/operation-set.decoder.js +185 -0
- package/dist/protocol/operation-set.handler.d.ts +27 -0
- package/dist/protocol/operation-set.handler.js +179 -0
- package/dist/protocol/protocol-headers.d.ts +9 -0
- package/dist/protocol/protocol-headers.js +12 -5
- package/dist/protocol/protocol-surface.d.ts +12 -4
- package/dist/protocol/protocol-surface.js +34 -1
- package/dist/protocol/protocol.binder.d.ts +37 -1
- package/dist/protocol/protocol.binder.js +2 -0
- package/dist/protocol/protocol.handler.d.ts +35 -2
- package/dist/protocol/protocol.handler.js +84 -8
- package/dist/protocol/read-cache.responder.d.ts +26 -0
- package/dist/protocol/read-cache.responder.js +47 -0
- package/dist/protocol/read-query.codec.d.ts +34 -0
- package/dist/protocol/read-query.codec.js +125 -0
- package/dist/runtime/computed-read-runtime.js +2 -8
- package/dist/runtime/create-requirement.js +1 -3
- package/dist/runtime/diagnostics-reporter.d.ts +6 -1
- package/dist/runtime/diagnostics-reporter.js +3 -8
- package/dist/runtime/error.catalog.d.ts +2 -2
- package/dist/runtime/error.catalog.js +1 -1
- package/dist/runtime/field-stage.runtime.d.ts +0 -2
- package/dist/runtime/field-stage.runtime.js +1 -7
- package/dist/runtime/field-validator.runtime.js +2 -1
- package/dist/runtime/mutation-planner.js +1 -3
- package/dist/runtime/nested-write.walker.js +2 -1
- package/dist/runtime/output-validator.js +1 -3
- package/dist/runtime/owned-execution.registry.d.ts +22 -2
- package/dist/runtime/pipeline.d.ts +5 -1
- package/dist/runtime/record.guard.d.ts +14 -0
- package/dist/runtime/record.guard.js +10 -0
- package/dist/runtime/request-envelope.validator.js +13 -10
- package/dist/runtime/scalar-runtime.d.ts +0 -10
- package/dist/runtime/scalar-runtime.js +1 -10
- package/dist/runtime/thenable.guard.d.ts +7 -0
- package/dist/runtime/thenable.guard.js +5 -0
- package/dist/runtime/unsent-value.normalizer.js +2 -1
- package/dist/transactions/operation-arguments-encoder.js +1 -7
- package/dist/transactions/sql-statement.validator.js +2 -1
- package/dist/transactions/transaction-plan-assembler.js +1 -7
- package/dist/transactions/transaction-plan-validator.js +4 -6
- package/dist/transactions/transaction-reference-collector.js +8 -10
- package/dist/transactions/transaction-reference-resolver.js +1 -3
- package/dist/transactions/transaction-runtime.js +31 -2
- package/dist/transactions/transaction-stage.runtime.js +1 -3
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { validationIssue, } from "../operations/validation-code.js";
|
|
2
|
+
import { operationFailure, } from "../runtime/operation-response.js";
|
|
3
|
+
import { checkPostRoute } from "./absent-route.responder.js";
|
|
4
|
+
import { isParsedByDecoder, markParsedByDecoder, readBody, transportOf, } from "./http-request.decoder.js";
|
|
5
|
+
import { ALL_ROUTE, readRequest } from "./http-request.reader.js";
|
|
6
|
+
import { encodeOperationResponse, } from "./http-response.encoder.js";
|
|
7
|
+
const BODY_KEYS = ["operations"];
|
|
8
|
+
const ENTRY_KEYS = ["resource", "family", "variant", "args"];
|
|
9
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
10
|
+
export function decodeAllRequest(contracts, received, origin = "client") {
|
|
11
|
+
const read = readRequest(received);
|
|
12
|
+
if (!read.ok) {
|
|
13
|
+
return read;
|
|
14
|
+
}
|
|
15
|
+
const { requestId } = read;
|
|
16
|
+
const refuse = (response) => ({ ok: false, response, requestId });
|
|
17
|
+
const route = checkPostRoute(ALL_ROUTE, contracts, read, origin);
|
|
18
|
+
if (!route.ok) {
|
|
19
|
+
return refuse(route.response);
|
|
20
|
+
}
|
|
21
|
+
const body = readBody(read, contracts, origin);
|
|
22
|
+
if (!body.ok) {
|
|
23
|
+
return refuse(body.response);
|
|
24
|
+
}
|
|
25
|
+
const entries = readEntries(body.value, contracts[origin].limits.maxAll, isParsedByDecoder(body.value));
|
|
26
|
+
if (!Array.isArray(entries)) {
|
|
27
|
+
return refuse(encodeOperationResponse(entries, requestId));
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
ok: true,
|
|
31
|
+
request: {
|
|
32
|
+
origin,
|
|
33
|
+
requestId,
|
|
34
|
+
protocol: route.protocol,
|
|
35
|
+
transport: transportOf(read),
|
|
36
|
+
entries,
|
|
37
|
+
streamed: acceptsNdjson(read.headers.accept),
|
|
38
|
+
},
|
|
39
|
+
requestId,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function shapeFailure(issue) {
|
|
43
|
+
return operationFailure("A2004", {
|
|
44
|
+
message: "Operation Set failed framework validation.",
|
|
45
|
+
issues: [issue],
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function readEntries(body, maxAll, parsedHere) {
|
|
49
|
+
const members = ownMembers(body, []);
|
|
50
|
+
if (!(members instanceof Map)) {
|
|
51
|
+
return shapeFailure(members);
|
|
52
|
+
}
|
|
53
|
+
const exact = exactMembers(members, BODY_KEYS, []);
|
|
54
|
+
if (exact !== undefined) {
|
|
55
|
+
return shapeFailure(exact);
|
|
56
|
+
}
|
|
57
|
+
const operations = members.get("operations");
|
|
58
|
+
const keyed = isRecord(operations)
|
|
59
|
+
? ownMembers(operations, ["operations"])
|
|
60
|
+
: undefined;
|
|
61
|
+
if (keyed === undefined) {
|
|
62
|
+
return shapeFailure(validationIssue("V1001", ["operations"], "operations must be an object."));
|
|
63
|
+
}
|
|
64
|
+
if (!(keyed instanceof Map)) {
|
|
65
|
+
return shapeFailure(keyed);
|
|
66
|
+
}
|
|
67
|
+
if (keyed.size === 0) {
|
|
68
|
+
return shapeFailure(validationIssue("V1004", ["operations"], "Value outside accepted range."));
|
|
69
|
+
}
|
|
70
|
+
if (keyed.size > maxAll) {
|
|
71
|
+
return operationFailure("A2009", {
|
|
72
|
+
message: "Operation count exceeds the active Contract limit.",
|
|
73
|
+
issues: [
|
|
74
|
+
validationIssue("V1014", ["operations"], `Operation count exceeds maxAll (${maxAll}).`),
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const key of keyed.keys()) {
|
|
79
|
+
if (!IDENTIFIER.test(key)) {
|
|
80
|
+
return shapeFailure(validationIssue("V1001", ["operations"], "Every operation key must be an identifier."));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const entries = [];
|
|
84
|
+
for (const [key, value] of keyed) {
|
|
85
|
+
const entry = readEntry(key, value);
|
|
86
|
+
if (!("key" in entry)) {
|
|
87
|
+
return shapeFailure(entry);
|
|
88
|
+
}
|
|
89
|
+
if (parsedHere) {
|
|
90
|
+
markParsedByDecoder(entry.args);
|
|
91
|
+
}
|
|
92
|
+
entries.push(entry);
|
|
93
|
+
}
|
|
94
|
+
return entries;
|
|
95
|
+
}
|
|
96
|
+
function readEntry(key, value) {
|
|
97
|
+
const at = ["operations", key];
|
|
98
|
+
const members = isRecord(value) ? ownMembers(value, at) : undefined;
|
|
99
|
+
if (members === undefined) {
|
|
100
|
+
return validationIssue("V1001", at, "Operation entry must be an object.");
|
|
101
|
+
}
|
|
102
|
+
if (!(members instanceof Map)) {
|
|
103
|
+
return members;
|
|
104
|
+
}
|
|
105
|
+
const exact = exactMembers(members, ENTRY_KEYS, at);
|
|
106
|
+
if (exact !== undefined) {
|
|
107
|
+
return exact;
|
|
108
|
+
}
|
|
109
|
+
const strings = {};
|
|
110
|
+
for (const name of ["resource", "family", "variant"]) {
|
|
111
|
+
const member = members.get(name);
|
|
112
|
+
if (typeof member !== "string") {
|
|
113
|
+
return validationIssue("V1001", [...at, name], `${name} must be a string.`);
|
|
114
|
+
}
|
|
115
|
+
strings[name] = member;
|
|
116
|
+
}
|
|
117
|
+
const args = members.get("args");
|
|
118
|
+
if (!isRecord(args)) {
|
|
119
|
+
return validationIssue("V1001", [...at, "args"], "args must be an object.");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
key,
|
|
123
|
+
resource: strings.resource,
|
|
124
|
+
family: strings.family,
|
|
125
|
+
variant: strings.variant,
|
|
126
|
+
args,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function isRecord(value) {
|
|
130
|
+
try {
|
|
131
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function ownMembers(value, at) {
|
|
138
|
+
const refused = (message) => validationIssue("V1001", at, message);
|
|
139
|
+
try {
|
|
140
|
+
const prototype = Object.getPrototypeOf(value);
|
|
141
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
142
|
+
return refused("Value must be a plain object.");
|
|
143
|
+
}
|
|
144
|
+
const members = new Map();
|
|
145
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
146
|
+
if (typeof key === "symbol") {
|
|
147
|
+
return refused("Symbol properties are not valid here.");
|
|
148
|
+
}
|
|
149
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
150
|
+
if (descriptor === undefined ||
|
|
151
|
+
!descriptor.enumerable ||
|
|
152
|
+
!("value" in descriptor)) {
|
|
153
|
+
return refused("Every property must be an own enumerable data property.");
|
|
154
|
+
}
|
|
155
|
+
members.set(key, descriptor.value);
|
|
156
|
+
}
|
|
157
|
+
return members;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return refused("Value could not be read.");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function exactMembers(members, names, at) {
|
|
164
|
+
for (const name of names) {
|
|
165
|
+
if (!members.has(name)) {
|
|
166
|
+
return validationIssue("V1000", [...at, name], `Required property "${name}" is missing.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const key of members.keys()) {
|
|
170
|
+
if (!names.includes(key)) {
|
|
171
|
+
return validationIssue("V1001", [...at, key], `Property "${key}" is not valid here.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
function acceptsNdjson(accept) {
|
|
177
|
+
if (accept === undefined) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return accept.split(",").some((range) => {
|
|
181
|
+
const [type, ...parameters] = range.split(";").map((part) => part.trim());
|
|
182
|
+
return (type?.toLowerCase() === "application/x-ndjson" &&
|
|
183
|
+
!parameters.some((parameter) => /^q\s*=\s*0(\.0{0,3})?$/i.test(parameter)));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AbortSignalShape } from "../framework/operation-set.runner.js";
|
|
2
|
+
import type { AvProtocolRequest } from "./http-request.decoder.js";
|
|
3
|
+
import { type AvProtocolResponse } from "./http-response.encoder.js";
|
|
4
|
+
import { type HandlerContext, type ProtocolCallOptions } from "./protocol.handler.js";
|
|
5
|
+
/** What a host passes beside an Operation Set request. */
|
|
6
|
+
export type OperationSetCallOptions = ProtocolCallOptions & {
|
|
7
|
+
/** Aborted when the client disconnects: entries not yet started never start, and nothing more is written. */
|
|
8
|
+
readonly signal?: AbortSignalShape;
|
|
9
|
+
};
|
|
10
|
+
/** A streamed answer: status, headers, and the body as lines produced as entries settle. */
|
|
11
|
+
type OperationSetStreamedResponse = {
|
|
12
|
+
readonly status: 200;
|
|
13
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
14
|
+
readonly body: AsyncIterable<string>;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* `protocol.handleAll(request, options?)`: the `/_all` request decoded, its state
|
|
18
|
+
* seeded once, its principal derived once, and every entry run through the bound
|
|
19
|
+
* Framework's own `execute` concurrently, at most `limits.maxAllConcurrency` at once, each
|
|
20
|
+
* answered by the envelope its single route writes. The whole request is refused
|
|
21
|
+
* only for its transport, identity, body shape or `limits.maxAll`, or when its
|
|
22
|
+
* seed or principal fails; otherwise it is `200 A1000` with `data` keyed by the
|
|
23
|
+
* caller's keys, or, under `Accept: application/x-ndjson`, one line per entry as
|
|
24
|
+
* it settles. Total over any value, as `handleOperation` is.
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleAllRequest(context: HandlerContext, request: AvProtocolRequest, options?: OperationSetCallOptions): Promise<AvProtocolResponse | OperationSetStreamedResponse>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { maxAllConcurrencyOf, } from "../framework/operation-set.runner.js";
|
|
2
|
+
import { createDiagnosticsReporter, } from "../runtime/diagnostics-reporter.js";
|
|
3
|
+
import { internalFailure } from "../runtime/operation-response.js";
|
|
4
|
+
import { resolveOperationRoute } from "./absent-route.responder.js";
|
|
5
|
+
import { encodeOperationResponse, encodeOperationSetResponse, operationSetLine, streamedHeaders, } from "./http-response.encoder.js";
|
|
6
|
+
import { decodeAllRequest, } from "./operation-set.decoder.js";
|
|
7
|
+
import { admitOperation, seededState, } from "./protocol.handler.js";
|
|
8
|
+
export async function handleAllRequest(context, request, options) {
|
|
9
|
+
const decoded = decodeAllRequest(context.contracts, request, context.origin);
|
|
10
|
+
if (!decoded.ok) {
|
|
11
|
+
return decoded.response;
|
|
12
|
+
}
|
|
13
|
+
const set = decoded.request;
|
|
14
|
+
const signal = signalOf(options);
|
|
15
|
+
const entries = set.entries.map((entry) => admitEntry(context, set, entry));
|
|
16
|
+
const reportEach = (error, code, marks) => {
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if ("report" in entry) {
|
|
19
|
+
entry.report(error, code, marks);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const seeded = await seededState(options, reportEach, {
|
|
24
|
+
path: ["operations"],
|
|
25
|
+
arguments: undefined,
|
|
26
|
+
});
|
|
27
|
+
if (!seeded.ok) {
|
|
28
|
+
return encodeOperationResponse(seeded.response, set.requestId);
|
|
29
|
+
}
|
|
30
|
+
const state = seeded.state ?? {};
|
|
31
|
+
let caller;
|
|
32
|
+
try {
|
|
33
|
+
caller = await context.owned?.caller({ origin: set.origin, state });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
reportEach(error, "A3000");
|
|
37
|
+
return encodeOperationResponse(internalFailure(), set.requestId);
|
|
38
|
+
}
|
|
39
|
+
const run = (entry) => "answered" in entry
|
|
40
|
+
? Promise.resolve(entry.answered)
|
|
41
|
+
: runEntry(entry, state, caller, set.requestId);
|
|
42
|
+
const cap = Math.min(maxAllConcurrencyOf(context.contracts[set.origin].limits), entries.length);
|
|
43
|
+
if (!set.streamed) {
|
|
44
|
+
const settled = new Map();
|
|
45
|
+
await runConcurrently(entries, cap, () => signal?.aborted === true, run, (entry, text) => {
|
|
46
|
+
settled.set(entry.key, text);
|
|
47
|
+
});
|
|
48
|
+
return encodeOperationSetResponse(entries.flatMap(({ key }) => {
|
|
49
|
+
const envelope = settled.get(key);
|
|
50
|
+
return envelope === undefined ? [] : [{ key, envelope }];
|
|
51
|
+
}), set.requestId);
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: streamedHeaders(set.requestId),
|
|
56
|
+
body: streamLines(entries, cap, signal, run),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function signalOf(options) {
|
|
60
|
+
const signal = options?.signal;
|
|
61
|
+
return typeof signal === "object" &&
|
|
62
|
+
signal !== null &&
|
|
63
|
+
typeof signal.aborted === "boolean" &&
|
|
64
|
+
typeof signal.addEventListener === "function"
|
|
65
|
+
? signal
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
function admitEntry(context, set, entry) {
|
|
69
|
+
const route = resolveOperationRoute(context.contracts, set.protocol, entry, set.origin);
|
|
70
|
+
if (!route.ok) {
|
|
71
|
+
return {
|
|
72
|
+
key: entry.key,
|
|
73
|
+
answered: encodeOperationResponse(route.failure, set.requestId).body,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const request = {
|
|
77
|
+
origin: set.origin,
|
|
78
|
+
requestId: set.requestId,
|
|
79
|
+
resource: entry.resource,
|
|
80
|
+
...route.operation,
|
|
81
|
+
protocol: set.protocol,
|
|
82
|
+
transport: set.transport,
|
|
83
|
+
arguments: entry.args,
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
key: entry.key,
|
|
87
|
+
admitted: admitOperation(context, request),
|
|
88
|
+
report: createDiagnosticsReporter(context.sink, request, undefined, entry.key),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
async function runEntry(entry, state, caller, requestId) {
|
|
92
|
+
const { admitted, key, report } = entry;
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await admitted.run({ ...admitted.request, state }, caller === undefined ? { key } : { caller, key });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
report(error, "A3000");
|
|
99
|
+
response = internalFailure();
|
|
100
|
+
}
|
|
101
|
+
return encodeOperationResponse(response, requestId, (error) => report(error, "A3003")).body;
|
|
102
|
+
}
|
|
103
|
+
function runConcurrently(entries, cap, stopped, run, settled) {
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
let next = 0;
|
|
106
|
+
let running = 0;
|
|
107
|
+
const launch = () => {
|
|
108
|
+
while (running < cap && next < entries.length && !stopped()) {
|
|
109
|
+
const entry = entries[next];
|
|
110
|
+
next += 1;
|
|
111
|
+
running += 1;
|
|
112
|
+
run(entry).then((text) => {
|
|
113
|
+
running -= 1;
|
|
114
|
+
settled(entry, text);
|
|
115
|
+
launch();
|
|
116
|
+
}, () => {
|
|
117
|
+
running -= 1;
|
|
118
|
+
launch();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (running === 0) {
|
|
122
|
+
resolve();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
launch();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function streamLines(entries, cap, signal, run) {
|
|
129
|
+
const queue = [];
|
|
130
|
+
let finished = false;
|
|
131
|
+
let halted = false;
|
|
132
|
+
const waiting = [];
|
|
133
|
+
const notify = () => {
|
|
134
|
+
for (const wake of waiting.splice(0)) {
|
|
135
|
+
wake();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
const stopped = () => halted || signal?.aborted === true;
|
|
139
|
+
const halt = () => {
|
|
140
|
+
halted = true;
|
|
141
|
+
signal?.removeEventListener("abort", notify);
|
|
142
|
+
notify();
|
|
143
|
+
};
|
|
144
|
+
signal?.addEventListener("abort", notify, { once: true });
|
|
145
|
+
void runConcurrently(entries, cap, stopped, run, (entry, text) => {
|
|
146
|
+
if (!stopped()) {
|
|
147
|
+
queue.push(operationSetLine(entry.key, text));
|
|
148
|
+
}
|
|
149
|
+
notify();
|
|
150
|
+
}).then(() => {
|
|
151
|
+
finished = true;
|
|
152
|
+
signal?.removeEventListener("abort", notify);
|
|
153
|
+
notify();
|
|
154
|
+
});
|
|
155
|
+
const end = { done: true, value: undefined };
|
|
156
|
+
const iterator = {
|
|
157
|
+
async next() {
|
|
158
|
+
while (!stopped()) {
|
|
159
|
+
const line = queue.shift();
|
|
160
|
+
if (line !== undefined) {
|
|
161
|
+
return { done: false, value: line };
|
|
162
|
+
}
|
|
163
|
+
if (finished) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
await new Promise((resolve) => {
|
|
167
|
+
waiting.push(resolve);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
halt();
|
|
171
|
+
return end;
|
|
172
|
+
},
|
|
173
|
+
async return() {
|
|
174
|
+
halt();
|
|
175
|
+
return end;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
return { [Symbol.asyncIterator]: () => iterator };
|
|
179
|
+
}
|
|
@@ -27,6 +27,11 @@ export declare function headerValue(normalized: Readonly<Record<string, string>>
|
|
|
27
27
|
* protocol and ClientContract it speaks, so there is nothing to compare.
|
|
28
28
|
*/
|
|
29
29
|
export declare function protocolIdentityOf(normalized: Readonly<Record<string, string>>): AvRequestProtocol | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The `Aventara-Protocol-Version` header as a number — `NaN` when it is not a
|
|
32
|
+
* decimal integer — or `undefined` when it is absent.
|
|
33
|
+
*/
|
|
34
|
+
export declare function protocolVersionOf(normalized: Readonly<Record<string, string>>): number | undefined;
|
|
30
35
|
/**
|
|
31
36
|
* The identity headers a request did not send, in the order the framework sends them —
|
|
32
37
|
* what the `A2000` answering it names. Empty exactly when {@link protocolIdentityOf}
|
|
@@ -39,4 +44,8 @@ export type IdentityHeaderName = (typeof headers)["protocolVersion"] | (typeof h
|
|
|
39
44
|
* A repeated header joins with `", "`, which holds a space, so it is never adopted.
|
|
40
45
|
*/
|
|
41
46
|
export declare function adoptedRequestId(normalized: Readonly<Record<string, string>>): string | undefined;
|
|
47
|
+
/** The header every answer to a GET carries: `X-Content-Type-Options: nosniff`. */
|
|
48
|
+
export declare const NO_SNIFF: Readonly<{
|
|
49
|
+
readonly "X-Content-Type-Options": "nosniff";
|
|
50
|
+
}>;
|
|
42
51
|
export {};
|
|
@@ -24,15 +24,19 @@ export function headerValue(normalized, name) {
|
|
|
24
24
|
}
|
|
25
25
|
const PROTOCOL_VERSION_TEXT = /^(?:0|[1-9]\d*)$/;
|
|
26
26
|
export function protocolIdentityOf(normalized) {
|
|
27
|
-
const version =
|
|
27
|
+
const version = protocolVersionOf(normalized);
|
|
28
28
|
const hash = headerValue(normalized, headers.contractHash);
|
|
29
29
|
if (version === undefined || hash === undefined) {
|
|
30
30
|
return undefined;
|
|
31
31
|
}
|
|
32
|
-
return {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
return { version, hash };
|
|
33
|
+
}
|
|
34
|
+
export function protocolVersionOf(normalized) {
|
|
35
|
+
const version = headerValue(normalized, headers.protocolVersion);
|
|
36
|
+
if (version === undefined) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
return PROTOCOL_VERSION_TEXT.test(version) ? Number(version) : Number.NaN;
|
|
36
40
|
}
|
|
37
41
|
export function missingIdentityHeaders(normalized) {
|
|
38
42
|
return [headers.protocolVersion, headers.contractHash].filter((name) => headerValue(normalized, name) === undefined);
|
|
@@ -44,3 +48,6 @@ export function adoptedRequestId(normalized) {
|
|
|
44
48
|
? value
|
|
45
49
|
: undefined;
|
|
46
50
|
}
|
|
51
|
+
export const NO_SNIFF = Object.freeze({
|
|
52
|
+
"X-Content-Type-Options": "nosniff",
|
|
53
|
+
});
|
|
@@ -6,7 +6,7 @@ import { type AvOperationIdentity } from "../operations/operation-identity.js";
|
|
|
6
6
|
*/
|
|
7
7
|
export type SurfaceRoute = ({
|
|
8
8
|
readonly kind: "operation";
|
|
9
|
-
readonly method: "POST";
|
|
9
|
+
readonly method: "POST" | "GET";
|
|
10
10
|
readonly path: `/_resources/${string}`;
|
|
11
11
|
readonly resource: string;
|
|
12
12
|
} & AvOperationIdentity) | {
|
|
@@ -21,12 +21,20 @@ export type SurfaceRoute = ({
|
|
|
21
21
|
readonly kind: "can";
|
|
22
22
|
readonly method: "POST";
|
|
23
23
|
readonly path: "/_access/can";
|
|
24
|
+
} | {
|
|
25
|
+
readonly kind: "all";
|
|
26
|
+
readonly method: "POST";
|
|
27
|
+
readonly path: "/_all";
|
|
24
28
|
};
|
|
29
|
+
/** Whether `client` advertises at least one operation: exactly when `/_all` is a route. */
|
|
30
|
+
export declare function advertisesAnyOperation(client: AvContractClient): boolean;
|
|
25
31
|
/**
|
|
26
32
|
* The route list of one ClientContract: every route a host mounts, from one list,
|
|
27
33
|
* deep-frozen — exactly the advertised operation triples in the Contract's own key
|
|
28
|
-
* order
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
34
|
+
* order (a `find` triple the Contract marks cacheable twice: `POST`, then `GET`),
|
|
35
|
+
* then `GET /_contract`, then `POST /_transactions` when, and only when,
|
|
36
|
+
* `transactions === "interactive"`, then `POST /_all` when, and only when, it
|
|
37
|
+
* advertises an operation, then `POST /_access/can` when, and only when, the
|
|
38
|
+
* Contract has its `access` section.
|
|
31
39
|
*/
|
|
32
40
|
export declare function surfaceOf(client: AvContractClient): readonly SurfaceRoute[];
|
|
@@ -2,6 +2,21 @@ import { lookupOwn, lookupResource } from "../contracts/contract-lookup.js";
|
|
|
2
2
|
import { isOperationVariantAvailable } from "../contracts/resource-operations.js";
|
|
3
3
|
import { parseOperationIdentity, } from "../operations/operation-identity.js";
|
|
4
4
|
const ownKeys = (record) => record === undefined ? [] : Object.keys(record);
|
|
5
|
+
export function advertisesAnyOperation(client) {
|
|
6
|
+
for (const resourceName of ownKeys(client.resources)) {
|
|
7
|
+
const advertised = lookupResource(client, resourceName)?.operations;
|
|
8
|
+
for (const familyName of ownKeys(advertised)) {
|
|
9
|
+
const family = lookupOwn(advertised, familyName);
|
|
10
|
+
for (const variantName of ownKeys(family)) {
|
|
11
|
+
if (parseOperationIdentity(familyName, variantName) !== undefined &&
|
|
12
|
+
isOperationVariantAvailable(lookupOwn(family, variantName))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
5
20
|
export function surfaceOf(client) {
|
|
6
21
|
const routes = [];
|
|
7
22
|
for (const resourceName of ownKeys(client.resources)) {
|
|
@@ -15,13 +30,24 @@ export function surfaceOf(client) {
|
|
|
15
30
|
!isOperationVariantAvailable(lookupOwn(family, variantName))) {
|
|
16
31
|
continue;
|
|
17
32
|
}
|
|
33
|
+
const path = `/_resources/${[resourceName, identity.family, identity.variant].map(encodeURIComponent).join("/")}`;
|
|
18
34
|
routes.push(Object.freeze({
|
|
19
35
|
kind: "operation",
|
|
20
36
|
method: "POST",
|
|
21
|
-
path
|
|
37
|
+
path,
|
|
22
38
|
resource: resourceName,
|
|
23
39
|
...identity,
|
|
24
40
|
}));
|
|
41
|
+
if (identity.family === "find" &&
|
|
42
|
+
lookupOwn(resource?.cache, identity.variant) !== undefined) {
|
|
43
|
+
routes.push(Object.freeze({
|
|
44
|
+
kind: "operation",
|
|
45
|
+
method: "GET",
|
|
46
|
+
path,
|
|
47
|
+
resource: resourceName,
|
|
48
|
+
...identity,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
25
51
|
}
|
|
26
52
|
}
|
|
27
53
|
}
|
|
@@ -37,6 +63,13 @@ export function surfaceOf(client) {
|
|
|
37
63
|
path: "/_transactions",
|
|
38
64
|
}));
|
|
39
65
|
}
|
|
66
|
+
if (advertisesAnyOperation(client)) {
|
|
67
|
+
routes.push(Object.freeze({
|
|
68
|
+
kind: "all",
|
|
69
|
+
method: "POST",
|
|
70
|
+
path: "/_all",
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
40
73
|
if (client.access !== undefined) {
|
|
41
74
|
routes.push(Object.freeze({
|
|
42
75
|
kind: "can",
|
|
@@ -17,7 +17,8 @@ export type AvProtocolBinding = {
|
|
|
17
17
|
* tagged by `kind`: one `POST` `operation` route per `(resource, family, variant)`
|
|
18
18
|
* it advertises and no other, then the `contract` route `GET /_contract`
|
|
19
19
|
* (always), then the `transactions` route `POST /_transactions` exactly when its
|
|
20
|
-
* `transactions` is `"interactive"`, then the `
|
|
20
|
+
* `transactions` is `"interactive"`, then the `all` route `POST /_all` exactly
|
|
21
|
+
* when it advertises an operation, then the `can` route `POST /_access/can`
|
|
21
22
|
* exactly when it has its `access` section. Each path is relative to
|
|
22
23
|
* `framework.api`. Read once at bind time: every call answers the same
|
|
23
24
|
* deep-frozen list, which is passive data a host may serialize.
|
|
@@ -103,6 +104,41 @@ export type AvProtocolBinding = {
|
|
|
103
104
|
/** Seeds the request's `ctx.$state`, as for `handleOperation`. */
|
|
104
105
|
readonly state?: () => AvState | Promise<AvState>;
|
|
105
106
|
}) => Promise<AvProtocolResponse>;
|
|
107
|
+
/**
|
|
108
|
+
* The `POST /_all` shortcut: one Operation Set — any mix of the operations the
|
|
109
|
+
* bound Contract advertises, under the caller's own identifier keys — decoded,
|
|
110
|
+
* its `ctx.$state` seeded once through `options.state` (one state every entry
|
|
111
|
+
* shares), its principal derived once, and every entry run concurrently,
|
|
112
|
+
* exactly as through its own route, at most `limits.maxAllConcurrency` at once.
|
|
113
|
+
* Answers `200 A1000` with `data` holding each key's own envelope, the one its
|
|
114
|
+
* single route writes. Under `Accept: application/x-ndjson` it answers `200`
|
|
115
|
+
* with `body` an async iterable of lines instead, one per entry as it settles:
|
|
116
|
+
* `{"key":…,"envelope":…}` and a newline. Refused whole only for its
|
|
117
|
+
* transport, identity, body shape or `limits.maxAll`, or when its seed or
|
|
118
|
+
* principal fails, as plain JSON at that code's status. When `options.signal`
|
|
119
|
+
* aborts (the client went away), entries not yet started never start and no
|
|
120
|
+
* more lines are written (a plain answer then holds only the entries that
|
|
121
|
+
* ran); so it is when the host stops reading. Absent (`404
|
|
122
|
+
* A2002`) on a Contract that advertises no operation. Total over any value, as
|
|
123
|
+
* `handleOperation` is.
|
|
124
|
+
*/
|
|
125
|
+
readonly handleAll: (request: AvProtocolRequest, options?: {
|
|
126
|
+
/** Seeds the set's one `ctx.$state`, as for `handleOperation`. */
|
|
127
|
+
readonly state?: () => AvState | Promise<AvState>;
|
|
128
|
+
/** The client's disconnect: entries not yet started never start. Any platform's `AbortSignal` is one. */
|
|
129
|
+
readonly signal?: {
|
|
130
|
+
readonly aborted: boolean;
|
|
131
|
+
readonly reason: unknown;
|
|
132
|
+
addEventListener(type: "abort", listener: () => void, options?: {
|
|
133
|
+
readonly once?: boolean;
|
|
134
|
+
}): void;
|
|
135
|
+
removeEventListener(type: "abort", listener: () => void): void;
|
|
136
|
+
};
|
|
137
|
+
}) => Promise<AvProtocolResponse | {
|
|
138
|
+
readonly status: 200;
|
|
139
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
140
|
+
readonly body: AsyncIterable<string>;
|
|
141
|
+
}>;
|
|
106
142
|
/**
|
|
107
143
|
* The `GET /_contract` responder: the bound ClientContract's JCS-canonical bytes,
|
|
108
144
|
* `ETag` its quoted hash, `Cache-Control: no-cache`, and `304` with no body when
|
|
@@ -5,6 +5,7 @@ import { answerAbsentRequest } from "./absent-route.responder.js";
|
|
|
5
5
|
import { contractDocumentOf, encodeContractRequest, } from "./contract-document.responder.js";
|
|
6
6
|
import { decodeOperationRequest, decodeTransactionRequest, } from "./http-request.decoder.js";
|
|
7
7
|
import { encodeOperationResponse, encodeTransactionResponse, } from "./http-response.encoder.js";
|
|
8
|
+
import { handleAllRequest, } from "./operation-set.handler.js";
|
|
8
9
|
import { handleCanRequest, handleOperationRequest, handleTransactionRequest, operationUnencodableReport, planUnencodableReport, } from "./protocol.handler.js";
|
|
9
10
|
import { transportFailure } from "./protocol-failure.js";
|
|
10
11
|
import { surfaceOf } from "./protocol-surface.js";
|
|
@@ -36,6 +37,7 @@ export function bind(framework, options) {
|
|
|
36
37
|
handleOperation: (request, options) => handleOperationRequest(handler, request, options),
|
|
37
38
|
handleTransaction: (request, options) => handleTransactionRequest(handler, request, options),
|
|
38
39
|
handleCan: (request, options) => handleCanRequest(handler, request, options),
|
|
40
|
+
handleAll: (request, options) => handleAllRequest(handler, request, options),
|
|
39
41
|
encodeContract: (request) => encodeContractRequest(document, request),
|
|
40
42
|
answerAbsent: (request) => answerAbsentRequest(contracts, request, sink, origin),
|
|
41
43
|
});
|
|
@@ -2,8 +2,9 @@ import type { AvContracts } from "../compiler/contract.compiler.js";
|
|
|
2
2
|
import type { RemoteRequestOrigin } from "../config/config-layer.js";
|
|
3
3
|
import type { AvFramework } from "../framework/framework.js";
|
|
4
4
|
import type { AccessQuestionsAnswerer } from "../policy/access-question.runtime.js";
|
|
5
|
-
import { type AvDiagnosticsSink } from "../runtime/diagnostics-reporter.js";
|
|
5
|
+
import { type AvDiagnosticsSink, createReporter } from "../runtime/diagnostics-reporter.js";
|
|
6
6
|
import type { RemoteOperationRequest } from "../runtime/operation-request.js";
|
|
7
|
+
import { type AvOperationFailureResponse, type AvOperationResponse } from "../runtime/operation-response.js";
|
|
7
8
|
import type { OwnedExecution } from "../runtime/owned-execution.registry.js";
|
|
8
9
|
import type { AvState } from "../runtime/request-state.js";
|
|
9
10
|
import { type AvProtocolRequest } from "./http-request.decoder.js";
|
|
@@ -14,7 +15,7 @@ import { type AvProtocolResponse, type UnencodableReport } from "./http-response
|
|
|
14
15
|
* decoder parsed without copying it again (absent for anything but a
|
|
15
16
|
* constructed Framework) — and its configured diagnostics sink.
|
|
16
17
|
*/
|
|
17
|
-
type HandlerContext = {
|
|
18
|
+
export type HandlerContext = {
|
|
18
19
|
readonly contracts: AvContracts;
|
|
19
20
|
/** The origin every request the binding decodes runs under. */
|
|
20
21
|
readonly origin: RemoteRequestOrigin;
|
|
@@ -25,11 +26,43 @@ type HandlerContext = {
|
|
|
25
26
|
/** The Framework's `can` answerer: absent for one without `access`, or anything `createFramework` did not return. */
|
|
26
27
|
readonly can: AccessQuestionsAnswerer | undefined;
|
|
27
28
|
};
|
|
29
|
+
/** A decoded operation as the shortcut runs it: the arguments it reads, and its entry. */
|
|
30
|
+
export type AdmittedOperation = {
|
|
31
|
+
readonly request: RemoteOperationRequest;
|
|
32
|
+
/**
|
|
33
|
+
* `member` is an Operation Set entry's (its principal, derived once for the
|
|
34
|
+
* set, and its key), which only a constructed Framework's owned entry reads.
|
|
35
|
+
*/
|
|
36
|
+
readonly run: (request: RemoteOperationRequest, member?: OperationSetMember) => Promise<AvOperationResponse>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* The entry a decoded operation runs through, with the arguments it reads, settled
|
|
40
|
+
* at the call, before the shortcut's first await. A body parsed here from raw
|
|
41
|
+
* content is fresh and runs uncopied. A host's `{ kind: "parsed" }` body is the
|
|
42
|
+
* host's object, so it is copied here, as `framework.execute` copies a server-side
|
|
43
|
+
* caller's arguments when called: a change the host makes after the call changes
|
|
44
|
+
* nothing, and a body that cannot be copied is answered as that door answers it
|
|
45
|
+
* (`A2004`/`V1001` at its path), never read again.
|
|
46
|
+
*/
|
|
47
|
+
export declare function admitOperation(context: HandlerContext, request: RemoteOperationRequest): AdmittedOperation;
|
|
48
|
+
/** What an Operation Set entry runs under besides its request: the set's principal, and its key. */
|
|
49
|
+
type OperationSetMember = NonNullable<Parameters<OwnedExecution["execute"]>[2]>;
|
|
28
50
|
/** What a host passes beside one protocol request. */
|
|
29
51
|
export type ProtocolCallOptions = {
|
|
30
52
|
/** Seeds the request's `ctx.$state`; called once, after decoding. Omitted, the state is `{}`. */
|
|
31
53
|
readonly state?: () => AvState | Promise<AvState>;
|
|
32
54
|
};
|
|
55
|
+
/** The request's seeded state, or the failure the seed's throw answers. */
|
|
56
|
+
export declare function seededState(options: ProtocolCallOptions | undefined, report: ReturnType<typeof createReporter>, view: {
|
|
57
|
+
readonly path: readonly (string | number)[];
|
|
58
|
+
readonly arguments: unknown;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
readonly ok: true;
|
|
61
|
+
readonly state: AvState | undefined;
|
|
62
|
+
} | {
|
|
63
|
+
readonly ok: false;
|
|
64
|
+
readonly response: AvOperationFailureResponse;
|
|
65
|
+
}>;
|
|
33
66
|
/**
|
|
34
67
|
* Where a plan's `A3003` goes: the Framework's diagnostics sink, as a `"transaction"`
|
|
35
68
|
* diagnostic — client origin, the request id the plan ran under, and the step the
|