@aventara/client 1.0.0-rc.10 → 1.0.0-rc.12
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 +61 -0
- package/README.md +110 -10
- package/dist/emit/cached-read.emitter.d.ts +3 -0
- package/dist/emit/cached-read.emitter.js +158 -0
- package/dist/emit/client-surface.emitter.js +56 -11
- package/dist/emit/client-tree.emitter.js +6 -1
- package/dist/emit/descriptor.emitter.d.ts +2 -0
- package/dist/emit/descriptor.emitter.js +28 -1
- package/dist/emit/emitted-tree.interface.d.ts +1 -1
- package/dist/emit/name.deriver.js +1 -0
- package/dist/emit/operation-set.emitter.d.ts +3 -0
- package/dist/emit/operation-set.emitter.js +604 -0
- package/dist/emit/runtime.emitter.js +386 -38
- package/package.json +3 -3
|
@@ -227,7 +227,7 @@ export async function askCan(
|
|
|
227
227
|
: question,
|
|
228
228
|
),
|
|
229
229
|
});
|
|
230
|
-
const response = await post(connection, "/_access/can", operation, headers, body, options);
|
|
230
|
+
const response = await post(connection, "/_access/can", operation, headers, body, options.signal);
|
|
231
231
|
return decoded(operation, response.status, () => {
|
|
232
232
|
const data: unknown = response.data;
|
|
233
233
|
if (!Array.isArray(data) || data.length !== questions.length || !data.every((answer) => typeof answer === "boolean")) {
|
|
@@ -269,7 +269,8 @@ import {
|
|
|
269
269
|
} from ${from("./errors")};
|
|
270
270
|
|
|
271
271
|
/**
|
|
272
|
-
* The transport of Aventara protocol version 1: one POST per operation,
|
|
272
|
+
* The transport of Aventara protocol version 1: one POST per operation, or one
|
|
273
|
+
* POST /_all for an Operation Set of them,
|
|
273
274
|
* sent with the protocol version and ClientContract hash, its response
|
|
274
275
|
* read by the protocol's rule: A1xxx resolves with the envelope's data, every other
|
|
275
276
|
* code throws its AvError subclass, and anything that is not a framework
|
|
@@ -297,6 +298,9 @@ const FRAMEWORK_HEADERS: Readonly<Record<string, string>> = {
|
|
|
297
298
|
[WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.contractHash)}]: CLIENT_CONTRACT_HASH,
|
|
298
299
|
};
|
|
299
300
|
|
|
301
|
+
/** The media type of a streamed Operation Set answer: one JSON line per entry. */
|
|
302
|
+
const NDJSON = "application/x-ndjson";
|
|
303
|
+
|
|
300
304
|
/** The request id header: AvCallOptions.requestId sends it. */
|
|
301
305
|
const REQUEST_ID_HEADER = WIRE_PREFIX + ${JSON.stringify(IDENTITY_HEADERS.requestId)};
|
|
302
306
|
|
|
@@ -307,9 +311,11 @@ export interface StructuralSignal {
|
|
|
307
311
|
|
|
308
312
|
/** The request the transport hands to fetch. */
|
|
309
313
|
export interface FetchInit {
|
|
310
|
-
|
|
314
|
+
/** POST, or GET for a cached read. */
|
|
315
|
+
readonly method: "POST" | "GET";
|
|
311
316
|
readonly headers: Readonly<Record<string, string>>;
|
|
312
|
-
|
|
317
|
+
/** The JSON body of a POST; a GET has none. */
|
|
318
|
+
readonly body?: string;
|
|
313
319
|
readonly signal?: PlatformSignal;
|
|
314
320
|
}
|
|
315
321
|
|
|
@@ -317,6 +323,16 @@ export interface FetchInit {
|
|
|
317
323
|
export interface FetchResponse {
|
|
318
324
|
readonly status: number;
|
|
319
325
|
text(): Promise<string>;
|
|
326
|
+
/** Read for an Operation Set's answer: its media type. */
|
|
327
|
+
readonly headers?: { get(name: string): string | null };
|
|
328
|
+
/** Read for a streamed Operation Set answer, where the runtime gives one. */
|
|
329
|
+
readonly body?: { getReader(): BodyReader } | null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** What the transport reads of a response body's stream. */
|
|
333
|
+
export interface BodyReader {
|
|
334
|
+
read(): Promise<{ readonly done: boolean; readonly value?: Uint8Array | undefined }>;
|
|
335
|
+
cancel(reason?: unknown): Promise<void>;
|
|
320
336
|
}
|
|
321
337
|
|
|
322
338
|
/** The AvFetch API as far as the transport calls it, where the consumer's lib declares none. */
|
|
@@ -346,15 +362,30 @@ export interface TransportConnection {
|
|
|
346
362
|
|
|
347
363
|
/** Per-call options. Never serialized into the operation body. */
|
|
348
364
|
export interface AvCallOptions {
|
|
349
|
-
/**
|
|
365
|
+
/**
|
|
366
|
+
* Aborts this call, and only this call. Sent in an Operation Set, the call
|
|
367
|
+
* rejects with the signal's reason (an AbortError) and the others go on; sent
|
|
368
|
+
* alone, it aborts the request, and the call rejects with what fetch rejected
|
|
369
|
+
* with. Aborted before it is sent, it is never sent.
|
|
370
|
+
*/
|
|
350
371
|
readonly signal?: PlatformSignal;
|
|
351
|
-
/**
|
|
372
|
+
/**
|
|
373
|
+
* Extra request headers. The framework's own headers cannot be set here. A
|
|
374
|
+
* call with headers is sent alone, never in an Operation Set.
|
|
375
|
+
*/
|
|
352
376
|
readonly headers?: Readonly<Record<string, string>>;
|
|
353
377
|
/**
|
|
354
378
|
* Sent as the request id header (Aventara-Request-Id); the empty string is
|
|
355
|
-
* absent. It wins over that header in headers, which it overwrites.
|
|
379
|
+
* absent. It wins over that header in headers, which it overwrites. A call
|
|
380
|
+
* with a request id is sent alone, never in an Operation Set.
|
|
356
381
|
*/
|
|
357
382
|
readonly requestId?: string;
|
|
383
|
+
/**
|
|
384
|
+
* false sends this operation call to its own route, never in an Operation Set
|
|
385
|
+
* (POST /_all) with the calls made beside it. A transaction is always
|
|
386
|
+
* sent alone.
|
|
387
|
+
*/
|
|
388
|
+
readonly grouping?: boolean;
|
|
358
389
|
}
|
|
359
390
|
|
|
360
391
|
type SuccessCode = Exclude<AvOperationCode, AvOperationErrorCode>;
|
|
@@ -405,6 +436,80 @@ export function refuseUnsentValues(
|
|
|
405
436
|
);
|
|
406
437
|
}
|
|
407
438
|
|
|
439
|
+
/** One operation, checked and encoded when it was called: what any request for it sends. */
|
|
440
|
+
export interface PreparedOperation {
|
|
441
|
+
readonly resource: string;
|
|
442
|
+
readonly family: string;
|
|
443
|
+
readonly variant: string;
|
|
444
|
+
/** How a failure names it: \`"Post" find.many\`. */
|
|
445
|
+
readonly label: string;
|
|
446
|
+
/** Its request headers: the framework's, and the caller's when it is sent alone. */
|
|
447
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
448
|
+
/** The caller's headers alone (its request id among them): what a cached read's GET sends. */
|
|
449
|
+
readonly callerHeaders: Readonly<Record<string, string>>;
|
|
450
|
+
/** Its arguments in wire form, as JSON text: its own route's body. */
|
|
451
|
+
readonly body: string;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Checks and encodes one operation call, sending nothing: the checks
|
|
456
|
+
* {@link execute} runs before it sends, in its order, and with its errors.
|
|
457
|
+
*/
|
|
458
|
+
export function prepareOperation(
|
|
459
|
+
resource: string,
|
|
460
|
+
family: string,
|
|
461
|
+
variant: string,
|
|
462
|
+
args: Readonly<Record<string, unknown>>,
|
|
463
|
+
options: AvCallOptions = {},
|
|
464
|
+
check?: (resource: string, family: string, args: unknown) => void,
|
|
465
|
+
): PreparedOperation {
|
|
466
|
+
refuseUnsentValues(resource, family, variant, args, ["arguments"]);
|
|
467
|
+
check?.(resource, family, args);
|
|
468
|
+
const label = JSON.stringify(resource) + " " + family + "." + variant;
|
|
469
|
+
const callerHeaders = callerHeadersOf(options);
|
|
470
|
+
const headers = { ...FRAMEWORK_HEADERS, ...callerHeaders };
|
|
471
|
+
const body = serializeWireBody(encodeArguments(argumentObject(args)));
|
|
472
|
+
return { resource, family, variant, label, headers, callerHeaders, body };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Sends one prepared operation to its own route and settles its envelope.
|
|
477
|
+
*
|
|
478
|
+
* @throws as {@link execute} does once its arguments are checked.
|
|
479
|
+
*/
|
|
480
|
+
export async function sendOperation(
|
|
481
|
+
connection: TransportConnection,
|
|
482
|
+
operation: PreparedOperation,
|
|
483
|
+
signal?: PlatformSignal,
|
|
484
|
+
): Promise<unknown> {
|
|
485
|
+
const response = await post(connection, routeOf(operation), operation.label, operation.headers, operation.body, signal);
|
|
486
|
+
return decoded(operation.label, response.status, () => decodeResult(DECODE_TABLE, operation.resource, response.data));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** An operation's own route: \`/_resources/<resource>/<family>/<variant>\`. */
|
|
490
|
+
export function routeOf(operation: PreparedOperation): string {
|
|
491
|
+
return "/_resources/" + [operation.resource, operation.family, operation.variant].map(encodeURIComponent).join("/");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Sends one cached read as a GET to its own route with \`query\` (its \`q\` and
|
|
496
|
+
* \`v\`) and only the caller's headers, and settles its envelope as
|
|
497
|
+
* {@link sendOperation} does.
|
|
498
|
+
*/
|
|
499
|
+
export async function sendRead(
|
|
500
|
+
connection: TransportConnection,
|
|
501
|
+
operation: PreparedOperation,
|
|
502
|
+
query: string,
|
|
503
|
+
signal?: PlatformSignal,
|
|
504
|
+
): Promise<unknown> {
|
|
505
|
+
const init: FetchInit =
|
|
506
|
+
signal === undefined
|
|
507
|
+
? { method: "GET", headers: operation.callerHeaders }
|
|
508
|
+
: { method: "GET", headers: operation.callerHeaders, signal };
|
|
509
|
+
const response = await received(connection, routeOf(operation) + "?" + query, operation.label, init);
|
|
510
|
+
return decoded(operation.label, response.status, () => decodeResult(DECODE_TABLE, operation.resource, response.data));
|
|
511
|
+
}
|
|
512
|
+
|
|
408
513
|
/**
|
|
409
514
|
* Runs one operation: POSTs its arguments and settles its envelope.
|
|
410
515
|
*
|
|
@@ -428,14 +533,7 @@ export async function execute(
|
|
|
428
533
|
options: AvCallOptions = {},
|
|
429
534
|
check?: (resource: string, family: string, args: unknown) => void,
|
|
430
535
|
): Promise<unknown> {
|
|
431
|
-
|
|
432
|
-
check?.(resource, family, args);
|
|
433
|
-
const operation = JSON.stringify(resource) + " " + family + "." + variant;
|
|
434
|
-
const headers = requestHeaders(options);
|
|
435
|
-
const body = serializeWireBody(encodeArguments(argumentObject(args)));
|
|
436
|
-
const path = "/_resources/" + [resource, family, variant].map(encodeURIComponent).join("/");
|
|
437
|
-
const response = await post(connection, path, operation, headers, body, options);
|
|
438
|
-
return decoded(operation, response.status, () => decodeResult(DECODE_TABLE, resource, response.data));
|
|
536
|
+
return sendOperation(connection, prepareOperation(resource, family, variant, args, options, check), options.signal);
|
|
439
537
|
}
|
|
440
538
|
|
|
441
539
|
/**
|
|
@@ -455,7 +553,7 @@ export async function executePlan(
|
|
|
455
553
|
const operation = "the transaction";
|
|
456
554
|
const headers = requestHeaders(options);
|
|
457
555
|
const body = serializeWireBody(plan);
|
|
458
|
-
const response = await post(connection, "/_transactions", operation, headers, body, options);
|
|
556
|
+
const response = await post(connection, "/_transactions", operation, headers, body, options.signal);
|
|
459
557
|
return decoded(operation, response.status, () => {
|
|
460
558
|
if (!Array.isArray(response.data) || response.data.length !== resources.length) {
|
|
461
559
|
throw new TypeError("A committed plan's data must hold one result per operation.");
|
|
@@ -471,38 +569,279 @@ async function post(
|
|
|
471
569
|
operation: string,
|
|
472
570
|
headers: Readonly<Record<string, string>>,
|
|
473
571
|
body: string,
|
|
474
|
-
|
|
572
|
+
signal: PlatformSignal | undefined,
|
|
573
|
+
): Promise<{ readonly status: number; readonly data: unknown }> {
|
|
574
|
+
const init: FetchInit = signal === undefined ? { method: "POST", headers, body } : { method: "POST", headers, body, signal };
|
|
575
|
+
return received(connection, path, operation, init);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** One request and its settled envelope data, still in wire form. */
|
|
579
|
+
async function received(
|
|
580
|
+
connection: TransportConnection,
|
|
581
|
+
path: string,
|
|
582
|
+
operation: string,
|
|
583
|
+
init: FetchInit,
|
|
475
584
|
): Promise<{ readonly status: number; readonly data: unknown }> {
|
|
585
|
+
const signal = init.signal;
|
|
586
|
+
const response = await send(connection, path, operation, init);
|
|
587
|
+
let text: string;
|
|
588
|
+
try {
|
|
589
|
+
text = await response.text();
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (signal?.aborted === true) {
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
throw new AvTransportError(
|
|
595
|
+
"The response to " + operation + " (HTTP " + response.status + ") could not be read.",
|
|
596
|
+
response.status,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return { status: response.status, data: settle(operation, response.status, text) };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** One request, until its response arrives; a caller's abort rejects with what fetch rejected with. */
|
|
603
|
+
async function send(
|
|
604
|
+
connection: TransportConnection,
|
|
605
|
+
path: string,
|
|
606
|
+
operation: string,
|
|
607
|
+
init: FetchInit,
|
|
608
|
+
): Promise<FetchResponse> {
|
|
476
609
|
const url = connection.entrypoint + path;
|
|
477
|
-
const
|
|
478
|
-
options.signal === undefined
|
|
479
|
-
? { method: "POST", headers, body }
|
|
480
|
-
: { method: "POST", headers, body, signal: options.signal };
|
|
610
|
+
const signal = init.signal;
|
|
481
611
|
|
|
482
612
|
// Called unbound: a browser's fetch refuses a this that is not the global.
|
|
483
613
|
const fetch = connection.fetch as StructuralFetch;
|
|
484
|
-
let response: FetchResponse;
|
|
485
614
|
try {
|
|
486
|
-
|
|
615
|
+
return await fetch(url, init);
|
|
487
616
|
} catch (error) {
|
|
488
|
-
if (
|
|
617
|
+
if (signal?.aborted === true) {
|
|
489
618
|
throw error;
|
|
490
619
|
}
|
|
491
620
|
throw new AvTransportError("The request for " + operation + " failed before any response arrived.", null);
|
|
492
621
|
}
|
|
493
|
-
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** How one entry of an Operation Set settled: its decoded data, or what its call rejects with. */
|
|
625
|
+
export type EntryOutcome =
|
|
626
|
+
| { readonly kind: "value"; readonly value: unknown }
|
|
627
|
+
| { readonly kind: "error"; readonly error: unknown };
|
|
628
|
+
|
|
629
|
+
/** What an Operation Set request came to: its entries answered, or no /_all on the server (A2002, A2000). */
|
|
630
|
+
export type OperationSetAnswer = "answered" | "absent";
|
|
631
|
+
|
|
632
|
+
/** How errors name an Operation Set request. */
|
|
633
|
+
const SET_LABEL = "the Operation Set";
|
|
634
|
+
|
|
635
|
+
/** The key the client gives the entry at \`index\`: an identifier, never the caller's. */
|
|
636
|
+
export function operationSetKey(index: number): string {
|
|
637
|
+
return "k" + index;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/** One entry of an Operation Set's body, as JSON text, under \`key\`. */
|
|
641
|
+
export function operationSetEntry(key: string, operation: PreparedOperation): string {
|
|
642
|
+
return (
|
|
643
|
+
JSON.stringify(key) +
|
|
644
|
+
':{"resource":' +
|
|
645
|
+
JSON.stringify(operation.resource) +
|
|
646
|
+
',"family":' +
|
|
647
|
+
JSON.stringify(operation.family) +
|
|
648
|
+
',"variant":' +
|
|
649
|
+
JSON.stringify(operation.variant) +
|
|
650
|
+
',"args":' +
|
|
651
|
+
operation.body +
|
|
652
|
+
"}"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** The JSON text of an Operation Set's body, around its entries' text. */
|
|
657
|
+
export function operationSetBody(entries: readonly string[]): string {
|
|
658
|
+
return '{"operations":{' + entries.join(",") + "}}";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Sends prepared operations as one Operation Set (\`POST /_all\`), each under a key
|
|
663
|
+
* the client makes, and hands each entry's outcome to \`answer\` as it arrives: line
|
|
664
|
+
* by line when the answer is streamed (NDJSON, asked for where the runtime can
|
|
665
|
+
* stream), all at once from a plain JSON answer. Each entry decodes and fails as
|
|
666
|
+
* its own route would answer it. A request refused whole answers every entry with
|
|
667
|
+
* that refusal, except A2002 or A2000, which answer none: the server has no /_all.
|
|
668
|
+
* A response that is not an Aventara answer answers each entry still waiting with
|
|
669
|
+
* AvTransportError. Never rejects.
|
|
670
|
+
*/
|
|
671
|
+
export async function sendOperationSet(
|
|
672
|
+
connection: TransportConnection,
|
|
673
|
+
operations: readonly PreparedOperation[],
|
|
674
|
+
signal: PlatformSignal | undefined,
|
|
675
|
+
answer: (index: number, outcome: EntryOutcome) => void,
|
|
676
|
+
): Promise<OperationSetAnswer> {
|
|
677
|
+
const keys = new Map(operations.map((_, index) => [operationSetKey(index), index]));
|
|
678
|
+
const waiting = new Set(keys.values());
|
|
679
|
+
const settleEntry = (index: number, outcome: EntryOutcome): void => {
|
|
680
|
+
if (waiting.delete(index)) {
|
|
681
|
+
answer(index, outcome);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
const failWaiting = (error: unknown): OperationSetAnswer => {
|
|
685
|
+
for (const index of [...waiting]) {
|
|
686
|
+
settleEntry(index, { kind: "error", error });
|
|
687
|
+
}
|
|
688
|
+
return "answered";
|
|
689
|
+
};
|
|
690
|
+
const entryOf = (index: number, status: number, envelope: unknown): EntryOutcome => {
|
|
691
|
+
const operation = operations[index] as PreparedOperation;
|
|
692
|
+
try {
|
|
693
|
+
const data = settleEnvelope(operation.label, status, envelope);
|
|
694
|
+
return { kind: "value", value: decoded(operation.label, status, () => decodeResult(DECODE_TABLE, operation.resource, data)) };
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return { kind: "error", error };
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
const streaming = canStream();
|
|
700
|
+
const headers: Record<string, string> = streaming ? { ...FRAMEWORK_HEADERS, Accept: NDJSON } : { ...FRAMEWORK_HEADERS };
|
|
701
|
+
const body = operationSetBody(operations.map((operation, index) => operationSetEntry(operationSetKey(index), operation)));
|
|
702
|
+
|
|
703
|
+
let response: FetchResponse;
|
|
494
704
|
try {
|
|
495
|
-
|
|
705
|
+
response = await send(
|
|
706
|
+
connection,
|
|
707
|
+
"/_all",
|
|
708
|
+
SET_LABEL,
|
|
709
|
+
signal === undefined ? { method: "POST", headers, body } : { method: "POST", headers, body, signal },
|
|
710
|
+
);
|
|
496
711
|
} catch (error) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
712
|
+
return failWaiting(error);
|
|
713
|
+
}
|
|
714
|
+
const unreadable = (): AvTransportError =>
|
|
715
|
+
new AvTransportError("The response to " + SET_LABEL + " (HTTP " + response.status + ") could not be read.", response.status);
|
|
716
|
+
const malformed = (): AvTransportError =>
|
|
717
|
+
new AvTransportError(
|
|
718
|
+
"The response to " + SET_LABEL + " (HTTP " + response.status + ") is not an Aventara Operation Set answer.",
|
|
502
719
|
response.status,
|
|
503
720
|
);
|
|
721
|
+
|
|
722
|
+
const mediaType = (response.headers?.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
|
|
723
|
+
if (mediaType !== NDJSON) {
|
|
724
|
+
let text: string;
|
|
725
|
+
try {
|
|
726
|
+
text = await response.text();
|
|
727
|
+
} catch {
|
|
728
|
+
return failWaiting(unreadable());
|
|
729
|
+
}
|
|
730
|
+
const envelope = envelopeOf(parseJson(text));
|
|
731
|
+
if (envelope === undefined) {
|
|
732
|
+
return failWaiting(
|
|
733
|
+
new AvTransportError(
|
|
734
|
+
"The response to " + SET_LABEL + " (HTTP " + response.status + ") is not an Aventara response envelope.",
|
|
735
|
+
response.status,
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
if (envelope.outcome === "failure") {
|
|
740
|
+
// A server without /_all: A2002 (no such route), or A2000 from a server older
|
|
741
|
+
// than Operation Sets, which reads /_all as a malformed operation path.
|
|
742
|
+
return envelope.code === "A2002" || envelope.code === "A2000"
|
|
743
|
+
? "absent"
|
|
744
|
+
: failWaiting(frameworkErrorOf(envelope.code, envelope.cause));
|
|
745
|
+
}
|
|
746
|
+
const entries = envelope.data;
|
|
747
|
+
if (!isRecord(entries)) {
|
|
748
|
+
return failWaiting(malformed());
|
|
749
|
+
}
|
|
750
|
+
for (const [key, index] of keys) {
|
|
751
|
+
if (Object.hasOwn(entries, key)) {
|
|
752
|
+
settleEntry(index, entryOf(index, response.status, entries[key]));
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return failWaiting(
|
|
756
|
+
new AvTransportError(
|
|
757
|
+
"The response to " + SET_LABEL + " (HTTP " + response.status + ") holds no answer for this call.",
|
|
758
|
+
response.status,
|
|
759
|
+
),
|
|
760
|
+
);
|
|
504
761
|
}
|
|
505
|
-
|
|
762
|
+
|
|
763
|
+
// One line: {"key": …, "envelope": …}. false when it is not one of this request's.
|
|
764
|
+
const readLine = (line: string): boolean => {
|
|
765
|
+
if (line.trim() === "") {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
const parsed = parseJson(line);
|
|
769
|
+
if (!isRecord(parsed) || !Object.hasOwn(parsed, "envelope") || typeof parsed["key"] !== "string") {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
const index = keys.get(parsed["key"]);
|
|
773
|
+
if (index === undefined) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
settleEntry(index, entryOf(index, response.status, parsed["envelope"]));
|
|
777
|
+
return true;
|
|
778
|
+
};
|
|
779
|
+
const ended = (): OperationSetAnswer =>
|
|
780
|
+
failWaiting(
|
|
781
|
+
new AvTransportError(
|
|
782
|
+
"The response to " + SET_LABEL + " (HTTP " + response.status + ") ended before it answered this call.",
|
|
783
|
+
response.status,
|
|
784
|
+
),
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const reader = streaming ? response.body?.getReader() : undefined;
|
|
788
|
+
if (reader === undefined) {
|
|
789
|
+
let text: string;
|
|
790
|
+
try {
|
|
791
|
+
text = await response.text();
|
|
792
|
+
} catch {
|
|
793
|
+
return failWaiting(unreadable());
|
|
794
|
+
}
|
|
795
|
+
for (const line of text.split("\\n")) {
|
|
796
|
+
if (!readLine(line)) {
|
|
797
|
+
return failWaiting(malformed());
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return ended();
|
|
801
|
+
}
|
|
802
|
+
const decoder = textDecoder();
|
|
803
|
+
let buffer = "";
|
|
804
|
+
for (;;) {
|
|
805
|
+
let chunk: { readonly done: boolean; readonly value?: Uint8Array | undefined };
|
|
806
|
+
try {
|
|
807
|
+
chunk = await reader.read();
|
|
808
|
+
} catch {
|
|
809
|
+
return failWaiting(unreadable());
|
|
810
|
+
}
|
|
811
|
+
buffer += decoder.decode(chunk.value, { stream: !chunk.done });
|
|
812
|
+
let newline = buffer.indexOf("\\n");
|
|
813
|
+
while (newline !== -1) {
|
|
814
|
+
if (!readLine(buffer.slice(0, newline))) {
|
|
815
|
+
reader.cancel().catch((): void => undefined);
|
|
816
|
+
return failWaiting(malformed());
|
|
817
|
+
}
|
|
818
|
+
buffer = buffer.slice(newline + 1);
|
|
819
|
+
newline = buffer.indexOf("\\n");
|
|
820
|
+
}
|
|
821
|
+
if (chunk.done) {
|
|
822
|
+
return readLine(buffer) ? ended() : failWaiting(malformed());
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** What the transport reads of a TextDecoder, where the consumer's lib declares none. */
|
|
828
|
+
interface StreamDecoder {
|
|
829
|
+
decode(input?: Uint8Array, options?: { readonly stream?: boolean }): string;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/** Whether this runtime can read an answer as it streams: a stream reader and a text decoder. */
|
|
833
|
+
function canStream(): boolean {
|
|
834
|
+
const platform = globalThis as {
|
|
835
|
+
readonly ReadableStream?: { readonly prototype?: { readonly getReader?: unknown } };
|
|
836
|
+
readonly TextDecoder?: unknown;
|
|
837
|
+
};
|
|
838
|
+
return typeof platform.ReadableStream?.prototype?.getReader === "function" && typeof platform.TextDecoder === "function";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/** The platform's TextDecoder, for UTF-8 (canStream says one exists). */
|
|
842
|
+
function textDecoder(): StreamDecoder {
|
|
843
|
+
const Platform = (globalThis as unknown as { readonly TextDecoder: new () => StreamDecoder }).TextDecoder;
|
|
844
|
+
return new Platform();
|
|
506
845
|
}
|
|
507
846
|
|
|
508
847
|
/**
|
|
@@ -529,7 +868,12 @@ function decoded<T>(operation: string, status: number, decode: () => T): T {
|
|
|
529
868
|
* this client cannot read, which is what AvTransportError means.
|
|
530
869
|
*/
|
|
531
870
|
function settle(operation: string, status: number, text: string): unknown {
|
|
532
|
-
|
|
871
|
+
return settleEnvelope(operation, status, parseJson(text));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Settles one parsed response body (or one Operation Set entry's envelope) by the same rule. */
|
|
875
|
+
function settleEnvelope(operation: string, status: number, body: unknown): unknown {
|
|
876
|
+
const envelope = envelopeOf(body);
|
|
533
877
|
if (envelope === undefined) {
|
|
534
878
|
throw new AvTransportError(
|
|
535
879
|
"The response to " + operation + " (HTTP " + status + ") is not an Aventara response envelope.",
|
|
@@ -680,13 +1024,17 @@ function argumentObject(args: unknown): unknown {
|
|
|
680
1024
|
return args;
|
|
681
1025
|
}
|
|
682
1026
|
|
|
1027
|
+
/** The framework's headers plus the caller's (see callerHeadersOf). */
|
|
1028
|
+
function requestHeaders(options: AvCallOptions): Record<string, string> {
|
|
1029
|
+
return { ...FRAMEWORK_HEADERS, ...callerHeadersOf(options) };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
683
1032
|
/**
|
|
684
|
-
* The
|
|
685
|
-
*
|
|
686
|
-
* caller set.
|
|
1033
|
+
* The caller's headers, refusing one that names one of the framework's; then
|
|
1034
|
+
* requestId, which overwrites any request id header the caller set.
|
|
687
1035
|
*/
|
|
688
|
-
function
|
|
689
|
-
const headers: Record<string, string> = {
|
|
1036
|
+
function callerHeadersOf(options: AvCallOptions): Record<string, string> {
|
|
1037
|
+
const headers: Record<string, string> = {};
|
|
690
1038
|
const owned = new Set(Object.keys(FRAMEWORK_HEADERS).map((name) => name.toLowerCase()));
|
|
691
1039
|
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
692
1040
|
if (owned.has(name.toLowerCase())) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aventara/client",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.12",
|
|
4
4
|
"license": "SEE LICENSE IN LICENSE",
|
|
5
5
|
"description": "Development-time generator for Aventara typed remote clients.",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"CHANGELOG.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@aventara/core": "1.0.0-rc.
|
|
31
|
+
"@aventara/core": "1.0.0-rc.12",
|
|
32
32
|
"c12": "3.3.4",
|
|
33
33
|
"get-tsconfig": "4.10.0"
|
|
34
34
|
},
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@aventara/testing": "1.0.0-rc.
|
|
47
|
+
"@aventara/testing": "1.0.0-rc.12",
|
|
48
48
|
"@types/node": "24.10.1",
|
|
49
49
|
"typescript": "^5.9.2"
|
|
50
50
|
},
|