@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.
@@ -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
- const items = validatedItems(procedure, result, options.signal);
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. `false` turns this off. The default keeps 1000 results for 10 minutes.
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
- /** The items of a stream. At the end of the stream, or when its reader stops, `release` runs. */
303
- async function* releasedAtEnd<T>(items: AsyncIterable<T>, release: () => void): AsyncGenerator<T, void, undefined> {
304
- try {
305
- yield* items;
306
- } finally {
307
- release();
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
- const stream = response.stream;
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) => (response.stream ? { ...response, stream: forgottenAtRetryableEnd(response.stream, forget) } : 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));