@astrale-os/sdk 0.6.0-beta.0 → 0.6.0-beta.10
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 +70 -0
- package/dist/application/action/action.d.ts +6 -0
- package/dist/application/action/authoring.d.ts +1 -1
- package/dist/application/action/context.d.ts +12 -2
- package/dist/application/action/define.d.ts +3 -3
- package/dist/application/action/index.d.ts +1 -1
- package/dist/application/dependency/index.d.ts +1 -1
- package/dist/application/dependency/invoker.d.ts +9 -4
- package/dist/application/mutation/authoring/properties.d.ts +5 -3
- package/dist/application/mutation/authoring/properties.js +35 -2
- package/dist/application/mutation/authoring/transition.js +4 -1
- package/dist/application/runtime/actions.js +3 -0
- package/dist/application/runtime/workflows.d.ts +2 -0
- package/dist/application/runtime/workflows.js +5 -0
- package/dist/application/view/route.d.ts +8 -0
- package/dist/application/workflow/authoring.d.ts +3 -2
- package/dist/application/workflow/context.d.ts +8 -2
- package/dist/application/workflow/define.d.ts +3 -3
- package/dist/application/workflow/define.js +19 -3
- package/dist/application/workflow/durable.d.ts +83 -0
- package/dist/application/workflow/durable.js +1 -0
- package/dist/application/workflow/index.d.ts +3 -2
- package/dist/application/workflow/runner/inline.js +3 -4
- package/dist/application/workflow/runner/runner.d.ts +2 -2
- package/dist/application/workflow/step/output.d.ts +2 -0
- package/dist/application/workflow/step/output.js +73 -0
- package/dist/application/workflow/workflow.d.ts +12 -0
- package/dist/deployment/environment.d.ts +6 -0
- package/dist/deployment/environment.js +6 -0
- package/dist/deployment/release/publication/seal.d.ts +1 -1
- package/dist/deployment/release/publication/seal.js +1 -1
- package/dist/deployment/release/release.d.ts +1 -1
- package/dist/deployment/verify/preflight/publication.js +2 -1
- package/dist/execution/identity/authentication.d.ts +9 -0
- package/dist/execution/identity/authentication.js +41 -34
- package/dist/execution/identity/callback.d.ts +12 -4
- package/dist/execution/identity/callback.js +9 -4
- package/dist/execution/identity/identity.d.ts +9 -0
- package/dist/execution/identity/identity.js +6 -2
- package/dist/execution/index.d.ts +3 -1
- package/dist/execution/index.js +2 -1
- package/dist/execution/invocation/admission/authority.d.ts +3 -3
- package/dist/execution/invocation/admission/input.d.ts +2 -2
- package/dist/execution/invocation/admission/input.js +16 -2
- package/dist/execution/invocation/dependency/bind.js +35 -13
- package/dist/execution/invocation/dispatch/context.d.ts +2 -1
- package/dist/execution/invocation/dispatch/context.js +1 -0
- package/dist/execution/invocation/invocation.js +7 -2
- package/dist/execution/serve.d.ts +6 -0
- package/dist/execution/serve.js +3 -0
- package/dist/execution/service/create.js +30 -0
- package/dist/execution/service/initialization.d.ts +4 -4
- package/dist/execution/service/initialization.js +23 -14
- package/dist/execution/service/service.d.ts +6 -0
- package/dist/execution/workflows/discovery.d.ts +14 -0
- package/dist/execution/workflows/discovery.js +151 -0
- package/dist/execution/workflows/engine.d.ts +52 -0
- package/dist/execution/workflows/engine.js +1 -0
- package/dist/execution/workflows/envelope.d.ts +12 -0
- package/dist/execution/workflows/envelope.js +46 -0
- package/dist/execution/workflows/failure.d.ts +11 -0
- package/dist/execution/workflows/failure.js +43 -0
- package/dist/execution/workflows/index.d.ts +4 -0
- package/dist/execution/workflows/index.js +3 -0
- package/dist/execution/workflows/options.d.ts +40 -0
- package/dist/execution/workflows/options.js +83 -0
- package/dist/execution/workflows/receiver.d.ts +22 -0
- package/dist/execution/workflows/receiver.js +99 -0
- package/dist/execution/workflows/route.d.ts +14 -0
- package/dist/execution/workflows/route.js +40 -0
- package/dist/execution/workflows/run.d.ts +22 -0
- package/dist/execution/workflows/run.js +211 -0
- package/dist/platform/schema/index.d.ts +2 -1
- package/dist/platform/schema/property-input.d.ts +15 -0
- package/dist/platform/schema/property-input.js +1 -0
- package/dist/platform/versioning/dependents.d.ts +18 -0
- package/dist/platform/versioning/dependents.js +15 -0
- package/dist/platform/versioning/index.d.ts +5 -0
- package/dist/platform/versioning/index.js +5 -0
- package/dist/platform/versioning/name.d.ts +37 -0
- package/dist/platform/versioning/name.js +57 -0
- package/dist/platform/versioning/reference.d.ts +48 -0
- package/dist/platform/versioning/reference.js +95 -0
- package/dist/platform/versioning/version.d.ts +72 -0
- package/dist/platform/versioning/version.js +98 -0
- package/dist/project/define.js +1 -1
- package/dist/testing/dataset/builder.d.ts +3 -3
- package/dist/tooling/cli/deprecations.d.ts +9 -0
- package/dist/tooling/cli/deprecations.js +15 -0
- package/dist/tooling/cli/development/develop.js +4 -1
- package/dist/tooling/cli/orchestrate.js +4 -1
- package/dist/tooling/linter/adapters/typescript/discover.d.ts +2 -2
- package/dist/tooling/linter/adapters/typescript/discover.js +11 -7
- package/dist/tooling/linter/implementations/source/global.js +1 -1
- package/dist/tooling/linter/lint.js +5 -4
- package/dist/tooling/linter/oxlint/run.d.ts +1 -1
- package/dist/tooling/linter/oxlint/run.js +5 -1
- package/dist/tooling/linter/policy/configuration.d.ts +3 -0
- package/dist/tooling/linter/policy/configuration.js +3 -1
- package/dist/tooling/linter/policy/ignore.d.ts +11 -0
- package/dist/tooling/linter/policy/ignore.js +101 -0
- package/dist/tooling/linter/policy/index.d.ts +1 -0
- package/package.json +15 -6
|
@@ -4,6 +4,7 @@ import { invocationPath } from '../../deployment/release/invocation.js';
|
|
|
4
4
|
import { identity as captureIdentity } from '../identity/index.js';
|
|
5
5
|
import { createHandler } from '../invocation/index.js';
|
|
6
6
|
import { initialize, load } from '../runtime/index.js';
|
|
7
|
+
import { createDurableReceiver, durableServing, isDurableRequest, } from '../workflows/index.js';
|
|
7
8
|
import { initialization } from './initialization.js';
|
|
8
9
|
import { lifecycle } from './lifecycle.js';
|
|
9
10
|
import { delivery, installation, source } from './publication/index.js';
|
|
@@ -16,6 +17,9 @@ export function createService(input) {
|
|
|
16
17
|
throw new TypeError('Service Runtime is not admitted.');
|
|
17
18
|
const selectedIdentity = captureIdentity(input.identity);
|
|
18
19
|
const providedDelivery = delivery(input.delivery);
|
|
20
|
+
const durable = input.durable;
|
|
21
|
+
// Preserve eager admission for an already loaded engine; providers stay outside ordinary startup.
|
|
22
|
+
const serving = durable === undefined || typeof durable === 'function' ? undefined : durableServing(durable);
|
|
19
23
|
let running;
|
|
20
24
|
const state = lifecycle(() => running?.server.drain());
|
|
21
25
|
const realized = initialization(async () => {
|
|
@@ -55,6 +59,19 @@ export function createService(input) {
|
|
|
55
59
|
server.drain();
|
|
56
60
|
return running;
|
|
57
61
|
});
|
|
62
|
+
const receiver = initialization(async (request) => {
|
|
63
|
+
const selected = typeof durable === 'function' ? await durable(request) : undefined;
|
|
64
|
+
const configuration = serving ?? (selected === undefined ? undefined : durableServing(selected));
|
|
65
|
+
if (configuration === undefined)
|
|
66
|
+
return undefined;
|
|
67
|
+
const { runtime } = await realized.get();
|
|
68
|
+
return createDurableReceiver({
|
|
69
|
+
workflows: runtime.loaded.registry.workflows,
|
|
70
|
+
domain: runtime.loaded.domain,
|
|
71
|
+
identity: selectedIdentity,
|
|
72
|
+
serving: configuration,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
58
75
|
let closing;
|
|
59
76
|
const service = Object.freeze({
|
|
60
77
|
async fetch(request, host) {
|
|
@@ -63,6 +80,13 @@ export function createService(input) {
|
|
|
63
80
|
const ready = await realized.get();
|
|
64
81
|
if (!state.accepting)
|
|
65
82
|
return unavailable();
|
|
83
|
+
// Without engine configuration only durable calls fail: the engine's discovery is refused.
|
|
84
|
+
if (isDurableRequest(request)) {
|
|
85
|
+
const selected = await receiver.get(request);
|
|
86
|
+
if (!state.accepting)
|
|
87
|
+
return unavailable();
|
|
88
|
+
return selected?.fetch(request) ?? notServed();
|
|
89
|
+
}
|
|
66
90
|
return ready.server.app.fetch(request, host);
|
|
67
91
|
},
|
|
68
92
|
async release() {
|
|
@@ -104,6 +128,12 @@ function serverOptions(input) {
|
|
|
104
128
|
}
|
|
105
129
|
return input;
|
|
106
130
|
}
|
|
131
|
+
function notServed() {
|
|
132
|
+
return new Response('{"error":"This deployment serves no durable Workflows."}', {
|
|
133
|
+
status: 404,
|
|
134
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
135
|
+
});
|
|
136
|
+
}
|
|
107
137
|
function unavailable() {
|
|
108
138
|
return new Response('{"error":"Service is draining."}', {
|
|
109
139
|
status: 503,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface Initialization<Value> {
|
|
2
|
-
get(): Promise<Value>;
|
|
1
|
+
export interface Initialization<Value, Arguments extends readonly unknown[] = []> {
|
|
2
|
+
get(...args: Arguments): Promise<Value>;
|
|
3
3
|
current(): Promise<Value> | undefined;
|
|
4
4
|
}
|
|
5
|
-
/** Share
|
|
6
|
-
export declare function initialization<Value>(create: () => Promise<Value>): Initialization<Value>;
|
|
5
|
+
/** Share an initialized value; absence and failure leave initialization retryable. */
|
|
6
|
+
export declare function initialization<Value, Arguments extends readonly unknown[] = []>(create: (...args: Arguments) => Promise<Value>): Initialization<Value, Arguments>;
|
|
@@ -1,18 +1,27 @@
|
|
|
1
|
-
/** Share
|
|
1
|
+
/** Share an initialized value; absence and failure leave initialization retryable. */
|
|
2
2
|
export function initialization(create) {
|
|
3
|
-
let
|
|
3
|
+
let state;
|
|
4
|
+
const get = (...args) => {
|
|
5
|
+
if (state?.phase === 'ready')
|
|
6
|
+
return state.promise;
|
|
7
|
+
if (state !== undefined) {
|
|
8
|
+
// Absence can depend on the initializing request. A waiter gets its own eligibility check.
|
|
9
|
+
return state.promise.then((value) => (value === undefined ? get(...args) : value));
|
|
10
|
+
}
|
|
11
|
+
const pending = create(...args).then((value) => {
|
|
12
|
+
if (state?.promise === pending)
|
|
13
|
+
state = value === undefined ? undefined : { phase: 'ready', promise: pending };
|
|
14
|
+
return value;
|
|
15
|
+
}, (cause) => {
|
|
16
|
+
if (state?.promise === pending)
|
|
17
|
+
state = undefined;
|
|
18
|
+
throw cause;
|
|
19
|
+
});
|
|
20
|
+
state = { phase: 'pending', promise: pending };
|
|
21
|
+
return pending;
|
|
22
|
+
};
|
|
4
23
|
return Object.freeze({
|
|
5
|
-
get
|
|
6
|
-
|
|
7
|
-
return current;
|
|
8
|
-
const pending = create().catch((cause) => {
|
|
9
|
-
if (current === pending)
|
|
10
|
-
current = undefined;
|
|
11
|
-
throw cause;
|
|
12
|
-
});
|
|
13
|
-
current = pending;
|
|
14
|
-
return pending;
|
|
15
|
-
},
|
|
16
|
-
current: () => current,
|
|
24
|
+
get,
|
|
25
|
+
current: () => state?.promise,
|
|
17
26
|
});
|
|
18
27
|
}
|
|
@@ -4,6 +4,7 @@ import type { Runtime } from '../../application/runtime/index.js';
|
|
|
4
4
|
import type { WorkflowRunner } from '../../application/workflow/runner/index.js';
|
|
5
5
|
import type { Release } from '../../deployment/release/index.js';
|
|
6
6
|
import type { Identity } from '../identity/index.js';
|
|
7
|
+
import type { DurableServing, DurableServingProvider } from '../workflows/index.js';
|
|
7
8
|
import type { ReleaseLoader } from './release.js';
|
|
8
9
|
export type ServiceServerOptions = Omit<ServerOptions, 'path' | 'invocation' | 'issuer' | 'publicOrigin' | 'tokenExchange' | 'publication' | 'delivery'>;
|
|
9
10
|
export interface ServiceInput<Environment, RuntimeValue extends Runtime = Runtime> {
|
|
@@ -14,6 +15,11 @@ export interface ServiceInput<Environment, RuntimeValue extends Runtime = Runtim
|
|
|
14
15
|
readonly server: ServiceServerOptions;
|
|
15
16
|
readonly delivery?: publication.delivery.Provider;
|
|
16
17
|
readonly workflows?: WorkflowRunner;
|
|
18
|
+
/**
|
|
19
|
+
* Serve the Release's durable Workflows. A provider is resolved on the first engine request,
|
|
20
|
+
* shared by concurrent requests, and retried after initialization fails.
|
|
21
|
+
*/
|
|
22
|
+
readonly durable?: DurableServing | DurableServingProvider;
|
|
17
23
|
}
|
|
18
24
|
export interface Service {
|
|
19
25
|
fetch(request: Request, host?: ApplicationHost): Promise<Response>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Fetch } from '@astrale-os/kernel-client';
|
|
2
|
+
import type { WorkflowEngine } from '@astrale-os/kernel-protocol/issuer/workflow-engine';
|
|
3
|
+
/** Discovery failure never admits static keys for an issuer-prefixed request. */
|
|
4
|
+
export declare class WorkflowEngineUnavailable extends Error {
|
|
5
|
+
readonly name = "WorkflowEngineUnavailable";
|
|
6
|
+
}
|
|
7
|
+
/** One receiver retains bounded, recently admitted engine authority and deduplicates refreshes. */
|
|
8
|
+
export declare function workflowEngineDiscovery(fetch: Fetch, now?: () => number): Readonly<{
|
|
9
|
+
resolve(issuer: string): Promise<WorkflowEngine>;
|
|
10
|
+
/** A crypto provider rejected this authority; the next request must discover it again. */
|
|
11
|
+
forget(issuer: string, observedKeys: readonly string[]): void;
|
|
12
|
+
/** Authentication refusal expires the basis for the next request, without replaying its body. */
|
|
13
|
+
invalidate(issuer: string, observedKeys: readonly string[]): void;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { paths } from '@astrale-os/kernel-protocol/issuer/paths';
|
|
2
|
+
import { acceptWorkflowEngine } from '@astrale-os/kernel-protocol/issuer/workflow-engine';
|
|
3
|
+
const MAXIMUM_BYTES = 16 * 1024;
|
|
4
|
+
const MAXIMUM_ISSUERS = 64;
|
|
5
|
+
const TTL_MS = 60_000;
|
|
6
|
+
const TIMEOUT_MS = 10_000;
|
|
7
|
+
/** Discovery failure never admits static keys for an issuer-prefixed request. */
|
|
8
|
+
export class WorkflowEngineUnavailable extends Error {
|
|
9
|
+
name = 'WorkflowEngineUnavailable';
|
|
10
|
+
}
|
|
11
|
+
/** One receiver retains bounded, recently admitted engine authority and deduplicates refreshes. */
|
|
12
|
+
export function workflowEngineDiscovery(fetch, now = Date.now) {
|
|
13
|
+
const bases = new Map();
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
async resolve(issuer) {
|
|
16
|
+
const basis = bases.get(issuer);
|
|
17
|
+
if (basis?.pending !== undefined)
|
|
18
|
+
return basis.pending;
|
|
19
|
+
const observedAt = now();
|
|
20
|
+
if (basis?.engine !== undefined &&
|
|
21
|
+
observedAt >= basis.expiresAt - TTL_MS &&
|
|
22
|
+
observedAt < basis.expiresAt)
|
|
23
|
+
return basis.engine;
|
|
24
|
+
if (basis === undefined) {
|
|
25
|
+
if (bases.size >= MAXIMUM_ISSUERS) {
|
|
26
|
+
// Pending requests retain their slot; arbitrary issuers cannot exceed the concurrency cap.
|
|
27
|
+
const oldest = [...bases].find(([, value]) => value.pending === undefined);
|
|
28
|
+
if (oldest === undefined)
|
|
29
|
+
throw new WorkflowEngineUnavailable('Engine discovery is busy.');
|
|
30
|
+
bases.delete(oldest[0]);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// A refresh owns a new basis, so a late rejection of old keys cannot discard pending work.
|
|
34
|
+
const selected = {
|
|
35
|
+
expiresAt: 0,
|
|
36
|
+
...(basis?.rejectedAt === undefined ? {} : { rejectedAt: basis.rejectedAt }),
|
|
37
|
+
};
|
|
38
|
+
bases.set(issuer, selected);
|
|
39
|
+
const pending = discover(fetch, issuer)
|
|
40
|
+
.then((engine) => {
|
|
41
|
+
selected.engine = engine;
|
|
42
|
+
selected.expiresAt = now() + TTL_MS;
|
|
43
|
+
// Move the successfully refreshed authority to the end of the bounded cache.
|
|
44
|
+
bases.delete(issuer);
|
|
45
|
+
bases.set(issuer, selected);
|
|
46
|
+
return engine;
|
|
47
|
+
})
|
|
48
|
+
.finally(() => {
|
|
49
|
+
if (selected.pending === pending)
|
|
50
|
+
selected.pending = undefined;
|
|
51
|
+
});
|
|
52
|
+
selected.pending = pending;
|
|
53
|
+
return pending;
|
|
54
|
+
},
|
|
55
|
+
/** A crypto provider rejected this authority; the next request must discover it again. */
|
|
56
|
+
forget(issuer, observedKeys) {
|
|
57
|
+
if (bases.get(issuer)?.engine?.identity_keys === observedKeys)
|
|
58
|
+
bases.delete(issuer);
|
|
59
|
+
},
|
|
60
|
+
/** Authentication refusal expires the basis for the next request, without replaying its body. */
|
|
61
|
+
invalidate(issuer, observedKeys) {
|
|
62
|
+
const basis = bases.get(issuer);
|
|
63
|
+
const observedAt = now();
|
|
64
|
+
if (basis?.engine?.identity_keys !== observedKeys ||
|
|
65
|
+
(basis.rejectedAt !== undefined &&
|
|
66
|
+
observedAt >= basis.rejectedAt &&
|
|
67
|
+
observedAt - basis.rejectedAt < TTL_MS))
|
|
68
|
+
return;
|
|
69
|
+
basis.rejectedAt = observedAt;
|
|
70
|
+
basis.expiresAt = 0;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function discover(fetch, issuer) {
|
|
75
|
+
const url = new URL(paths(issuer).workflowEngine, issuer);
|
|
76
|
+
if (url.protocol !== 'https:')
|
|
77
|
+
throw new WorkflowEngineUnavailable('Engine discovery requires HTTPS.');
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(new WorkflowEngineUnavailable('Engine discovery timed out.')), TIMEOUT_MS);
|
|
80
|
+
let expired;
|
|
81
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
82
|
+
expired = () => reject(controller.signal.reason);
|
|
83
|
+
controller.signal.addEventListener('abort', expired, { once: true });
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
return await Promise.race([readDocument(fetch, url, issuer, controller.signal), timeout]);
|
|
87
|
+
}
|
|
88
|
+
catch (cause) {
|
|
89
|
+
if (cause instanceof WorkflowEngineUnavailable)
|
|
90
|
+
throw cause;
|
|
91
|
+
throw new WorkflowEngineUnavailable('Engine discovery is unavailable or invalid.', { cause });
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
controller.signal.removeEventListener('abort', expired);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function readDocument(fetch, url, issuer, signal) {
|
|
99
|
+
const response = await fetch(url.href, {
|
|
100
|
+
method: 'GET',
|
|
101
|
+
redirect: 'manual',
|
|
102
|
+
signal: signal,
|
|
103
|
+
});
|
|
104
|
+
if (!response.ok || response.redirected || (response.url !== '' && response.url !== url.href)) {
|
|
105
|
+
await response.body?.cancel();
|
|
106
|
+
throw new WorkflowEngineUnavailable('Engine discovery did not return its document.');
|
|
107
|
+
}
|
|
108
|
+
if (signal.aborted) {
|
|
109
|
+
await response.body?.cancel();
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
}
|
|
112
|
+
const advertised = response.headers.get('content-length');
|
|
113
|
+
if (advertised !== null && Number(advertised) > MAXIMUM_BYTES) {
|
|
114
|
+
await response.body?.cancel();
|
|
115
|
+
throw new WorkflowEngineUnavailable('Engine discovery exceeds its byte limit.');
|
|
116
|
+
}
|
|
117
|
+
if (response.body === null)
|
|
118
|
+
throw new WorkflowEngineUnavailable('Engine discovery is empty.');
|
|
119
|
+
const reader = response.body.getReader();
|
|
120
|
+
const cancel = () => {
|
|
121
|
+
void reader.cancel(signal.reason).catch(() => { });
|
|
122
|
+
};
|
|
123
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
124
|
+
const chunks = [];
|
|
125
|
+
let size = 0;
|
|
126
|
+
try {
|
|
127
|
+
for (;;) {
|
|
128
|
+
const { done, value } = await reader.read();
|
|
129
|
+
signal.throwIfAborted();
|
|
130
|
+
if (done)
|
|
131
|
+
break;
|
|
132
|
+
size += value.byteLength;
|
|
133
|
+
if (size > MAXIMUM_BYTES)
|
|
134
|
+
throw new WorkflowEngineUnavailable('Engine discovery exceeds its byte limit.');
|
|
135
|
+
chunks.push(value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
signal.removeEventListener('abort', cancel);
|
|
140
|
+
await reader.cancel();
|
|
141
|
+
reader.releaseLock();
|
|
142
|
+
}
|
|
143
|
+
const bytes = new Uint8Array(size);
|
|
144
|
+
let offset = 0;
|
|
145
|
+
for (const chunk of chunks) {
|
|
146
|
+
bytes.set(chunk, offset);
|
|
147
|
+
offset += chunk.byteLength;
|
|
148
|
+
}
|
|
149
|
+
const document = JSON.parse(new TextDecoder('utf-8', { fatal: true, ignoreBOM: false }).decode(bytes));
|
|
150
|
+
return acceptWorkflowEngine(document, issuer);
|
|
151
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The part of the Restate TypeScript SDK's Fetch entrypoint the durable receiver uses. A runtime
|
|
3
|
+
* supplies the build for its platform: `@restatedev/restate-sdk/fetch` on Node and Bun,
|
|
4
|
+
* `@restatedev/restate-sdk-cloudflare-workers/fetch` on Cloudflare Workers, which cannot compile
|
|
5
|
+
* WebAssembly at run time. Both expose this exact surface.
|
|
6
|
+
*/
|
|
7
|
+
export interface DurableEngine {
|
|
8
|
+
createEndpointHandler(options: {
|
|
9
|
+
readonly services: unknown[];
|
|
10
|
+
readonly identityKeys?: string[];
|
|
11
|
+
}): (request: Request) => Promise<Response>;
|
|
12
|
+
workflow(definition: EngineWorkflowDefinition): unknown;
|
|
13
|
+
readonly TerminalError: new (message: string, options?: {
|
|
14
|
+
readonly errorCode?: number;
|
|
15
|
+
readonly metadata?: Record<string, string>;
|
|
16
|
+
}) => Error;
|
|
17
|
+
readonly CancelledError: abstract new (...input: never[]) => Error;
|
|
18
|
+
}
|
|
19
|
+
export interface EngineWorkflowDefinition {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly handlers: {
|
|
22
|
+
readonly run: (context: EngineWorkflowContext, input: unknown) => Promise<unknown>;
|
|
23
|
+
readonly signal: (context: EngineSharedContext, input: unknown) => Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
readonly options: {
|
|
26
|
+
readonly workflowRetention: number;
|
|
27
|
+
readonly retryPolicy: EngineRetryPolicy;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface EngineRetryPolicy {
|
|
31
|
+
readonly maxAttempts: number;
|
|
32
|
+
readonly onMaxAttempts: 'pause' | 'kill';
|
|
33
|
+
readonly initialInterval: number;
|
|
34
|
+
readonly maxInterval: number;
|
|
35
|
+
readonly exponentiationFactor: number;
|
|
36
|
+
}
|
|
37
|
+
/** Journaled operations of one workflow run. */
|
|
38
|
+
export interface EngineWorkflowContext {
|
|
39
|
+
run<Output>(name: string, action: () => Promise<Output>): PromiseLike<Output>;
|
|
40
|
+
sleep(durationMs: number, name?: string): PromiseLike<void>;
|
|
41
|
+
promise<Output>(name: string): {
|
|
42
|
+
get(): PromiseLike<Output>;
|
|
43
|
+
};
|
|
44
|
+
request(): {
|
|
45
|
+
readonly attemptCompletedSignal: AbortSignal;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface EngineSharedContext {
|
|
49
|
+
promise<Output>(name: string): {
|
|
50
|
+
resolve(value?: Output): PromiseLike<void>;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DurableExecution } from '../../application/workflow/index.js';
|
|
2
|
+
import type { Value } from '../../platform/value/index.js';
|
|
3
|
+
/** Engine input of one run, as the Kernel submits it. */
|
|
4
|
+
export interface Envelope {
|
|
5
|
+
readonly execution: DurableExecution;
|
|
6
|
+
readonly input: Value;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Decode the run input the engine delivers. Fields a newer Kernel may add are ignored, so a Domain
|
|
10
|
+
* keeps running Executions submitted by it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function decodeEnvelope(input: unknown): Envelope;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { issuer } from '../../platform/auth/index.js';
|
|
2
|
+
import { NodeId } from '../../platform/graph/node/index.js';
|
|
3
|
+
import * as portable from '../../platform/value/index.js';
|
|
4
|
+
const VERSION = /^sha256:[0-9a-f]{64}$/u;
|
|
5
|
+
/**
|
|
6
|
+
* Decode the run input the engine delivers. Fields a newer Kernel may add are ignored, so a Domain
|
|
7
|
+
* keeps running Executions submitted by it.
|
|
8
|
+
*/
|
|
9
|
+
export function decodeEnvelope(input) {
|
|
10
|
+
const envelope = record(input, 'envelope');
|
|
11
|
+
const fn = record(envelope.function, 'envelope.function');
|
|
12
|
+
const initiator = record(envelope.initiator, 'envelope.initiator');
|
|
13
|
+
if (typeof fn.version !== 'string' || !VERSION.test(fn.version)) {
|
|
14
|
+
throw new TypeError('Durable envelope.function.version is invalid.');
|
|
15
|
+
}
|
|
16
|
+
if (typeof envelope.kernel !== 'string')
|
|
17
|
+
throw new TypeError('Durable envelope.kernel is invalid.');
|
|
18
|
+
const receiver = envelope.receiver === undefined ? undefined : node(envelope.receiver, 'receiver');
|
|
19
|
+
const execution = Object.freeze({
|
|
20
|
+
id: node(envelope.execution, 'execution'),
|
|
21
|
+
function: Object.freeze({
|
|
22
|
+
id: node(fn.id, 'function.id'),
|
|
23
|
+
version: fn.version,
|
|
24
|
+
}),
|
|
25
|
+
kernel: issuer.accept(envelope.kernel),
|
|
26
|
+
initiator: Object.freeze({
|
|
27
|
+
principal: node(initiator.principal, 'initiator.principal'),
|
|
28
|
+
caller: node(initiator.caller, 'initiator.caller'),
|
|
29
|
+
}),
|
|
30
|
+
...(receiver === undefined ? {} : { receiver }),
|
|
31
|
+
});
|
|
32
|
+
if (!Object.hasOwn(envelope, 'input'))
|
|
33
|
+
throw new TypeError('Durable envelope.input is missing.');
|
|
34
|
+
return Object.freeze({ execution, input: portable.accept(envelope.input) });
|
|
35
|
+
}
|
|
36
|
+
function record(input, label) {
|
|
37
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
38
|
+
throw new TypeError(`Durable ${label} must be an object.`);
|
|
39
|
+
}
|
|
40
|
+
return input;
|
|
41
|
+
}
|
|
42
|
+
function node(input, label) {
|
|
43
|
+
if (typeof input !== 'string')
|
|
44
|
+
throw new TypeError(`Durable envelope.${label} is invalid.`);
|
|
45
|
+
return NodeId(input);
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DurableEngine } from './engine.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encode a declared business error of `definition` as a terminal engine failure, or return
|
|
4
|
+
* undefined for any other error, which the engine retries.
|
|
5
|
+
*/
|
|
6
|
+
export declare function terminalFailure(engine: DurableEngine, cause: unknown, definition: object): Error | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* The declared business error a journaled Step failed with, rebuilt through the definition's own
|
|
9
|
+
* factory, so a replay throws what the live run threw.
|
|
10
|
+
*/
|
|
11
|
+
export declare function replayedFailure(engine: DurableEngine, cause: unknown, definition: object): Error | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { declaredErrorFactory, declaredErrorOf } from '../../application/error/index.js';
|
|
2
|
+
/** Metadata key the Kernel reads a declared failure from (`backend/restate` outcome decoding). */
|
|
3
|
+
const FAILURE = 'astrale.failure';
|
|
4
|
+
const STATUS = {
|
|
5
|
+
VALIDATION_ERROR: 422,
|
|
6
|
+
NOT_FOUND: 404,
|
|
7
|
+
CONFLICT: 409,
|
|
8
|
+
UNSUPPORTED: 422,
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Encode a declared business error of `definition` as a terminal engine failure, or return
|
|
12
|
+
* undefined for any other error, which the engine retries.
|
|
13
|
+
*/
|
|
14
|
+
export function terminalFailure(engine, cause, definition) {
|
|
15
|
+
const declared = declaredErrorOf(cause, definition);
|
|
16
|
+
if (declared === undefined)
|
|
17
|
+
return undefined;
|
|
18
|
+
return new engine.TerminalError(declared.message, {
|
|
19
|
+
errorCode: STATUS[declared.family],
|
|
20
|
+
metadata: {
|
|
21
|
+
[FAILURE]: JSON.stringify({
|
|
22
|
+
family: declared.family,
|
|
23
|
+
code: declared.code,
|
|
24
|
+
message: declared.message,
|
|
25
|
+
details: declared.details,
|
|
26
|
+
}),
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The declared business error a journaled Step failed with, rebuilt through the definition's own
|
|
32
|
+
* factory, so a replay throws what the live run threw.
|
|
33
|
+
*/
|
|
34
|
+
export function replayedFailure(engine, cause, definition) {
|
|
35
|
+
if (!(cause instanceof engine.TerminalError) || cause instanceof engine.CancelledError) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const encoded = cause.metadata?.[FAILURE];
|
|
39
|
+
if (encoded === undefined)
|
|
40
|
+
return undefined;
|
|
41
|
+
const failure = JSON.parse(encoded);
|
|
42
|
+
return declaredErrorFactory(definition)(failure.code, failure.details);
|
|
43
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type { DurableEngine, EngineRetryPolicy, EngineSharedContext, EngineWorkflowContext, EngineWorkflowDefinition, } from './engine.js';
|
|
2
|
+
export { durableServing, type AdmittedDurableServing, type DurableServing, type DurableServingProvider, } from './options.js';
|
|
3
|
+
export { createDurableReceiver, type DurableReceiver } from './receiver.js';
|
|
4
|
+
export { isDurableRequest, isCanonicalDurableRequest, isLegacyDurableRequest } from './route.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Fetch } from '@astrale-os/kernel-client';
|
|
2
|
+
import type { DurableEngine, EngineRetryPolicy } from './engine.js';
|
|
3
|
+
/** Serving configuration of the durable Workflows of one deployment. */
|
|
4
|
+
export interface DurableServing {
|
|
5
|
+
/** The Restate SDK Fetch build of this platform. */
|
|
6
|
+
readonly engine: DurableEngine;
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated Explicit signing authority for retained callbacks registered before issuer-prefixed
|
|
9
|
+
* paths. Canonical requests discover their authority from the named Kernel and ignore these keys.
|
|
10
|
+
*/
|
|
11
|
+
readonly identityKeys?: readonly string[];
|
|
12
|
+
/** Fetch capability used for HTTPS discovery of a canonical request's Kernel engine. */
|
|
13
|
+
readonly fetch?: Fetch;
|
|
14
|
+
/**
|
|
15
|
+
* How long the engine retains a finished run. At least the Host minimum (one hour or more, one
|
|
16
|
+
* day by default); the Kernel resubmits an unstarted call only within half of it. Seven days by
|
|
17
|
+
* default.
|
|
18
|
+
*/
|
|
19
|
+
readonly retentionMs?: number;
|
|
20
|
+
/** Attempts of a failing run before it pauses for `resume` or `cancel`. The Host caps them. */
|
|
21
|
+
readonly retry?: {
|
|
22
|
+
readonly maxAttempts?: number;
|
|
23
|
+
readonly initialIntervalMs?: number;
|
|
24
|
+
readonly maxIntervalMs?: number;
|
|
25
|
+
};
|
|
26
|
+
/** Deadline of one Step, including its Kernel calls. One minute by default. */
|
|
27
|
+
readonly stepTimeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
/** Resolve a deployment's engine only when its durable receiver is first requested. */
|
|
30
|
+
export type DurableServingProvider = (request: Request) => DurableServing | undefined | Promise<DurableServing | undefined>;
|
|
31
|
+
export interface AdmittedDurableServing {
|
|
32
|
+
readonly engine: DurableEngine;
|
|
33
|
+
readonly identityKeys: readonly string[];
|
|
34
|
+
readonly fetch: Fetch;
|
|
35
|
+
readonly retentionMs: number;
|
|
36
|
+
readonly retryPolicy: EngineRetryPolicy;
|
|
37
|
+
readonly stepTimeoutMs: number;
|
|
38
|
+
}
|
|
39
|
+
/** Capture one durable serving configuration, failing closed on any gap. */
|
|
40
|
+
export declare function durableServing(input: DurableServing): AdmittedDurableServing;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { acceptWorkflowEngineIdentityKey } from '@astrale-os/kernel-protocol/issuer/workflow-engine';
|
|
2
|
+
const HOUR_MS = 60 * 60_000;
|
|
3
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
4
|
+
const MAXIMUM_IDENTITY_KEYS = 16;
|
|
5
|
+
/** Capture one durable serving configuration, failing closed on any gap. */
|
|
6
|
+
export function durableServing(input) {
|
|
7
|
+
if (input === null ||
|
|
8
|
+
typeof input !== 'object' ||
|
|
9
|
+
Array.isArray(input) ||
|
|
10
|
+
Reflect.ownKeys(input).some((key) => key !== 'engine' &&
|
|
11
|
+
key !== 'identityKeys' &&
|
|
12
|
+
key !== 'fetch' &&
|
|
13
|
+
key !== 'retentionMs' &&
|
|
14
|
+
key !== 'retry' &&
|
|
15
|
+
key !== 'stepTimeoutMs')) {
|
|
16
|
+
invalid('Durable serving options are invalid.');
|
|
17
|
+
}
|
|
18
|
+
const engine = input.engine;
|
|
19
|
+
if (engine === null ||
|
|
20
|
+
typeof engine !== 'object' ||
|
|
21
|
+
typeof engine.createEndpointHandler !== 'function' ||
|
|
22
|
+
typeof engine.workflow !== 'function' ||
|
|
23
|
+
typeof engine.TerminalError !== 'function' ||
|
|
24
|
+
typeof engine.CancelledError !== 'function') {
|
|
25
|
+
invalid('Durable serving requires the Restate SDK Fetch entrypoint of this platform.');
|
|
26
|
+
}
|
|
27
|
+
const keys = input.identityKeys;
|
|
28
|
+
let identityKeys = Object.freeze([]);
|
|
29
|
+
if (keys !== undefined) {
|
|
30
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > MAXIMUM_IDENTITY_KEYS) {
|
|
31
|
+
invalid('Durable serving requires engine request identity keys (publickeyv1_…).');
|
|
32
|
+
}
|
|
33
|
+
// Legacy callbacks retain their original sixteen-key input budget and duplicate policy.
|
|
34
|
+
try {
|
|
35
|
+
identityKeys = Object.freeze(Array.from(keys, acceptWorkflowEngineIdentityKey));
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
throw new TypeError('Durable serving requires engine request identity keys (publickeyv1_…).', { cause });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const fetch = input.fetch ?? globalThis.fetch.bind(globalThis);
|
|
42
|
+
if (typeof fetch !== 'function')
|
|
43
|
+
invalid('Durable serving requires Fetch for engine discovery.');
|
|
44
|
+
const retry = input.retry ?? {};
|
|
45
|
+
if (retry === null ||
|
|
46
|
+
typeof retry !== 'object' ||
|
|
47
|
+
Array.isArray(retry) ||
|
|
48
|
+
Reflect.ownKeys(retry).some((key) => key !== 'maxAttempts' && key !== 'initialIntervalMs' && key !== 'maxIntervalMs')) {
|
|
49
|
+
invalid('Durable retry options are invalid.');
|
|
50
|
+
}
|
|
51
|
+
const initialInterval = bounded(retry.initialIntervalMs, 1_000, 1, HOUR_MS, 'initialIntervalMs');
|
|
52
|
+
const maxInterval = bounded(retry.maxIntervalMs, 60_000, initialInterval, DAY_MS, 'maxIntervalMs');
|
|
53
|
+
return Object.freeze({
|
|
54
|
+
engine: Object.freeze({
|
|
55
|
+
createEndpointHandler: engine.createEndpointHandler.bind(engine),
|
|
56
|
+
workflow: engine.workflow.bind(engine),
|
|
57
|
+
TerminalError: engine.TerminalError,
|
|
58
|
+
CancelledError: engine.CancelledError,
|
|
59
|
+
}),
|
|
60
|
+
identityKeys,
|
|
61
|
+
fetch,
|
|
62
|
+
retentionMs: bounded(input.retentionMs, 7 * DAY_MS, HOUR_MS, 365 * DAY_MS, 'retentionMs'),
|
|
63
|
+
retryPolicy: Object.freeze({
|
|
64
|
+
maxAttempts: bounded(retry.maxAttempts, 20, 1, 1_000, 'maxAttempts'),
|
|
65
|
+
onMaxAttempts: 'pause',
|
|
66
|
+
initialInterval,
|
|
67
|
+
maxInterval,
|
|
68
|
+
exponentiationFactor: 2,
|
|
69
|
+
}),
|
|
70
|
+
stepTimeoutMs: bounded(input.stepTimeoutMs, 60_000, 1_000, HOUR_MS, 'stepTimeoutMs'),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function bounded(input, fallback, minimum, maximum, label) {
|
|
74
|
+
if (input === undefined)
|
|
75
|
+
return fallback;
|
|
76
|
+
if (!Number.isSafeInteger(input) || input < minimum || input > maximum) {
|
|
77
|
+
invalid(`Durable ${label} must be an integer between ${minimum} and ${maximum}.`);
|
|
78
|
+
}
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
function invalid(message) {
|
|
82
|
+
throw new TypeError(message);
|
|
83
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RuntimeWorkflow } from '../../application/runtime/workflows.js';
|
|
2
|
+
import type { Domain } from '../../platform/schema/index.js';
|
|
3
|
+
import type { Identity } from '../identity/index.js';
|
|
4
|
+
import type { AdmittedDurableServing } from './options.js';
|
|
5
|
+
/** Serves the engine's discovery and invocations of this deployment's durable Workflows. */
|
|
6
|
+
export interface DurableReceiver {
|
|
7
|
+
fetch(request: Request): Promise<Response>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Route `…/invoke/restate/<Kernel issuer>/<declaration key>/<service>/{discover,invoke/…}` to one
|
|
11
|
+
* engine endpoint. Retained services registered before issuer-prefixed paths keep their old
|
|
12
|
+
* `<declaration key>/<service>` route with explicit legacy signing keys. Canonical routes discover
|
|
13
|
+
* their signing authority at the named Kernel; that issuer also binds the run envelope. Each
|
|
14
|
+
* endpoint serves exactly that service with the declared Workflow. A declaration this Release does not
|
|
15
|
+
* serve durably is refused, so an endpoint never runs code it does not declare.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createDurableReceiver(input: {
|
|
18
|
+
readonly workflows: readonly RuntimeWorkflow[];
|
|
19
|
+
readonly domain: Domain;
|
|
20
|
+
readonly identity: Identity;
|
|
21
|
+
readonly serving: AdmittedDurableServing;
|
|
22
|
+
}): DurableReceiver;
|