@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.
- package/README.md +10 -0
- package/dist/application/action/context.d.ts +14 -11
- package/dist/application/action/index.d.ts +1 -1
- package/dist/application/dependency/invoker.d.ts +10 -4
- package/dist/application/integration/define.d.ts +8 -1
- package/dist/application/integration/index.d.ts +2 -2
- package/dist/application/integration/integration.d.ts +13 -1
- package/dist/application/integration/operation.d.ts +14 -1
- package/dist/application/mutation/execution/execute.js +1 -1
- package/dist/application/mutation/failure.d.ts +6 -0
- package/dist/application/mutation/failure.js +24 -18
- package/dist/application/query/collection/union.js +11 -4
- package/dist/application/query/conformance.d.ts +15 -0
- package/dist/application/query/conformance.js +46 -0
- package/dist/application/query/define.js +4 -3
- package/dist/application/query/execution/execute.d.ts +0 -1
- package/dist/application/query/execution/execute.js +7 -1
- package/dist/application/query/failure.d.ts +8 -0
- package/dist/application/query/failure.js +8 -0
- package/dist/application/query/index.d.ts +1 -1
- package/dist/application/query/index.js +1 -1
- package/dist/application/workflow/authoring.d.ts +1 -1
- package/dist/application/workflow/context.d.ts +1 -1
- package/dist/application/workflow/durable.d.ts +55 -20
- package/dist/application/workflow/index.d.ts +1 -1
- package/dist/execution/cache/cache.d.ts +17 -0
- package/dist/execution/cache/cache.js +11 -0
- package/dist/execution/cache/index.d.ts +2 -0
- package/dist/execution/cache/index.js +2 -0
- package/dist/execution/cache/memory.d.ts +13 -0
- package/dist/execution/cache/memory.js +48 -0
- package/dist/execution/identity/authentication.js +12 -6
- package/dist/execution/identity/callback.d.ts +7 -8
- package/dist/execution/identity/callback.js +17 -14
- package/dist/execution/index.d.ts +1 -0
- package/dist/execution/index.js +1 -0
- package/dist/execution/invocation/admission/age.d.ts +57 -0
- package/dist/execution/invocation/admission/age.js +208 -0
- package/dist/execution/invocation/admission/authentication.d.ts +2 -0
- package/dist/execution/invocation/admission/authentication.js +6 -2
- package/dist/execution/invocation/admission/index.d.ts +3 -2
- package/dist/execution/invocation/admission/index.js +3 -2
- package/dist/execution/invocation/admission/target.d.ts +11 -0
- package/dist/execution/invocation/admission/target.js +22 -3
- package/dist/execution/invocation/capabilities/bind.d.ts +11 -0
- package/dist/execution/invocation/capabilities/bind.js +25 -0
- package/dist/execution/invocation/capabilities/index.d.ts +1 -0
- package/dist/execution/invocation/capabilities/index.js +1 -0
- package/dist/execution/invocation/dispatch/context.js +2 -21
- package/dist/execution/invocation/handler.js +8 -2
- package/dist/execution/invocation/integration/authentication.d.ts +3 -3
- package/dist/execution/invocation/integration/authentication.js +7 -7
- package/dist/execution/invocation/integration/call.js +2 -2
- package/dist/execution/invocation/integration/index.d.ts +1 -0
- package/dist/execution/invocation/integration/index.js +1 -0
- package/dist/execution/invocation/invocation.d.ts +6 -0
- package/dist/execution/invocation/invocation.js +94 -19
- package/dist/execution/invocation/lifecycle/active.d.ts +8 -2
- package/dist/execution/invocation/lifecycle/active.js +3 -3
- package/dist/execution/invocation/lifecycle/index.d.ts +1 -1
- package/dist/execution/serve.d.ts +8 -0
- package/dist/execution/serve.js +7 -0
- package/dist/execution/service/create.js +2 -0
- package/dist/execution/service/service.d.ts +6 -0
- package/dist/execution/workflows/engine.d.ts +12 -2
- package/dist/execution/workflows/index.d.ts +1 -1
- package/dist/execution/workflows/integrations.d.ts +13 -0
- package/dist/execution/workflows/integrations.js +25 -0
- package/dist/execution/workflows/options.js +3 -1
- package/dist/execution/workflows/receiver.d.ts +3 -0
- package/dist/execution/workflows/receiver.js +7 -2
- package/dist/execution/workflows/run.d.ts +3 -4
- package/dist/execution/workflows/run.js +32 -60
- package/dist/execution/workflows/steps.d.ts +42 -0
- package/dist/execution/workflows/steps.js +67 -0
- package/dist/platform/schema/index.d.ts +1 -1
- package/dist/testing/durable/engine.d.ts +111 -0
- package/dist/testing/durable/engine.js +540 -0
- package/dist/testing/durable/index.d.ts +3 -0
- package/dist/testing/durable/index.js +1 -0
- package/dist/testing/durable/steps.d.ts +30 -0
- package/dist/testing/durable/steps.js +40 -0
- package/dist/testing/index.d.ts +6 -1
- package/dist/testing/index.js +5 -1
- package/dist/tooling/cli/dotenv.d.ts +3 -3
- package/dist/tooling/cli/dotenv.js +3 -3
- package/dist/tooling/cli/log.d.ts +0 -2
- package/dist/tooling/cli/log.js +0 -2
- package/dist/tooling/linter/adapters/typescript/imports.d.ts +0 -1
- package/dist/tooling/linter/adapters/typescript/imports.js +1 -5
- package/dist/tooling/linter/adapters/typescript/index.d.ts +1 -1
- package/dist/tooling/linter/adapters/typescript/index.js +1 -1
- package/dist/tooling/linter/adapters/typescript/syntax.d.ts +3 -3
- package/dist/tooling/linter/adapters/typescript/syntax.js +13 -25
- package/dist/tooling/linter/implementations/source/callable-requirements.d.ts +0 -1
- package/dist/tooling/linter/implementations/source/callable-requirements.js +1 -6
- package/dist/tooling/linter/implementations/source/dependencies.js +12 -1
- package/dist/tooling/linter/implementations/source/mutations.js +1 -12
- package/dist/tooling/linter/implementations/source/queries.js +1 -17
- package/dist/tooling/linter/implementations/source/shared.d.ts +0 -4
- package/dist/tooling/linter/implementations/source/shared.js +1 -33
- package/dist/tooling/linter/implementations/source/workflows.js +5 -15
- package/dist/tooling/linter/policy/generated.js +2 -2
- 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,
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2
|
-
|
|
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(
|
|
3
|
-
if (
|
|
2
|
+
export function active(lifetime) {
|
|
3
|
+
if (lifetime.signal.aborted) {
|
|
4
4
|
throw protocolError('CANCELLED', 'Invocation was cancelled.');
|
|
5
5
|
}
|
|
6
|
-
if (Date.now() >=
|
|
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.
|
package/dist/execution/serve.js
CHANGED
|
@@ -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
|
|
42
|
-
get():
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
87
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
140
|
-
if (typeof name !== 'string' || name.length === 0 || name.length >
|
|
141
|
-
throw new TypeError('Durable
|
|
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 >
|
|
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 {};
|