@astrale-os/sdk 0.6.0-beta.35 → 0.6.0-beta.37

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.
Files changed (104) hide show
  1. package/README.md +10 -0
  2. package/dist/application/action/context.d.ts +14 -11
  3. package/dist/application/action/index.d.ts +1 -1
  4. package/dist/application/dependency/invoker.d.ts +10 -4
  5. package/dist/application/integration/define.d.ts +8 -1
  6. package/dist/application/integration/index.d.ts +2 -2
  7. package/dist/application/integration/integration.d.ts +13 -1
  8. package/dist/application/integration/operation.d.ts +14 -1
  9. package/dist/application/mutation/execution/execute.js +1 -1
  10. package/dist/application/mutation/failure.d.ts +6 -0
  11. package/dist/application/mutation/failure.js +24 -18
  12. package/dist/application/query/collection/union.js +11 -4
  13. package/dist/application/query/conformance.d.ts +15 -0
  14. package/dist/application/query/conformance.js +46 -0
  15. package/dist/application/query/define.js +4 -3
  16. package/dist/application/query/execution/execute.d.ts +0 -1
  17. package/dist/application/query/execution/execute.js +7 -1
  18. package/dist/application/query/failure.d.ts +8 -0
  19. package/dist/application/query/failure.js +8 -0
  20. package/dist/application/query/index.d.ts +1 -1
  21. package/dist/application/query/index.js +1 -1
  22. package/dist/application/workflow/authoring.d.ts +1 -1
  23. package/dist/application/workflow/context.d.ts +1 -1
  24. package/dist/application/workflow/durable.d.ts +55 -20
  25. package/dist/application/workflow/index.d.ts +1 -1
  26. package/dist/execution/cache/cache.d.ts +17 -0
  27. package/dist/execution/cache/cache.js +11 -0
  28. package/dist/execution/cache/index.d.ts +2 -0
  29. package/dist/execution/cache/index.js +2 -0
  30. package/dist/execution/cache/memory.d.ts +13 -0
  31. package/dist/execution/cache/memory.js +48 -0
  32. package/dist/execution/identity/authentication.js +12 -6
  33. package/dist/execution/identity/callback.d.ts +7 -8
  34. package/dist/execution/identity/callback.js +17 -14
  35. package/dist/execution/index.d.ts +1 -0
  36. package/dist/execution/index.js +1 -0
  37. package/dist/execution/invocation/admission/age.d.ts +57 -0
  38. package/dist/execution/invocation/admission/age.js +208 -0
  39. package/dist/execution/invocation/admission/authentication.d.ts +2 -0
  40. package/dist/execution/invocation/admission/authentication.js +6 -2
  41. package/dist/execution/invocation/admission/index.d.ts +3 -2
  42. package/dist/execution/invocation/admission/index.js +3 -2
  43. package/dist/execution/invocation/admission/target.d.ts +11 -0
  44. package/dist/execution/invocation/admission/target.js +22 -3
  45. package/dist/execution/invocation/capabilities/bind.d.ts +11 -0
  46. package/dist/execution/invocation/capabilities/bind.js +25 -0
  47. package/dist/execution/invocation/capabilities/index.d.ts +1 -0
  48. package/dist/execution/invocation/capabilities/index.js +1 -0
  49. package/dist/execution/invocation/dispatch/context.js +2 -21
  50. package/dist/execution/invocation/handler.js +8 -2
  51. package/dist/execution/invocation/integration/authentication.d.ts +3 -3
  52. package/dist/execution/invocation/integration/authentication.js +7 -7
  53. package/dist/execution/invocation/integration/call.js +2 -2
  54. package/dist/execution/invocation/integration/index.d.ts +1 -0
  55. package/dist/execution/invocation/integration/index.js +1 -0
  56. package/dist/execution/invocation/invocation.d.ts +6 -0
  57. package/dist/execution/invocation/invocation.js +94 -19
  58. package/dist/execution/invocation/lifecycle/active.d.ts +8 -2
  59. package/dist/execution/invocation/lifecycle/active.js +3 -3
  60. package/dist/execution/invocation/lifecycle/index.d.ts +1 -1
  61. package/dist/execution/serve.d.ts +8 -0
  62. package/dist/execution/serve.js +7 -0
  63. package/dist/execution/service/create.js +2 -0
  64. package/dist/execution/service/service.d.ts +6 -0
  65. package/dist/execution/workflows/engine.d.ts +12 -2
  66. package/dist/execution/workflows/index.d.ts +1 -1
  67. package/dist/execution/workflows/integrations.d.ts +13 -0
  68. package/dist/execution/workflows/integrations.js +25 -0
  69. package/dist/execution/workflows/options.js +3 -1
  70. package/dist/execution/workflows/receiver.d.ts +3 -0
  71. package/dist/execution/workflows/receiver.js +7 -2
  72. package/dist/execution/workflows/run.d.ts +3 -4
  73. package/dist/execution/workflows/run.js +32 -60
  74. package/dist/execution/workflows/steps.d.ts +42 -0
  75. package/dist/execution/workflows/steps.js +67 -0
  76. package/dist/platform/schema/index.d.ts +1 -1
  77. package/dist/testing/durable/engine.d.ts +111 -0
  78. package/dist/testing/durable/engine.js +540 -0
  79. package/dist/testing/durable/index.d.ts +3 -0
  80. package/dist/testing/durable/index.js +1 -0
  81. package/dist/testing/durable/steps.d.ts +30 -0
  82. package/dist/testing/durable/steps.js +40 -0
  83. package/dist/testing/index.d.ts +6 -1
  84. package/dist/testing/index.js +5 -1
  85. package/dist/tooling/cli/dotenv.d.ts +3 -3
  86. package/dist/tooling/cli/dotenv.js +3 -3
  87. package/dist/tooling/cli/log.d.ts +0 -2
  88. package/dist/tooling/cli/log.js +0 -2
  89. package/dist/tooling/linter/adapters/typescript/imports.d.ts +0 -1
  90. package/dist/tooling/linter/adapters/typescript/imports.js +1 -5
  91. package/dist/tooling/linter/adapters/typescript/index.d.ts +1 -1
  92. package/dist/tooling/linter/adapters/typescript/index.js +1 -1
  93. package/dist/tooling/linter/adapters/typescript/syntax.d.ts +3 -3
  94. package/dist/tooling/linter/adapters/typescript/syntax.js +13 -25
  95. package/dist/tooling/linter/implementations/source/callable-requirements.d.ts +0 -1
  96. package/dist/tooling/linter/implementations/source/callable-requirements.js +1 -6
  97. package/dist/tooling/linter/implementations/source/dependencies.js +12 -1
  98. package/dist/tooling/linter/implementations/source/mutations.js +1 -12
  99. package/dist/tooling/linter/implementations/source/queries.js +1 -17
  100. package/dist/tooling/linter/implementations/source/shared.d.ts +0 -4
  101. package/dist/tooling/linter/implementations/source/shared.js +1 -33
  102. package/dist/tooling/linter/implementations/source/workflows.js +5 -15
  103. package/dist/tooling/linter/policy/generated.js +2 -2
  104. package/package.json +6 -6
@@ -1,6 +1,6 @@
1
1
  import * as errors from '@astrale-os/kernel-protocol/errors';
2
2
  import * as release from '@astrale-os/kernel-protocol/release';
3
- import { authentication, input, requireVia, target } from './admission/index.js';
3
+ import { admission as acceptAdmission, admissionKey, admissionScope, authentication, credentialAuthentication, input, keepAdmission, recallAdmission, requireVia, } from './admission/index.js';
4
4
  import { action, context, select, workflow } from './dispatch/index.js';
5
5
  import { active, settle } from './lifecycle/index.js';
6
6
  import { project, protocolError } from './result/index.js';
@@ -8,35 +8,110 @@ import { project, protocolError } from './result/index.js';
8
8
  const MISS = Object.freeze({ kind: 'miss' });
9
9
  const FUNCTION_MISS = 'FUNCTION_MISS';
10
10
  export async function invoke(request, inputConfig) {
11
- active(request.context);
11
+ active(request.context.execution);
12
12
  if (!pinsRunningRelease(request, inputConfig.runtime.loaded.release)) {
13
13
  throw protocolError('CONFLICT', 'Invocation release differs from the running Release.');
14
14
  }
15
15
  if (request.credential !== undefined && typeof request.credential !== 'string') {
16
16
  throw protocolError('AUTH_INVALID', 'Credential is invalid.');
17
17
  }
18
- const admitted = await admit(inputConfig.authority, {
18
+ const admission = {
19
19
  call: request.call,
20
20
  ...(request.credential === undefined ? {} : { credential: request.credential }),
21
21
  via: requireVia(request),
22
22
  domain: inputConfig.runtime.loaded.domain,
23
23
  context: request.context,
24
- });
25
- if (admitted === undefined)
26
- return MISS;
27
- const selected = select(inputConfig.runtime.loaded.registry, admitted);
28
- if (selected === undefined)
29
- return MISS;
30
- if (selected.implementation.kind === 'workflow' && selected.implementation.durable) {
31
- // The Kernel records a durable call as an Execution and runs it on the engine; running it
32
- // inline here would silently drop durability.
33
- throw protocolError('UNSUPPORTED', 'A durable Workflow runs only as a Kernel Execution.');
24
+ };
25
+ const { cache } = inputConfig;
26
+ const scope = cache === undefined
27
+ ? undefined
28
+ : admissionScope(inputConfig.runtime.loaded.registry, request.call);
29
+ if (cache === undefined || scope === undefined) {
30
+ return dispatchAdmitted(request, inputConfig, await admittedTarget(inputConfig, admission));
31
+ }
32
+ // The caller is verified before any kept admission is read: an unproven credential never learns
33
+ // whether another caller's admission is kept. A credential this verification refuses is decided
34
+ // as without a cache, the Kernel's admission first.
35
+ let established;
36
+ if (request.credential !== undefined) {
37
+ try {
38
+ established = await credentialAuthentication(request, inputConfig.runtime.loaded.domain, inputConfig.authority);
39
+ }
40
+ catch {
41
+ return dispatchAdmitted(request, inputConfig, await admittedTarget(inputConfig, admission));
42
+ }
43
+ }
44
+ let admitted;
45
+ try {
46
+ admitted = await recallOrAdmit(cache, scope, admission, inputConfig, established);
47
+ }
48
+ catch (cause) {
49
+ established?.close();
50
+ throw cause;
34
51
  }
35
- input(selected.implementation.callable, request.call.input, request.context.binary);
36
- const admittedAuthentication = await authentication(selected.implementation.callable, request, inputConfig.runtime.loaded.domain, inputConfig.authority);
37
- const close = once(admittedAuthentication.close);
52
+ return dispatchAdmitted(request, inputConfig, admitted, established);
53
+ }
54
+ /**
55
+ * The admission of a call whose callable declares an admission age: one this deployment kept while
56
+ * it is younger than the age it carried, else the source Kernel's, kept for the calls that follow.
57
+ */
58
+ async function recallOrAdmit(cache, scope, admission, inputConfig, established) {
59
+ let key;
60
+ try {
61
+ key = await admissionKey(scope, {
62
+ kernel: admission.via.issuer,
63
+ release: inputConfig.runtime.loaded.release.document.digest,
64
+ ...(established === undefined
65
+ ? {}
66
+ : {
67
+ caller: {
68
+ principal: established.caller.principal,
69
+ identity: established.caller.identity,
70
+ },
71
+ }),
72
+ }, admission.call);
73
+ }
74
+ catch {
75
+ // Without a key nothing is kept or read: the Kernel admits this call.
76
+ return admittedTarget(inputConfig, admission);
77
+ }
78
+ const kept = await recallAdmission(cache, key, scope);
79
+ if (kept !== undefined)
80
+ return kept;
81
+ const sentAt = Date.now();
82
+ const admitted = await admit(inputConfig.authority, admission);
83
+ if (admitted === undefined)
84
+ return undefined;
85
+ keepAdmission(cache, key, scope, admitted, sentAt, admission.context.background);
86
+ return admitted.target;
87
+ }
88
+ /** The target the source Kernel admits, or undefined when it no longer places the call here. */
89
+ async function admittedTarget(inputConfig, admission) {
90
+ return (await admit(inputConfig.authority, admission))?.target;
91
+ }
92
+ /** Run the admitted target: everything after admission, identical however it was admitted. */
93
+ async function dispatchAdmitted(request, inputConfig, admitted, established) {
94
+ let admittedAuthentication = established;
95
+ let close = established === undefined ? undefined : once(established.close);
38
96
  try {
39
- active(request.context);
97
+ if (admitted === undefined) {
98
+ close?.();
99
+ return MISS;
100
+ }
101
+ const selected = select(inputConfig.runtime.loaded.registry, admitted);
102
+ if (selected === undefined) {
103
+ close?.();
104
+ return MISS;
105
+ }
106
+ if (selected.implementation.kind === 'workflow' && selected.implementation.durable) {
107
+ // The Kernel records a durable call as an Execution and runs it on the engine; running it
108
+ // inline here would silently drop durability.
109
+ throw protocolError('UNSUPPORTED', 'A durable Workflow runs only as a Kernel Execution.');
110
+ }
111
+ input(selected.implementation.callable, request.call.input, request.context.binary);
112
+ admittedAuthentication ??= await authentication(selected.implementation.callable, request, inputConfig.runtime.loaded.domain, inputConfig.authority);
113
+ close ??= once(admittedAuthentication.close);
114
+ active(request.context.execution);
40
115
  const dispatchContext = context(request, inputConfig.runtime, admittedAuthentication, selected.self);
41
116
  const output = selected.implementation.kind === 'action'
42
117
  ? await action(selected.implementation, dispatchContext)
@@ -44,7 +119,7 @@ export async function invoke(request, inputConfig) {
44
119
  return settle(project(selected.implementation.callable, output), close);
45
120
  }
46
121
  catch (cause) {
47
- close();
122
+ close?.();
48
123
  throw cause;
49
124
  }
50
125
  }
@@ -64,7 +139,7 @@ async function admit(authority, admission) {
64
139
  return undefined;
65
140
  throw cause;
66
141
  }
67
- return target(admitted);
142
+ return acceptAdmission(admitted);
68
143
  }
69
144
  function refusedAsMiss(cause) {
70
145
  return (cause instanceof errors.ProtocolError &&
@@ -1,4 +1,10 @@
1
- import type { HandlerResult, InvocationContext } from '@astrale-os/kernel-server';
2
- export declare function active(context: InvocationContext): void;
1
+ import type { HandlerResult } from '@astrale-os/kernel-server';
2
+ /** The bounds of one unit of work: an invocation, or one attempt of a durable Step. */
3
+ export interface Lifetime {
4
+ readonly signal: AbortSignal;
5
+ /** Absolute Unix time in milliseconds. */
6
+ readonly deadline: number;
7
+ }
8
+ export declare function active(lifetime: Lifetime): void;
3
9
  /** Close authenticated resources after finite results or stream settlement. */
4
10
  export declare function settle(result: HandlerResult, close: () => void): HandlerResult;
@@ -1,9 +1,9 @@
1
1
  import { protocolError } from '../result/index.js';
2
- export function active(context) {
3
- if (context.execution.signal.aborted) {
2
+ export function active(lifetime) {
3
+ if (lifetime.signal.aborted) {
4
4
  throw protocolError('CANCELLED', 'Invocation was cancelled.');
5
5
  }
6
- if (Date.now() >= context.execution.deadline) {
6
+ if (Date.now() >= lifetime.deadline) {
7
7
  throw protocolError('DEADLINE_EXCEEDED', 'Invocation deadline was exceeded.');
8
8
  }
9
9
  }
@@ -1 +1 @@
1
- export { active, settle } from './active.js';
1
+ export { active, settle, type Lifetime } from './active.js';
@@ -2,6 +2,7 @@ import type { ServerCloseOptions } from '@astrale-os/kernel-server';
2
2
  import type { release } from '@astrale-os/kernel-server';
3
3
  import type { Runtime } from '../application/runtime/index.js';
4
4
  import type { WorkflowRunner } from '../application/workflow/runner/index.js';
5
+ import type { DeploymentCache } from './cache/index.js';
5
6
  import type { Identity } from './identity/index.js';
6
7
  import type { ReleaseLoader } from './service/index.js';
7
8
  import type { ServiceServerOptions } from './service/index.js';
@@ -18,6 +19,13 @@ export interface ServeInput<Environment, RuntimeValue extends Runtime, RuntimeEn
18
19
  readonly server: ServiceServerOptions;
19
20
  readonly delivery?: (environment: Environment) => release.delivery.Provider | undefined;
20
21
  readonly workflows?: WorkflowRunner;
22
+ /**
23
+ * The deployment's cache of disposable entries, created once with the Service. It keeps the
24
+ * admissions of callables whose Schema declares an admission age, so that a call within that age
25
+ * skips its `Function.admit` round trip. Defaults to a bounded memory cache of this process or
26
+ * isolate; `false` keeps nothing, and every call is admitted by its source Kernel.
27
+ */
28
+ readonly cache?: ((environment: Environment) => DeploymentCache) | false;
21
29
  /**
22
30
  * Serve durable Workflows to the workflow engine; required when the Release declares any.
23
31
  * Undefined for an environment that names no engine: engine requests are then refused.
@@ -1,3 +1,4 @@
1
+ import { memoryCache } from './cache/index.js';
1
2
  import { createService } from './service/index.js';
2
3
  /** Create one provider-neutral Fetch entrypoint around a lazily initialized current Service. */
3
4
  export function serve(input) {
@@ -11,6 +12,9 @@ export function serve(input) {
11
12
  return service;
12
13
  if (typeof input.identity !== 'function')
13
14
  invalid();
15
+ if (input.cache !== undefined && input.cache !== false && typeof input.cache !== 'function') {
16
+ invalid();
17
+ }
14
18
  const runtimeEnvironment = input.initialize === undefined
15
19
  ? environment
16
20
  : input.initialize(environment);
@@ -22,6 +26,9 @@ export function serve(input) {
22
26
  server: input.server,
23
27
  ...(input.delivery === undefined ? {} : { delivery: input.delivery(environment) }),
24
28
  ...(input.workflows === undefined ? {} : { workflows: input.workflows }),
29
+ ...(input.cache === false
30
+ ? {}
31
+ : { cache: input.cache === undefined ? memoryCache() : input.cache(environment) }),
25
32
  ...(input.durable === undefined
26
33
  ? {}
27
34
  : { durable: (request) => input.durable(environment, request) }),
@@ -54,6 +54,7 @@ export function createService(input) {
54
54
  runtime,
55
55
  authority: selectedIdentity,
56
56
  ...(input.workflows === undefined ? {} : { workflows: input.workflows }),
57
+ ...(input.cache === undefined ? {} : { cache: input.cache }),
57
58
  }));
58
59
  running = Object.freeze({ release, runtime, server });
59
60
  if (!state.accepting)
@@ -71,6 +72,7 @@ export function createService(input) {
71
72
  domain: runtime.loaded.domain,
72
73
  identity: selectedIdentity,
73
74
  serving: configuration,
75
+ integrations: runtime.clients,
74
76
  });
75
77
  });
76
78
  let closing;
@@ -2,6 +2,7 @@ import type { ApplicationHost, ServerCloseOptions, ServerOptions } from '@astral
2
2
  import type { release } from '@astrale-os/kernel-server';
3
3
  import type { Runtime } from '../../application/runtime/index.js';
4
4
  import type { WorkflowRunner } from '../../application/workflow/runner/index.js';
5
+ import type { DeploymentCache } from '../cache/index.js';
5
6
  import type { Identity } from '../identity/index.js';
6
7
  import type { ServedRelease } from '../runtime/index.js';
7
8
  import type { DurableServing, DurableServingProvider } from '../workflows/index.js';
@@ -18,6 +19,11 @@ export interface ServiceInput<Environment, RuntimeValue extends Runtime = Runtim
18
19
  */
19
20
  readonly delivery?: release.delivery.Provider;
20
21
  readonly workflows?: WorkflowRunner;
22
+ /**
23
+ * Where admissions of callables that declare an admission age are kept for the next calls.
24
+ * Absent: every call is admitted by its source Kernel.
25
+ */
26
+ readonly cache?: DeploymentCache;
21
27
  /**
22
28
  * Serve the Release's durable Workflows. A provider is resolved on the first engine request,
23
29
  * shared by concurrent requests, and retried after initialization fails.
@@ -15,6 +15,8 @@ export interface DurableEngine {
15
15
  readonly metadata?: Record<string, string>;
16
16
  }) => Error;
17
17
  readonly CancelledError: abstract new (...input: never[]) => Error;
18
+ /** The rejection of `orTimeout` once its duration elapses first. */
19
+ readonly TimeoutError: abstract new (...input: never[]) => Error;
18
20
  }
19
21
  export interface EngineWorkflowDefinition {
20
22
  readonly name: string;
@@ -34,12 +36,20 @@ export interface EngineRetryPolicy {
34
36
  readonly maxInterval: number;
35
37
  readonly exponentiationFactor: number;
36
38
  }
39
+ /**
40
+ * A promise the engine journals. Its combinators are journaled too, so a replay settles them as the
41
+ * first run did; racing it against another promise with `Promise.race` would not be.
42
+ */
43
+ export interface EnginePromise<Output> extends PromiseLike<Output> {
44
+ /** Settle with this promise, or reject with `TimeoutError` once `millis` elapse first. */
45
+ orTimeout(millis: number): PromiseLike<unknown>;
46
+ }
37
47
  /** Journaled operations of one workflow run. */
38
48
  export interface EngineWorkflowContext {
39
49
  run<Output>(name: string, action: () => Promise<Output>): PromiseLike<Output>;
40
50
  sleep(durationMs: number, name?: string): PromiseLike<void>;
41
- promise<Output>(name: string): {
42
- get(): PromiseLike<Output>;
51
+ promise(name: string): {
52
+ get(): EnginePromise<unknown>;
43
53
  };
44
54
  request(): {
45
55
  readonly attemptCompletedSignal: AbortSignal;
@@ -1,4 +1,4 @@
1
- export type { DurableEngine, EngineRetryPolicy, EngineSharedContext, EngineWorkflowContext, EngineWorkflowDefinition, } from './engine.js';
1
+ export type { DurableEngine, EnginePromise, EngineRetryPolicy, EngineSharedContext, EngineWorkflowContext, EngineWorkflowDefinition, } from './engine.js';
2
2
  export { durableServing, type AdmittedDurableServing, type DurableServing, type DurableServingProvider, } from './options.js';
3
3
  export { createDurableReceiver, type DurableReceiver } from './receiver.js';
4
4
  export { isDurableRequest, isCanonicalDurableRequest } from './route.js';
@@ -0,0 +1,13 @@
1
+ import type { IntegrationAuthentication, IntegrationInvocationOwner } from '../../application/integration/index.js';
2
+ import type { Lifetime } from '../invocation/lifecycle/index.js';
3
+ /**
4
+ * The Integration calls of one Step attempt. Each call takes the Step's next derived key as its
5
+ * call id, so every attempt presents the same ids in the same order, and a keyed operation of
6
+ * source `execution` sends the same idempotency key. A retried Step may run an operation again,
7
+ * so an `unsafe` one is refused before its Provider runs. There is no caller to invoke for.
8
+ */
9
+ export declare function stepCalls(input: {
10
+ readonly next: () => string;
11
+ readonly lifetime: Lifetime;
12
+ readonly auth: IntegrationAuthentication;
13
+ }): IntegrationInvocationOwner;
@@ -0,0 +1,25 @@
1
+ import { active } from '../invocation/lifecycle/index.js';
2
+ /**
3
+ * The Integration calls of one Step attempt. Each call takes the Step's next derived key as its
4
+ * call id, so every attempt presents the same ids in the same order, and a keyed operation of
5
+ * source `execution` sends the same idempotency key. A retried Step may run an operation again,
6
+ * so an `unsafe` one is refused before its Provider runs. There is no caller to invoke for.
7
+ */
8
+ export function stepCalls(input) {
9
+ const { next, lifetime, auth } = input;
10
+ return Object.freeze({
11
+ invoke(call) {
12
+ if (call.replay.kind === 'unsafe') {
13
+ throw new TypeError(`Integration operation ${call.integration}.${call.operation} is unsafe, and a retried ` +
14
+ 'durable Step would run it again.');
15
+ }
16
+ active(lifetime);
17
+ return call.attempt({
18
+ callId: next(),
19
+ signal: lifetime.signal,
20
+ deadline: lifetime.deadline,
21
+ auth,
22
+ });
23
+ },
24
+ });
25
+ }
@@ -18,7 +18,8 @@ export function durableServing(input) {
18
18
  typeof engine.createEndpointHandler !== 'function' ||
19
19
  typeof engine.workflow !== 'function' ||
20
20
  typeof engine.TerminalError !== 'function' ||
21
- typeof engine.CancelledError !== 'function') {
21
+ typeof engine.CancelledError !== 'function' ||
22
+ typeof engine.TimeoutError !== 'function') {
22
23
  invalid('Durable serving requires the Restate SDK Fetch entrypoint of this platform.');
23
24
  }
24
25
  const fetch = input.fetch ?? globalThis.fetch.bind(globalThis);
@@ -39,6 +40,7 @@ export function durableServing(input) {
39
40
  workflow: engine.workflow.bind(engine),
40
41
  TerminalError: engine.TerminalError,
41
42
  CancelledError: engine.CancelledError,
43
+ TimeoutError: engine.TimeoutError,
42
44
  }),
43
45
  fetch,
44
46
  retentionMs: bounded(input.retentionMs, 7 * DAY_MS, HOUR_MS, 365 * DAY_MS, 'retentionMs'),
@@ -1,3 +1,4 @@
1
+ import type { IntegrationClientFactory, Integrations } from '../../application/integration/index.js';
1
2
  import type { RuntimeWorkflow } from '../../application/runtime/workflows.js';
2
3
  import type { Domain } from '../../platform/schema/index.js';
3
4
  import type { Identity } from '../identity/index.js';
@@ -18,4 +19,6 @@ export declare function createDurableReceiver(input: {
18
19
  readonly domain: Domain;
19
20
  readonly identity: Identity;
20
21
  readonly serving: AdmittedDurableServing;
22
+ /** Integration clients of the initialized Runtime, which every Step holds. */
23
+ readonly integrations: IntegrationClientFactory<Integrations>;
21
24
  }): DurableReceiver;
@@ -1,6 +1,7 @@
1
1
  import { workflowEngineDiscovery, WorkflowEngineUnavailable } from './discovery.js';
2
2
  import { durableRoute } from './route.js';
3
3
  import { durableHandlers } from './run.js';
4
+ import { sessionSteps } from './steps.js';
4
5
  const MAXIMUM_ENDPOINTS = 256;
5
6
  /**
6
7
  * Route `…/invoke/restate/<Kernel issuer>/<declaration key>/<service>/{discover,invoke/…}` to one
@@ -14,7 +15,11 @@ export function createDurableReceiver(input) {
14
15
  if (executor === undefined) {
15
16
  throw new TypeError('Durable Workflows require an execution Identity that can act as the Domain.');
16
17
  }
17
- const bound = executor.bind(input.identity);
18
+ const steps = sessionSteps({
19
+ executor: executor.bind(input.identity),
20
+ domain: input.domain,
21
+ integrations: input.integrations,
22
+ });
18
23
  const declared = new Map(input.workflows
19
24
  .filter((workflow) => workflow.durable)
20
25
  .map((workflow) => [workflow.callable.key, workflow]));
@@ -48,8 +53,8 @@ export function createDurableReceiver(input) {
48
53
  handlers: durableHandlers({
49
54
  workflow,
50
55
  domain: input.domain,
51
- executor: bound,
52
56
  serving: input.serving,
57
+ steps,
53
58
  kernel,
54
59
  }),
55
60
  options: {
@@ -1,16 +1,16 @@
1
1
  import type { RuntimeWorkflow } from '../../application/runtime/workflows.js';
2
2
  import type { IssuerId } from '../../platform/auth/index.js';
3
3
  import type { Domain } from '../../platform/schema/index.js';
4
- import type { Identity } from '../identity/index.js';
5
4
  import type { EngineSharedContext, EngineWorkflowContext } from './engine.js';
6
5
  import type { AdmittedDurableServing } from './options.js';
7
- type Executor = NonNullable<Identity['executor']>;
6
+ import type { StepAuthorities } from './steps.js';
8
7
  export interface DurableRun {
9
8
  readonly kernel: IssuerId;
10
9
  readonly workflow: RuntimeWorkflow;
11
10
  readonly domain: Domain;
12
- readonly executor: Executor;
13
11
  readonly serving: AdmittedDurableServing;
12
+ /** What each Step attempt holds. */
13
+ readonly steps: StepAuthorities;
14
14
  }
15
15
  /** Engine handlers of one durable Workflow: the `run` and the shared `signal`. */
16
16
  export declare function durableHandlers(input: DurableRun): Readonly<{
@@ -19,4 +19,3 @@ export declare function durableHandlers(input: DurableRun): Readonly<{
19
19
  accepted: boolean;
20
20
  }>;
21
21
  }>;
22
- export {};
@@ -1,17 +1,15 @@
1
1
  import { declaredErrorFactory } from '../../application/error/index.js';
2
- import { bindMutation } from '../../application/mutation/execution/executor.js';
3
- import { bindQuery } from '../../application/query/execution/executor.js';
4
2
  import { acceptStepId } from '../../application/workflow/step/index.js';
5
3
  import { acceptStepOutput } from '../../application/workflow/step/output.js';
6
4
  import { ResponseError } from '../../platform/client/index.js';
7
- import { bindSession } from '../../platform/client/session/index.js';
8
5
  import { validateOutput } from '../invocation/result/index.js';
9
6
  import { decodeEnvelope } from './envelope.js';
10
7
  import { replayedFailure, terminalFailure } from './failure.js';
8
+ import { keySequence } from './steps.js';
11
9
  /** Token of a Workflow that declares none. */
12
10
  const DEFAULT_COMPAT = '0';
13
11
  const MAXIMUM_SLEEP_MS = 365 * 24 * 60 * 60_000;
14
- const MAXIMUM_PROMISE_NAME = 128;
12
+ const MAXIMUM_SIGNAL_NAME = 128;
15
13
  /** Bounds of one `result` wait and of the durable pause between two of them. */
16
14
  const AWAIT_WAIT_MS = 10_000;
17
15
  const AWAIT_MAXIMUM_PAUSE_MS = 5 * 60_000;
@@ -81,17 +79,16 @@ function durableContext(input, context, envelope) {
81
79
  const execution = envelope.execution;
82
80
  const signal = context.request().attemptCompletedSignal;
83
81
  const steps = new Set();
84
- /** One journaled unit of work acting as the Domain, with keys derived from its name. */
82
+ /**
83
+ * One journaled unit of work holding the capabilities of an Action as the Domain alone. Its
84
+ * Mutations, durable calls and Integration calls share one key sequence derived from its name.
85
+ */
85
86
  const journal = (name, execute) => Promise.resolve(context.run(name, async () => {
86
- const deadline = Date.now() + serving.stepTimeoutMs;
87
- const session = input.executor(execution.kernel, execution.id, { deadline, signal });
87
+ const lifetime = { deadline: Date.now() + serving.stepTimeoutMs, signal };
88
+ const next = await keySequence(execution.id, name);
89
+ const step = input.steps({ name, execution, next, lifetime });
88
90
  try {
89
- const kernel = keyed(bindSessionOf(session, deadline, signal), domain, await keySequence(execution.id, name));
90
- const output = await execute(Object.freeze({
91
- kernel,
92
- query: bindQuery(kernel, domain),
93
- mutate: bindMutation(kernel, domain),
94
- }));
91
+ const output = await execute(step.authority);
95
92
  try {
96
93
  return acceptStepOutput(output);
97
94
  }
@@ -103,7 +100,7 @@ function durableContext(input, context, envelope) {
103
100
  throw terminalFailure(engine, cause, definition) ?? cause;
104
101
  }
105
102
  finally {
106
- session.close();
103
+ step.close();
107
104
  }
108
105
  })).catch((cause) => {
109
106
  throw replayedFailure(engine, cause, definition) ?? cause;
@@ -128,6 +125,7 @@ function durableContext(input, context, envelope) {
128
125
  error: declaredErrorFactory(definition),
129
126
  step: Object.freeze({
130
127
  run: step,
128
+ query: (id, query, value) => step(id, (authority) => authority.query(query, value)),
131
129
  mutate: (id, mutation, value) => step(id, (authority) => authority.mutate(mutation, value)),
132
130
  }),
133
131
  async sleep(durationMs) {
@@ -136,11 +134,26 @@ function durableContext(input, context, envelope) {
136
134
  }
137
135
  await context.sleep(durationMs);
138
136
  },
139
- async promise(name) {
140
- if (typeof name !== 'string' || name.length === 0 || name.length > MAXIMUM_PROMISE_NAME) {
141
- throw new TypeError('Durable promise name must have 1 to 128 characters.');
137
+ async awaitSignal(name, options) {
138
+ if (typeof name !== 'string' || name.length === 0 || name.length > MAXIMUM_SIGNAL_NAME) {
139
+ throw new TypeError('Durable signal name must have 1 to 128 characters.');
140
+ }
141
+ if (options === undefined)
142
+ return (await context.promise(name).get());
143
+ const timeoutMs = options?.timeoutMs;
144
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > MAXIMUM_SLEEP_MS) {
145
+ throw new TypeError(`Durable signal timeout must be an integer between 0 and ${MAXIMUM_SLEEP_MS} ms.`);
146
+ }
147
+ // The engine journals the race between the signal and the timer: a replay settles it alike.
148
+ try {
149
+ const value = await context.promise(name).get().orTimeout(timeoutMs);
150
+ return Object.freeze({ kind: 'signal', value });
151
+ }
152
+ catch (cause) {
153
+ if (cause instanceof engine.TimeoutError)
154
+ return Object.freeze({ kind: 'timeout' });
155
+ throw cause;
142
156
  }
143
- return (await context.promise(name).get());
144
157
  },
145
158
  cancelled: (error) => error instanceof engine.CancelledError,
146
159
  async awaitExecution(target) {
@@ -167,47 +180,6 @@ function durableContext(input, context, envelope) {
167
180
  ...(execution.receiver === undefined ? {} : { self: execution.receiver }),
168
181
  });
169
182
  }
170
- function bindSessionOf(session, deadline, signal) {
171
- return bindSession(session, { deadline, signal });
172
- }
173
- /**
174
- * Every Mutation and every `invoke` of a durable callable of one Step without its own key carries
175
- * the next key of the Step's sequence. The sequence restarts with every attempt, so a retried Step
176
- * replays the same keys: the Kernel answers a Mutation with its receipt and a durable call with the
177
- * Execution it started, or refuses a request that changed under its key. Any other `invoke` carries
178
- * no key, because the Kernel accepts one only for a durable Function, and runs again with the Step.
179
- */
180
- function keyed(session, domain, next) {
181
- const invokeSource = session.invoke;
182
- const mutateSource = session.mutate;
183
- const invoke = ((reference, value, options) => invokeSource(reference, value, declaredDurable(domain, reference) ? withKey(options, next) : options));
184
- const mutate = ((author, options) => mutateSource(author, withKey(options, next)));
185
- return Object.freeze({ ...session, invoke, mutate });
186
- }
187
- /**
188
- * Whether the Domain's Schema closure declares the referenced callable durable. An untyped Path
189
- * names no declaration and a callable outside the closure has none here: both need their own key.
190
- */
191
- function declaredDurable(domain, reference) {
192
- if (reference === null || typeof reference !== 'object')
193
- return false;
194
- const key = reference.callable;
195
- return (typeof key === 'string' && domain.callable(key)?.execution?.durable === true);
196
- }
197
- function withKey(options, next) {
198
- if (options?.idempotencyKey !== undefined || options?.body !== undefined)
199
- return options;
200
- return { ...options, idempotencyKey: next() };
201
- }
202
- async function keySequence(execution, name) {
203
- const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(name)));
204
- const prefix = `${execution}.${Array.from(digest.subarray(0, 12), hex).join('')}`;
205
- let position = 0;
206
- return () => `${prefix}.${++position}`;
207
- }
208
- function hex(byte) {
209
- return byte.toString(16).padStart(2, '0');
210
- }
211
183
  function signalRequest(input) {
212
184
  if (input === null ||
213
185
  typeof input !== 'object' ||
@@ -216,7 +188,7 @@ function signalRequest(input) {
216
188
  throw new TypeError('Durable signal request is invalid.');
217
189
  }
218
190
  const { name, value } = input;
219
- if (name.length === 0 || name.length > MAXIMUM_PROMISE_NAME) {
191
+ if (name.length === 0 || name.length > MAXIMUM_SIGNAL_NAME) {
220
192
  throw new TypeError('Durable signal name must have 1 to 128 characters.');
221
193
  }
222
194
  return { name, value: value ?? null };
@@ -0,0 +1,42 @@
1
+ import type { IntegrationClientFactory, Integrations } from '../../application/integration/index.js';
2
+ import type { DurableExecution, DurableStepAuthority } from '../../application/workflow/index.js';
3
+ import type { BoundClientSession } from '../../platform/client/session/index.js';
4
+ import type { Domain } from '../../platform/schema/index.js';
5
+ import type { Identity } from '../identity/index.js';
6
+ import type { Lifetime } from '../invocation/lifecycle/index.js';
7
+ type Executor = NonNullable<Identity['executor']>;
8
+ /** One attempt of one Step: its journal name, its Execution, its key sequence and its bounds. */
9
+ export interface StepAttempt {
10
+ readonly name: string;
11
+ readonly execution: DurableExecution;
12
+ /** The next key of the Step's sequence; the sequence restarts with every attempt. */
13
+ readonly next: () => string;
14
+ readonly lifetime: Lifetime;
15
+ }
16
+ /** What one Step attempt holds, released once the attempt ends. */
17
+ export interface StepAuthorityBinding {
18
+ readonly authority: DurableStepAuthority;
19
+ close(): void;
20
+ }
21
+ /** Bind the capabilities one Step attempt holds. */
22
+ export type StepAuthorities = (attempt: StepAttempt) => StepAuthorityBinding;
23
+ /**
24
+ * Steps acting as the Domain through a Session of the Execution they belong to: the capabilities of
25
+ * an Action over the keyed Session, and the Runtime's Integration clients over the same keys.
26
+ */
27
+ export declare function sessionSteps(input: {
28
+ readonly executor: Executor;
29
+ readonly domain: Domain;
30
+ readonly integrations: IntegrationClientFactory<Integrations>;
31
+ }): StepAuthorities;
32
+ /**
33
+ * Every Mutation and every `invoke` of a durable callable of one Step without its own key carries
34
+ * the next key of the Step's sequence. The sequence restarts with every attempt, so a retried Step
35
+ * replays the same keys: the Kernel answers a Mutation with its receipt and a durable call with the
36
+ * Execution it started, or refuses a request that changed under its key. Any other `invoke` carries
37
+ * no key, because the Kernel accepts one only for a durable Function, and runs again with the Step.
38
+ */
39
+ export declare function keyed<Session extends Pick<BoundClientSession, 'invoke' | 'mutate'>>(session: Session, domain: Domain, next: () => string): Session;
40
+ /** The key sequence of one Step of one Execution: `<execution>.<digest of the name>.<position>`. */
41
+ export declare function keySequence(execution: DurableExecution['id'], name: string): Promise<() => string>;
42
+ export {};