@procedures/client 0.27.1 → 0.27.3
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 +16 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/procedures/interceptors.d.ts.map +1 -1
- package/dist/procedures/interceptors.js +6 -4
- package/dist/procedures/interceptors.js.map +1 -1
- package/dist/procedures/invoke.d.ts +8 -0
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +33 -2
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/server/procedure-server.d.ts +2 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +23 -26
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +1 -1
- package/src/procedures/interceptors.ts +20 -15
- package/src/procedures/invoke.ts +32 -2
- package/src/server/procedure-server.ts +25 -25
package/src/procedures/invoke.ts
CHANGED
|
@@ -286,6 +286,35 @@ function nextOrAbort<T>(iterator: AsyncIterator<T>, signal: AbortSignal, path: P
|
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* This function gives a generator of the items of `source` a `return()` that also works before
|
|
291
|
+
* the first `next()`. An async generator ignores such a `return()`: its body and its `finally` do
|
|
292
|
+
* not run. Then this `return()` gives `return()` to `source`, and `end` runs. Each wrapper of a
|
|
293
|
+
* stream of the server uses it. Thus a face that refuses a stream before its first item ends the
|
|
294
|
+
* call, its storages and its source (audit s21).
|
|
295
|
+
*/
|
|
296
|
+
export function closableUnread<T>(generator: AsyncGenerator<T, void, undefined>, source: AsyncIterable<unknown>, end?: () => void): AsyncGenerator<T, void, undefined> {
|
|
297
|
+
let started = false;
|
|
298
|
+
const next = generator.next.bind(generator);
|
|
299
|
+
const close = generator.return.bind(generator);
|
|
300
|
+
generator.next = (...args) => {
|
|
301
|
+
started = true;
|
|
302
|
+
return next(...args);
|
|
303
|
+
};
|
|
304
|
+
generator.return = async (value) => {
|
|
305
|
+
if (started) return close(value);
|
|
306
|
+
started = true;
|
|
307
|
+
const result = await close(value);
|
|
308
|
+
try {
|
|
309
|
+
await source[Symbol.asyncIterator]().return?.();
|
|
310
|
+
} finally {
|
|
311
|
+
end?.();
|
|
312
|
+
}
|
|
313
|
+
return result;
|
|
314
|
+
};
|
|
315
|
+
return generator;
|
|
316
|
+
}
|
|
317
|
+
|
|
289
318
|
/**
|
|
290
319
|
* The items of a handler's stream, each validated with the output schema. When the reader stops
|
|
291
320
|
* early or an item is not valid, the handler's generator gets `return()`, so its `finally` runs.
|
|
@@ -448,10 +477,11 @@ async function runInvocation<TOutput>(invocation: Invocation, options: InvokeOpt
|
|
|
448
477
|
// The output filter examines only what leaves the host
|
|
449
478
|
const admit = invocation.depth === 0 ? options.admitItem : undefined;
|
|
450
479
|
if (isAsyncIterable(result)) {
|
|
451
|
-
|
|
480
|
+
// A reader that closes the stream before its first item closes the result of the handler too
|
|
481
|
+
const items = closableUnread(validatedItems(procedure, result, options.signal), result);
|
|
452
482
|
return {
|
|
453
483
|
kind: "stream",
|
|
454
|
-
items: (admit ? admittedItems(items, admit, invocation) : items) as AsyncGenerator<TOutput, void, undefined>,
|
|
484
|
+
items: (admit ? closableUnread(admittedItems(items, admit, invocation), items) : items) as AsyncGenerator<TOutput, void, undefined>,
|
|
455
485
|
};
|
|
456
486
|
}
|
|
457
487
|
const value = await validateOutput(procedure, result);
|
|
@@ -18,7 +18,7 @@ import type {
|
|
|
18
18
|
import { pathToKey } from "../procedures/types.js";
|
|
19
19
|
import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js";
|
|
20
20
|
import { createCollectionProcedures } from "../procedures/collection/procedures.js";
|
|
21
|
-
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
|
+
import { InvocationError, closableUnread, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
22
22
|
import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
|
|
23
23
|
import { assertTopicsApart, type CollectionPartitions } from "../procedures/collection/partition.js";
|
|
24
24
|
import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
|
|
@@ -131,7 +131,8 @@ export interface ProcedureServerOptions extends ServerOptions {
|
|
|
131
131
|
/**
|
|
132
132
|
* The results of keyed calls (roadmap R1). A call to a procedure that is not repeatable, with
|
|
133
133
|
* an idempotency key in its metadata, runs one time: a later try with the same key gets the
|
|
134
|
-
* same result.
|
|
134
|
+
* same result. The results of each principal are apart: a call of another principal with the
|
|
135
|
+
* same key runs. `false` turns this off. The default keeps 1000 results for 10 minutes.
|
|
135
136
|
*/
|
|
136
137
|
idempotency?: IdempotencyOptions | false | undefined;
|
|
137
138
|
|
|
@@ -299,13 +300,19 @@ export interface StorageHandle {
|
|
|
299
300
|
close(): Promise<void>;
|
|
300
301
|
}
|
|
301
302
|
|
|
302
|
-
/**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
303
|
+
/**
|
|
304
|
+
* The items of a stream. At the end of the stream, or when its reader stops, `release` runs one
|
|
305
|
+
* time. A reader that stops before the first item also releases it (audit s21).
|
|
306
|
+
*/
|
|
307
|
+
function releasedAtEnd<T>(items: AsyncIterable<T>, release: () => void): AsyncGenerator<T, void, undefined> {
|
|
308
|
+
const released = (async function* () {
|
|
309
|
+
try {
|
|
310
|
+
yield* items;
|
|
311
|
+
} finally {
|
|
312
|
+
release();
|
|
313
|
+
}
|
|
314
|
+
})();
|
|
315
|
+
return closableUnread(released, items, release);
|
|
309
316
|
}
|
|
310
317
|
|
|
311
318
|
/**
|
|
@@ -902,28 +909,19 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
902
909
|
let response: ServerResponse<unknown> | undefined;
|
|
903
910
|
try {
|
|
904
911
|
const key = this.idempotency ? idempotencyKeyOf(request.metadata) : undefined;
|
|
912
|
+
// The key of the cache has the principal: another caller with the same key does not get the
|
|
913
|
+
// result of this call (audit s21). The JSON of the principal has no NUL character.
|
|
914
|
+
const principal = request.principal ? JSON.stringify([request.principal.via, request.principal.id]) : "";
|
|
905
915
|
response =
|
|
906
916
|
key === undefined || isRepeatable(effectsOf(procedure))
|
|
907
917
|
? await this.run(procedure, request, joinSignals(request.signal, this.lifetime.signal))
|
|
908
|
-
: await this.runKeyed(procedure, request, `${pathToKey(procedure.path)}\u0000${key}`);
|
|
918
|
+
: await this.runKeyed(procedure, request, `${pathToKey(procedure.path)}\u0000${principal}\u0000${key}`);
|
|
909
919
|
} finally {
|
|
910
920
|
// A stream is in flight until its end; another response is done now
|
|
911
921
|
if (!response?.stream) this.callEnded();
|
|
912
922
|
}
|
|
913
|
-
|
|
914
|
-
if (stream) {
|
|
915
|
-
const ended = (): void => this.callEnded();
|
|
916
|
-
response = {
|
|
917
|
-
...response,
|
|
918
|
-
stream: (async function* () {
|
|
919
|
-
try {
|
|
920
|
-
yield* stream;
|
|
921
|
-
} finally {
|
|
922
|
-
ended();
|
|
923
|
-
}
|
|
924
|
-
})(),
|
|
925
|
-
};
|
|
926
|
-
}
|
|
923
|
+
// A stream that its reader closes before the first item ends the call too (audit s21)
|
|
924
|
+
if (response.stream) response = { ...response, stream: releasedAtEnd(response.stream, () => this.callEnded()) };
|
|
927
925
|
return response;
|
|
928
926
|
}
|
|
929
927
|
|
|
@@ -1108,7 +1106,9 @@ export class ProcedureServer extends Server implements RepositoryProvider, StopP
|
|
|
1108
1106
|
// A stream that ends with a retryable error lets the next try run again too. Thus the retry
|
|
1109
1107
|
// of a stream that fails before its first item runs the procedure again (CLI-68).
|
|
1110
1108
|
const forget = (): void => this.idempotency!.forget(cacheKey, result);
|
|
1111
|
-
return result.then((response) =>
|
|
1109
|
+
return result.then((response) =>
|
|
1110
|
+
response.stream ? { ...response, stream: closableUnread(forgottenAtRetryableEnd(response.stream, forget), response.stream) } : response,
|
|
1111
|
+
);
|
|
1112
1112
|
}
|
|
1113
1113
|
const response = await untilAborted(result, request.signal);
|
|
1114
1114
|
if (response === undefined) return errorResponse(request.id, new InvocationError("ABORTED", "The request was aborted", procedure.path));
|