@aventara/client 1.0.0-rc.12 → 1.0.0-rc.13
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/README.md +60 -10
- package/dist/cli/command.parser.d.ts +6 -2
- package/dist/cli/command.parser.js +20 -1
- package/dist/cli.js +17 -2
- package/dist/config/config.resolver.js +1 -1
- package/dist/emit/access.emitter.js +2 -2
- package/dist/emit/client-surface.emitter.js +81 -13
- package/dist/emit/client-tree.emitter.js +3 -3
- package/dist/emit/name.deriver.js +2 -0
- package/dist/emit/operation-set.emitter.d.ts +1 -1
- package/dist/emit/operation-set.emitter.js +346 -29
- package/dist/emit/runtime.emitter.js +171 -2
- package/dist/init/client-init.orchestrator.js +13 -1
- package/dist/skill/client-skill.generated.d.ts +2 -0
- package/dist/skill/client-skill.generated.js +4 -0
- package/dist/skill/client-skill.writer.d.ts +16 -0
- package/dist/skill/client-skill.writer.js +63 -0
- package/package.json +4 -3
|
@@ -1,21 +1,77 @@
|
|
|
1
1
|
import { moduleSpecifierWriter, } from "./module-style.interface.js";
|
|
2
|
-
export function emitOperationSetModule(style, cached = false) {
|
|
2
|
+
export function emitOperationSetModule(style, cached = false, access = false) {
|
|
3
3
|
return {
|
|
4
4
|
path: "runtime/operation-set.ts",
|
|
5
|
-
source: operationSetModuleSource(moduleSpecifierWriter(style), cached)
|
|
5
|
+
source: operationSetModuleSource(moduleSpecifierWriter(style), cached) +
|
|
6
|
+
(access ? ACCESS_CALL_SOURCE : ""),
|
|
6
7
|
};
|
|
7
8
|
}
|
|
9
|
+
const ACCESS_CALL_SOURCE = `
|
|
10
|
+
/** One \`$access\` call, as the hooks see it; only \`can\` sends a request id. */
|
|
11
|
+
export function accessCall(verb: string, options: AvCallOptions): CallIdentity {
|
|
12
|
+
return identified({ kind: "access", verb, label: "$access." + verb }, verb === "can" ? options.requestId : undefined);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Runs one \`$access\` call under the hooks: onCall (awaited when async, unless
|
|
17
|
+
* its signal aborts first), then \`run\` with its additions, then onSuccess or
|
|
18
|
+
* onError. Settles as \`run\` does.
|
|
19
|
+
*/
|
|
20
|
+
export async function observeCall(
|
|
21
|
+
hooks: CallHooks,
|
|
22
|
+
call: CallIdentity,
|
|
23
|
+
signal: PlatformSignal | undefined,
|
|
24
|
+
run: (additions: CallAdditions) => Promise<unknown>,
|
|
25
|
+
): Promise<unknown> {
|
|
26
|
+
const additions = startCall(hooks, call);
|
|
27
|
+
let value: unknown;
|
|
28
|
+
try {
|
|
29
|
+
value = await run(additions instanceof Promise ? await untilAborted(additions, signal) : additions);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
endCall(hooks, call, { kind: "error", error });
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
endCall(hooks, call, { kind: "value", value });
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** An async onCall's additions, or the signal's reason as soon as it aborts first. */
|
|
39
|
+
function untilAborted(additions: Promise<CallAdditions>, signal: PlatformSignal | undefined): Promise<CallAdditions> {
|
|
40
|
+
const source = signal as unknown as AbortSource | undefined;
|
|
41
|
+
if (source === undefined) {
|
|
42
|
+
return additions;
|
|
43
|
+
}
|
|
44
|
+
return new Promise<CallAdditions>((resolve, reject) => {
|
|
45
|
+
const aborted = (): void => reject(abortReason(source));
|
|
46
|
+
if (source.aborted) {
|
|
47
|
+
aborted();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
source.addEventListener?.("abort", aborted);
|
|
51
|
+
void additions.then((added) => {
|
|
52
|
+
source.removeEventListener?.("abort", aborted);
|
|
53
|
+
resolve(added);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
const HOOK_THREW_WARNING = "AVENTARA_CLIENT_HOOK_THREW";
|
|
59
|
+
const HOOK_HEADER_DROPPED_WARNING = "AVENTARA_CLIENT_HOOK_HEADER_DROPPED";
|
|
8
60
|
function operationSetModuleSource(from, cached) {
|
|
9
61
|
return `${cached ? `import { isCachedRead, sendCachedRead } from ${from("./cached-read")};\n` : ""}import { OPERATION_SET_LIMITS } from ${from("./descriptor")};
|
|
10
62
|
import {
|
|
11
63
|
type AvCallOptions,
|
|
64
|
+
type CallAdditions,
|
|
12
65
|
type EntryOutcome,
|
|
66
|
+
labelOf,
|
|
67
|
+
NO_ADDITIONS,
|
|
13
68
|
operationSetBody,
|
|
14
69
|
operationSetEntry,
|
|
15
70
|
operationSetKey,
|
|
16
71
|
type PlatformSignal,
|
|
17
72
|
type PreparedOperation,
|
|
18
73
|
prepareOperation,
|
|
74
|
+
readAdditions,
|
|
19
75
|
sendOperation,
|
|
20
76
|
sendOperationSet,
|
|
21
77
|
type TransportConnection,
|
|
@@ -34,7 +90,193 @@ import {
|
|
|
34
90
|
*
|
|
35
91
|
* A call's signal aborts that call only. Aborted before it is sent, it is never
|
|
36
92
|
* sent. Nothing is aborted for you: there is no default timeout.
|
|
93
|
+
*
|
|
94
|
+
* The client's hooks see every call: onCall when it is made, and may add to its
|
|
95
|
+
* request; onSuccess or onError once it settles. Calls whose additions differ
|
|
96
|
+
* never share an Operation Set.
|
|
97
|
+
*/
|
|
98
|
+
|
|
99
|
+
/** One call as the hooks see it, frozen: what it is, never its arguments. */
|
|
100
|
+
export type CallIdentity =
|
|
101
|
+
| {
|
|
102
|
+
readonly kind: "operation";
|
|
103
|
+
readonly resource: string;
|
|
104
|
+
readonly family: string;
|
|
105
|
+
readonly variant: string;
|
|
106
|
+
readonly label: string;
|
|
107
|
+
readonly requestId?: string;
|
|
108
|
+
}
|
|
109
|
+
| { readonly kind: "transaction"; readonly label: string; readonly requestId?: string }
|
|
110
|
+
| { readonly kind: "access"; readonly verb: string; readonly label: string; readonly requestId?: string };
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The client's hooks, as its options hold them. Typed there by the client's own
|
|
114
|
+
* calls; the queue hands each only the calls this client makes.
|
|
115
|
+
*/
|
|
116
|
+
export interface CallHooks {
|
|
117
|
+
readonly onCall?: ((call: never) => unknown) | undefined;
|
|
118
|
+
readonly onSuccess?: ((call: never, result: never) => unknown) | undefined;
|
|
119
|
+
readonly onError?: ((call: never, error: never) => unknown) | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The call, frozen, with the request id it sends (the empty string is none). */
|
|
123
|
+
function identified(call: CallIdentity, requestId: string | undefined): CallIdentity {
|
|
124
|
+
if (requestId !== undefined && requestId !== "") {
|
|
125
|
+
(call as { requestId?: string }).requestId = requestId;
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze(call);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** An operation call, as the hooks see it. */
|
|
131
|
+
function operationCall(resource: string, family: string, variant: string, options: AvCallOptions): CallIdentity {
|
|
132
|
+
return identified({ kind: "operation", resource, family, variant, label: labelOf(resource, family, variant) }, options.requestId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A transaction, as the hooks see it: one call, whatever its steps. */
|
|
136
|
+
export function transactionCall(options: AvCallOptions): CallIdentity {
|
|
137
|
+
return identified({ kind: "transaction", label: "the transaction" }, options.requestId);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
/** What the platform's process offers for a warning, where there is one. */
|
|
142
|
+
type WarningEmitter = (message: string, options: { readonly type: string; readonly code: string }) => void;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Writes one AventaraWarning about a hook, through process.emitWarning where
|
|
146
|
+
* the runtime has it, else console.warn. Building or writing it never throws.
|
|
147
|
+
*/
|
|
148
|
+
function warnHook(code: string, message: () => string): void {
|
|
149
|
+
try {
|
|
150
|
+
const text = message();
|
|
151
|
+
const platform = globalThis as {
|
|
152
|
+
readonly process?: { readonly emitWarning?: WarningEmitter };
|
|
153
|
+
readonly console?: { warn(message: string): void };
|
|
154
|
+
};
|
|
155
|
+
const emitWarning = platform.process?.emitWarning;
|
|
156
|
+
if (typeof emitWarning === "function") {
|
|
157
|
+
emitWarning(text, { type: "AventaraWarning", code });
|
|
158
|
+
} else {
|
|
159
|
+
platform.console?.warn("AventaraWarning: " + text);
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// A warning that cannot be built or written changes nothing.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** What a hook threw, as its warning says it. */
|
|
167
|
+
function thrownText(error: unknown): string {
|
|
168
|
+
try {
|
|
169
|
+
return error instanceof Error ? String(error.message) : String(error);
|
|
170
|
+
} catch {
|
|
171
|
+
return "a value that cannot be written as text";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Reports a hook that threw or rejected, once; the hook is then skipped. */
|
|
176
|
+
function reportHook(hook: keyof CallHooks, call: CallIdentity, error: unknown): void {
|
|
177
|
+
warnHook(
|
|
178
|
+
${JSON.stringify(HOOK_THREW_WARNING)},
|
|
179
|
+
() => "avClient's " + hook + " threw for " + call.label + ", and was skipped: " + thrownText(error),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Runs one hook, and never throws: a throw is reported and the hook skipped.
|
|
185
|
+
* A sink's promise is not awaited (its rejection is reported); onCall's
|
|
186
|
+
* promise comes back as one that never rejects.
|
|
37
187
|
*/
|
|
188
|
+
function runHook(hook: keyof CallHooks, hooks: CallHooks, call: CallIdentity, value?: unknown): unknown {
|
|
189
|
+
try {
|
|
190
|
+
const run = hooks[hook] as ((call: CallIdentity, value?: unknown) => unknown) | undefined;
|
|
191
|
+
if (typeof run !== "function") {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
const returned = hook === "onCall" ? run(call) : run(call, value);
|
|
195
|
+
if (!isThenable(returned)) {
|
|
196
|
+
return hook === "onCall" ? returned : undefined;
|
|
197
|
+
}
|
|
198
|
+
const awaited = awaitHook(hook, call, returned);
|
|
199
|
+
return hook === "onCall" ? awaited : undefined;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
reportHook(hook, call, error);
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** What a hook's promise resolved to, or undefined, reported, when it rejected. */
|
|
207
|
+
async function awaitHook(hook: keyof CallHooks, call: CallIdentity, returned: PromiseLike<unknown>): Promise<unknown> {
|
|
208
|
+
try {
|
|
209
|
+
return await returned;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
reportHook(hook, call, error);
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* onCall's value read as the call's additions; never throws. A header it may
|
|
218
|
+
* not set is dropped with a warning; a value that cannot be read adds nothing,
|
|
219
|
+
* reported as a throw.
|
|
220
|
+
*/
|
|
221
|
+
function additionsOf(onCall: object | undefined, call: CallIdentity, value: unknown): CallAdditions {
|
|
222
|
+
try {
|
|
223
|
+
return readAdditions(value, (name, reason) => {
|
|
224
|
+
// Once per header name for each onCall: a page sending the same bad header on every call warns once.
|
|
225
|
+
const warned = onCall === undefined ? undefined : (DROPPED.get(onCall) ?? new Set<string>());
|
|
226
|
+
if (warned?.has(name) === true) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (onCall !== undefined && warned !== undefined) {
|
|
230
|
+
warned.add(name);
|
|
231
|
+
DROPPED.set(onCall, warned);
|
|
232
|
+
}
|
|
233
|
+
warnHook(
|
|
234
|
+
${JSON.stringify(HOOK_HEADER_DROPPED_WARNING)},
|
|
235
|
+
() => "avClient's onCall returned the header " + JSON.stringify(name) + " for " + call.label + ", which " + reason + "; it was dropped.",
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
} catch (error) {
|
|
239
|
+
reportHook("onCall", call, error);
|
|
240
|
+
return NO_ADDITIONS;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** The header names each onCall was warned about dropping. */
|
|
245
|
+
const DROPPED = new WeakMap<object, Set<string>>();
|
|
246
|
+
|
|
247
|
+
/** The client's onCall, as the key its warnings are kept under; undefined when it has none or it cannot be read. */
|
|
248
|
+
function onCallOf(hooks: CallHooks): object | undefined {
|
|
249
|
+
try {
|
|
250
|
+
const onCall: unknown = hooks.onCall;
|
|
251
|
+
return typeof onCall === "function" ? onCall : undefined;
|
|
252
|
+
} catch {
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Runs onCall for a call being made: its additions now, or a promise of them
|
|
259
|
+
* that never rejects when onCall is async. A throw or a rejection adds nothing.
|
|
260
|
+
*/
|
|
261
|
+
function startCall(hooks: CallHooks, call: CallIdentity): CallAdditions | Promise<CallAdditions> {
|
|
262
|
+
const returned = runHook("onCall", hooks, call);
|
|
263
|
+
const onCall = onCallOf(hooks);
|
|
264
|
+
return returned instanceof Promise ? laterAdditions(onCall, call, returned) : additionsOf(onCall, call, returned);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** An async onCall's additions, once it settles. */
|
|
268
|
+
async function laterAdditions(onCall: object | undefined, call: CallIdentity, returned: Promise<unknown>): Promise<CallAdditions> {
|
|
269
|
+
return additionsOf(onCall, call, await returned);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Runs onSuccess or onError for a call that settled; never throws. */
|
|
273
|
+
function endCall(hooks: CallHooks, call: CallIdentity, outcome: EntryOutcome): void {
|
|
274
|
+
if (outcome.kind === "value") {
|
|
275
|
+
runHook("onSuccess", hooks, call, outcome.value);
|
|
276
|
+
} else {
|
|
277
|
+
runHook("onError", hooks, call, outcome.error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
38
280
|
|
|
39
281
|
/** What the queue reads of an AbortSignal, where the consumer's lib declares none. */
|
|
40
282
|
interface AbortSource {
|
|
@@ -53,12 +295,22 @@ interface AbortHandle {
|
|
|
53
295
|
/** How a call is sent. */
|
|
54
296
|
type Dispatch =
|
|
55
297
|
| { readonly kind: "operation"; readonly operation: PreparedOperation; readonly groupable: boolean }
|
|
56
|
-
| {
|
|
298
|
+
| {
|
|
299
|
+
readonly kind: "alone";
|
|
300
|
+
readonly send: (connection: TransportConnection, signal: PlatformSignal | undefined) => Promise<unknown>;
|
|
301
|
+
}
|
|
57
302
|
| { readonly kind: "refused"; readonly error: unknown };
|
|
58
303
|
|
|
59
304
|
/** One call: how it is sent, what aborts it, and how it settles. */
|
|
60
305
|
interface CallRecord {
|
|
61
306
|
readonly dispatch: Dispatch;
|
|
307
|
+
/** The call as the hooks see it, and the hooks that see it. */
|
|
308
|
+
readonly call: CallIdentity;
|
|
309
|
+
readonly hooks: CallHooks;
|
|
310
|
+
/** What onCall added to its request; none until an async onCall resolves. */
|
|
311
|
+
additions: CallAdditions;
|
|
312
|
+
/** Whether its async onCall still runs: it is queued once that settles, holding no other call back. */
|
|
313
|
+
pending: boolean;
|
|
62
314
|
/** Its own signal, and the signal of each \`all\` it is in. */
|
|
63
315
|
readonly sources: AbortSource[];
|
|
64
316
|
/** What an abort of one of its sources does while it waits in the queue or in a set. */
|
|
@@ -156,6 +408,7 @@ function settle(record: CallRecord, outcome: EntryOutcome): void {
|
|
|
156
408
|
} else {
|
|
157
409
|
record.reject(outcome.error);
|
|
158
410
|
}
|
|
411
|
+
endCall(record.hooks, record.call, outcome);
|
|
159
412
|
}
|
|
160
413
|
|
|
161
414
|
/**
|
|
@@ -244,15 +497,17 @@ function groupable(options: AvCallOptions): boolean {
|
|
|
244
497
|
*/
|
|
245
498
|
export class CallQueue {
|
|
246
499
|
readonly #connect: () => TransportConnection;
|
|
500
|
+
readonly #hooks: CallHooks;
|
|
247
501
|
#grouping: boolean;
|
|
248
502
|
#waiting: CallRecord[] = [];
|
|
249
503
|
#scheduled = false;
|
|
250
504
|
/** Calls submitted so far: a flush waits until a microtask passes with none. */
|
|
251
505
|
#submitted = 0;
|
|
252
506
|
|
|
253
|
-
constructor(connect: () => TransportConnection, grouping: boolean) {
|
|
507
|
+
constructor(connect: () => TransportConnection, grouping: boolean, hooks: CallHooks = {}) {
|
|
254
508
|
this.#connect = connect;
|
|
255
509
|
this.#grouping = grouping;
|
|
510
|
+
this.#hooks = hooks;
|
|
256
511
|
}
|
|
257
512
|
|
|
258
513
|
/** One operation call: checked, encoded and queued now. */
|
|
@@ -264,6 +519,8 @@ export class CallQueue {
|
|
|
264
519
|
options: AvCallOptions,
|
|
265
520
|
check?: (resource: string, family: string, args: unknown) => void,
|
|
266
521
|
): Promise<unknown> {
|
|
522
|
+
const call = operationCall(resource, family, variant, options);
|
|
523
|
+
const additions = startCall(this.#hooks, call);
|
|
267
524
|
let dispatch: Dispatch;
|
|
268
525
|
try {
|
|
269
526
|
const operation = prepareOperation(resource, family, variant, args, options, check);
|
|
@@ -275,15 +532,28 @@ export class CallQueue {
|
|
|
275
532
|
} catch (error) {
|
|
276
533
|
dispatch = { kind: "refused", error };
|
|
277
534
|
}
|
|
278
|
-
return this.#call(dispatch, options.signal);
|
|
535
|
+
return this.#call(dispatch, options.signal, call, additions);
|
|
279
536
|
}
|
|
280
537
|
|
|
281
|
-
/**
|
|
282
|
-
|
|
283
|
-
|
|
538
|
+
/**
|
|
539
|
+
* A call that is always sent alone (a transaction): onCall runs now, it is
|
|
540
|
+
* queued now, and \`send\` runs at the flush, over the connection that carries
|
|
541
|
+
* its additions.
|
|
542
|
+
*/
|
|
543
|
+
alone(
|
|
544
|
+
send: (connection: TransportConnection, signal: PlatformSignal | undefined) => Promise<unknown>,
|
|
545
|
+
signal: PlatformSignal | undefined,
|
|
546
|
+
call: CallIdentity,
|
|
547
|
+
): Promise<unknown> {
|
|
548
|
+
return this.#call({ kind: "alone", send }, signal, call, startCall(this.#hooks, call));
|
|
284
549
|
}
|
|
285
550
|
|
|
286
|
-
#call(
|
|
551
|
+
#call(
|
|
552
|
+
dispatch: Dispatch,
|
|
553
|
+
signal: PlatformSignal | undefined,
|
|
554
|
+
call: CallIdentity,
|
|
555
|
+
additions: CallAdditions | Promise<CallAdditions>,
|
|
556
|
+
): Promise<unknown> {
|
|
287
557
|
let resolve: (value: unknown) => void = () => undefined;
|
|
288
558
|
let reject: (error: unknown) => void = () => undefined;
|
|
289
559
|
const settled = new Promise<unknown>((onResolve, onReject) => {
|
|
@@ -292,6 +562,10 @@ export class CallQueue {
|
|
|
292
562
|
});
|
|
293
563
|
const record: CallRecord = {
|
|
294
564
|
dispatch,
|
|
565
|
+
call,
|
|
566
|
+
hooks: this.#hooks,
|
|
567
|
+
additions: additions instanceof Promise ? NO_ADDITIONS : additions,
|
|
568
|
+
pending: additions instanceof Promise,
|
|
295
569
|
sources: signal === undefined ? [] : [signal as unknown as AbortSource],
|
|
296
570
|
onAbort: undefined,
|
|
297
571
|
settled: false,
|
|
@@ -300,9 +574,29 @@ export class CallQueue {
|
|
|
300
574
|
};
|
|
301
575
|
CALLS.set(settled, record);
|
|
302
576
|
this.#submit(record);
|
|
577
|
+
if (additions instanceof Promise) {
|
|
578
|
+
void this.#whenAdded(record, additions);
|
|
579
|
+
}
|
|
303
580
|
return settled;
|
|
304
581
|
}
|
|
305
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Queues a call once its async onCall has settled, as a call made then: it
|
|
585
|
+
* joins the calls queued with it, and holds back none queued before. A call
|
|
586
|
+
* that settled first (an abort) is not queued.
|
|
587
|
+
*/
|
|
588
|
+
async #whenAdded(record: CallRecord, additions: Promise<CallAdditions>): Promise<void> {
|
|
589
|
+
try {
|
|
590
|
+
record.additions = await additions;
|
|
591
|
+
} catch {
|
|
592
|
+
record.additions = NO_ADDITIONS;
|
|
593
|
+
}
|
|
594
|
+
record.pending = false;
|
|
595
|
+
if (!record.settled) {
|
|
596
|
+
this.#enqueue(record);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
306
600
|
#submit(record: CallRecord): void {
|
|
307
601
|
const source = abortedSource(record.sources);
|
|
308
602
|
if (source !== undefined) {
|
|
@@ -314,14 +608,21 @@ export class CallQueue {
|
|
|
314
608
|
settle(record, { kind: "error", error: dispatch.error });
|
|
315
609
|
return;
|
|
316
610
|
}
|
|
317
|
-
// Every call waits for the flush, so an all made in the same tick reaches
|
|
318
|
-
// each of its calls before it is sent, the ones sent alone too.
|
|
319
|
-
this.#waiting.push(record);
|
|
320
|
-
this.#submitted += 1;
|
|
321
611
|
watch(record, (reason) => {
|
|
322
612
|
this.#waiting = this.#waiting.filter((waiting) => waiting !== record);
|
|
323
613
|
settle(record, { kind: "error", error: reason });
|
|
324
614
|
});
|
|
615
|
+
if (!record.pending) {
|
|
616
|
+
this.#enqueue(record);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Queues a call for the next flush. */
|
|
621
|
+
#enqueue(record: CallRecord): void {
|
|
622
|
+
// Every call waits for the flush, so an all made in the same tick reaches
|
|
623
|
+
// each of its calls before it is sent, the ones sent alone too.
|
|
624
|
+
this.#waiting.push(record);
|
|
625
|
+
this.#submitted += 1;
|
|
325
626
|
if (!this.#scheduled) {
|
|
326
627
|
this.#scheduled = true;
|
|
327
628
|
this.#settle(-1);
|
|
@@ -345,35 +646,50 @@ export class CallQueue {
|
|
|
345
646
|
});
|
|
346
647
|
}
|
|
347
648
|
|
|
348
|
-
/**
|
|
349
|
-
* Sends what waits, in the order it was made: each call that is sent alone to
|
|
350
|
-
* its own route; the groupable ones as Operation Sets, a set of one to its own
|
|
351
|
-
* route.
|
|
352
|
-
*/
|
|
649
|
+
/** Sends what waits: every queued call, none of them waiting on an onCall. */
|
|
353
650
|
#flush(): void {
|
|
354
651
|
this.#scheduled = false;
|
|
355
652
|
const waiting = this.#waiting;
|
|
356
653
|
this.#waiting = [];
|
|
357
|
-
|
|
654
|
+
this.#dispatch(waiting);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Sends the calls in the order they were made: each call that is sent alone
|
|
659
|
+
* to its own route; the groupable ones as Operation Sets of calls whose
|
|
660
|
+
* additions are equal, a set of one to its own route.
|
|
661
|
+
*/
|
|
662
|
+
#dispatch(waiting: readonly CallRecord[]): void {
|
|
663
|
+
const groups = new Map<string, CallRecord[]>();
|
|
358
664
|
for (const record of waiting) {
|
|
359
665
|
const dispatch = record.dispatch;
|
|
360
666
|
if (dispatch.kind === "operation" && dispatch.groupable && this.#grouping) {
|
|
361
|
-
|
|
667
|
+
const group = groups.get(record.additions.key) ?? [];
|
|
668
|
+
groups.set(record.additions.key, group);
|
|
669
|
+
group.push(record);
|
|
362
670
|
} else {
|
|
363
671
|
this.#sendAlone(record);
|
|
364
672
|
}
|
|
365
673
|
}
|
|
366
|
-
for (const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
674
|
+
for (const group of groups.values()) {
|
|
675
|
+
for (const set of split(group)) {
|
|
676
|
+
if (set.length === 1 || !this.#grouping) {
|
|
677
|
+
for (const record of set) {
|
|
678
|
+
this.#sendAlone(record);
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
this.#sendSet(set);
|
|
370
682
|
}
|
|
371
|
-
} else {
|
|
372
|
-
this.#sendSet(set);
|
|
373
683
|
}
|
|
374
684
|
}
|
|
375
685
|
}
|
|
376
686
|
|
|
687
|
+
/** This client's connection, carrying what onCall added to the request's calls. */
|
|
688
|
+
#connectWith(additions: CallAdditions): TransportConnection {
|
|
689
|
+
const connection = this.#connect();
|
|
690
|
+
return additions.key === "" ? connection : { ...connection, additions };
|
|
691
|
+
}
|
|
692
|
+
|
|
377
693
|
#sendAlone(record: CallRecord): void {
|
|
378
694
|
unwatch(record);
|
|
379
695
|
const source = abortedSource(record.sources);
|
|
@@ -387,9 +703,9 @@ export class CallQueue {
|
|
|
387
703
|
try {
|
|
388
704
|
sent =
|
|
389
705
|
dispatch.kind === "operation"
|
|
390
|
-
? ${cached ? "(isCachedRead(dispatch.operation) ? sendCachedRead : sendOperation)" : "sendOperation"}(this.#
|
|
706
|
+
? ${cached ? "(isCachedRead(dispatch.operation) ? sendCachedRead : sendOperation)" : "sendOperation"}(this.#connectWith(record.additions), dispatch.operation, signal)
|
|
391
707
|
: dispatch.kind === "alone"
|
|
392
|
-
? dispatch.send(signal)
|
|
708
|
+
? dispatch.send(this.#connectWith(record.additions), signal)
|
|
393
709
|
: Promise.reject(dispatch.error);
|
|
394
710
|
} catch (error) {
|
|
395
711
|
sent = Promise.reject(error);
|
|
@@ -419,7 +735,8 @@ export class CallQueue {
|
|
|
419
735
|
}
|
|
420
736
|
let connection: TransportConnection;
|
|
421
737
|
try {
|
|
422
|
-
|
|
738
|
+
// Every call of a set has the same additions.
|
|
739
|
+
connection = this.#connectWith(records[0]?.additions ?? NO_ADDITIONS);
|
|
423
740
|
} catch (error) {
|
|
424
741
|
for (const record of records) {
|
|
425
742
|
answer(record, { kind: "error", error });
|