@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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { workflowEngineDiscovery, WorkflowEngineUnavailable } from './discovery.js';
|
|
2
|
+
import { durableRoute } from './route.js';
|
|
3
|
+
import { durableHandlers } from './run.js';
|
|
4
|
+
const MAXIMUM_ENDPOINTS = 256;
|
|
5
|
+
/**
|
|
6
|
+
* Route `…/invoke/restate/<Kernel issuer>/<declaration key>/<service>/{discover,invoke/…}` to one
|
|
7
|
+
* engine endpoint. Retained services registered before issuer-prefixed paths keep their old
|
|
8
|
+
* `<declaration key>/<service>` route with explicit legacy signing keys. Canonical routes discover
|
|
9
|
+
* their signing authority at the named Kernel; that issuer also binds the run envelope. Each
|
|
10
|
+
* endpoint serves exactly that service with the declared Workflow. A declaration this Release does not
|
|
11
|
+
* serve durably is refused, so an endpoint never runs code it does not declare.
|
|
12
|
+
*/
|
|
13
|
+
export function createDurableReceiver(input) {
|
|
14
|
+
const executor = input.identity.executor;
|
|
15
|
+
if (executor === undefined) {
|
|
16
|
+
throw new TypeError('Durable Workflows require an execution Identity that can act as the Domain.');
|
|
17
|
+
}
|
|
18
|
+
const bound = executor.bind(input.identity);
|
|
19
|
+
const declared = new Map(input.workflows
|
|
20
|
+
.filter((workflow) => workflow.durable)
|
|
21
|
+
.map((workflow) => [workflow.callable.key, workflow]));
|
|
22
|
+
const endpoints = new Map();
|
|
23
|
+
const { engine } = input.serving;
|
|
24
|
+
const discovery = workflowEngineDiscovery(input.serving.fetch);
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
async fetch(request) {
|
|
27
|
+
const route = durableRoute(request);
|
|
28
|
+
if (route === undefined)
|
|
29
|
+
return refused();
|
|
30
|
+
const { kernel, declaration, service } = route;
|
|
31
|
+
const workflow = declared.get(declaration);
|
|
32
|
+
if (workflow === undefined)
|
|
33
|
+
return refused();
|
|
34
|
+
let identityKeys = input.serving.identityKeys;
|
|
35
|
+
if (kernel !== undefined) {
|
|
36
|
+
try {
|
|
37
|
+
identityKeys = (await discovery.resolve(kernel)).identity_keys;
|
|
38
|
+
}
|
|
39
|
+
catch (cause) {
|
|
40
|
+
if (!(cause instanceof WorkflowEngineUnavailable))
|
|
41
|
+
throw cause;
|
|
42
|
+
return unavailable();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else if (identityKeys.length === 0)
|
|
46
|
+
return refused();
|
|
47
|
+
request.signal.throwIfAborted();
|
|
48
|
+
const cacheKey = JSON.stringify([kernel ?? null, declaration, service, identityKeys]);
|
|
49
|
+
let endpoint = endpoints.get(cacheKey);
|
|
50
|
+
if (endpoint === undefined) {
|
|
51
|
+
const definition = engine.workflow({
|
|
52
|
+
name: service,
|
|
53
|
+
handlers: durableHandlers({
|
|
54
|
+
workflow,
|
|
55
|
+
domain: input.domain,
|
|
56
|
+
executor: bound,
|
|
57
|
+
serving: input.serving,
|
|
58
|
+
...(kernel === undefined ? {} : { kernel }),
|
|
59
|
+
}),
|
|
60
|
+
options: {
|
|
61
|
+
workflowRetention: input.serving.retentionMs,
|
|
62
|
+
retryPolicy: input.serving.retryPolicy,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
try {
|
|
66
|
+
endpoint = engine.createEndpointHandler({
|
|
67
|
+
identityKeys: [...identityKeys],
|
|
68
|
+
services: [definition],
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
if (kernel === undefined)
|
|
73
|
+
throw cause;
|
|
74
|
+
discovery.forget(kernel, identityKeys);
|
|
75
|
+
return unavailable();
|
|
76
|
+
}
|
|
77
|
+
if (endpoints.size >= MAXIMUM_ENDPOINTS)
|
|
78
|
+
endpoints.delete(endpoints.keys().next().value);
|
|
79
|
+
endpoints.set(cacheKey, endpoint);
|
|
80
|
+
}
|
|
81
|
+
const response = await endpoint(request);
|
|
82
|
+
if (kernel !== undefined && response.status === 401)
|
|
83
|
+
discovery.invalidate(kernel, identityKeys);
|
|
84
|
+
return response;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function refused() {
|
|
89
|
+
return new Response('{"error":"Unknown durable declaration."}', {
|
|
90
|
+
status: 404,
|
|
91
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function unavailable() {
|
|
95
|
+
return new Response('{"error":"Workflow engine authority is unavailable."}', {
|
|
96
|
+
status: 503,
|
|
97
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IssuerId } from '../../platform/auth/index.js';
|
|
2
|
+
export interface DurableRoute {
|
|
3
|
+
readonly kernel?: IssuerId;
|
|
4
|
+
readonly declaration: string;
|
|
5
|
+
readonly service: string;
|
|
6
|
+
}
|
|
7
|
+
/** Whether the request is owned by the durable receiver, including malformed paths. */
|
|
8
|
+
export declare function isDurableRequest(request: Request): boolean;
|
|
9
|
+
/** Canonical requests discover authority at the HTTPS Kernel named by this admitted route. */
|
|
10
|
+
export declare function isCanonicalDurableRequest(request: Request): boolean;
|
|
11
|
+
/** Retained callbacks need explicit legacy signing authority before an engine module is loaded. */
|
|
12
|
+
export declare function isLegacyDurableRequest(request: Request): boolean;
|
|
13
|
+
/** One wire decoder shared by the receiver and platform lazy-load eligibility. */
|
|
14
|
+
export declare function durableRoute(request: Request): DurableRoute | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Key } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { invocationPath } from '../../deployment/release/invocation.js';
|
|
3
|
+
import { AuthValueError } from '../../platform/auth/index.js';
|
|
4
|
+
import { httpIssuer } from '../identity/errors.js';
|
|
5
|
+
const PREFIX = `${invocationPath()}/restate/`;
|
|
6
|
+
const SERVICE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/u;
|
|
7
|
+
/** Whether the request is owned by the durable receiver, including malformed paths. */
|
|
8
|
+
export function isDurableRequest(request) {
|
|
9
|
+
return new URL(request.url).pathname.startsWith(PREFIX);
|
|
10
|
+
}
|
|
11
|
+
/** Canonical requests discover authority at the HTTPS Kernel named by this admitted route. */
|
|
12
|
+
export function isCanonicalDurableRequest(request) {
|
|
13
|
+
return durableRoute(request)?.kernel !== undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Retained callbacks need explicit legacy signing authority before an engine module is loaded. */
|
|
16
|
+
export function isLegacyDurableRequest(request) {
|
|
17
|
+
const route = durableRoute(request);
|
|
18
|
+
return route !== undefined && route.kernel === undefined;
|
|
19
|
+
}
|
|
20
|
+
/** One wire decoder shared by the receiver and platform lazy-load eligibility. */
|
|
21
|
+
export function durableRoute(request) {
|
|
22
|
+
const pathname = new URL(request.url).pathname;
|
|
23
|
+
if (!pathname.startsWith(PREFIX))
|
|
24
|
+
return undefined;
|
|
25
|
+
const parts = pathname.slice(PREFIX.length).split('/');
|
|
26
|
+
try {
|
|
27
|
+
const first = decodeURIComponent(parts[0] ?? '');
|
|
28
|
+
const kernel = Key.is(first) ? undefined : httpIssuer(first, false);
|
|
29
|
+
const declaration = kernel === undefined ? first : decodeURIComponent(parts[1] ?? '');
|
|
30
|
+
const service = parts[kernel === undefined ? 1 : 2] ?? '';
|
|
31
|
+
if (!Key.is(declaration) || !SERVICE.test(service))
|
|
32
|
+
return undefined;
|
|
33
|
+
return Object.freeze({ ...(kernel === undefined ? {} : { kernel }), declaration, service });
|
|
34
|
+
}
|
|
35
|
+
catch (cause) {
|
|
36
|
+
if (cause instanceof URIError || cause instanceof AuthValueError || cause instanceof TypeError)
|
|
37
|
+
return undefined;
|
|
38
|
+
throw cause;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RuntimeWorkflow } from '../../application/runtime/workflows.js';
|
|
2
|
+
import type { IssuerId } from '../../platform/auth/index.js';
|
|
3
|
+
import type { Domain } from '../../platform/schema/index.js';
|
|
4
|
+
import type { Identity } from '../identity/index.js';
|
|
5
|
+
import type { EngineSharedContext, EngineWorkflowContext } from './engine.js';
|
|
6
|
+
import type { AdmittedDurableServing } from './options.js';
|
|
7
|
+
type Executor = NonNullable<Identity['executor']>;
|
|
8
|
+
export interface DurableRun {
|
|
9
|
+
readonly kernel?: IssuerId;
|
|
10
|
+
readonly workflow: RuntimeWorkflow;
|
|
11
|
+
readonly domain: Domain;
|
|
12
|
+
readonly executor: Executor;
|
|
13
|
+
readonly serving: AdmittedDurableServing;
|
|
14
|
+
}
|
|
15
|
+
/** Engine handlers of one durable Workflow: the `run` and the shared `signal`. */
|
|
16
|
+
export declare function durableHandlers(input: DurableRun): Readonly<{
|
|
17
|
+
run: (context: EngineWorkflowContext, raw: unknown) => Promise<import("../../platform/schema/index.js").Value>;
|
|
18
|
+
signal(context: EngineSharedContext, raw: unknown): Promise<{
|
|
19
|
+
accepted: boolean;
|
|
20
|
+
}>;
|
|
21
|
+
}>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
import { acceptStepId } from '../../application/workflow/step/index.js';
|
|
5
|
+
import { acceptStepOutput } from '../../application/workflow/step/output.js';
|
|
6
|
+
import { ResponseError } from '../../platform/client/index.js';
|
|
7
|
+
import { bindSession, execution as executionMethods } from '../../platform/client/session/index.js';
|
|
8
|
+
import { validateOutput } from '../invocation/result/index.js';
|
|
9
|
+
import { decodeEnvelope } from './envelope.js';
|
|
10
|
+
import { replayedFailure, terminalFailure } from './failure.js';
|
|
11
|
+
/** Token of a Workflow that declares none. */
|
|
12
|
+
const DEFAULT_COMPAT = '0';
|
|
13
|
+
const MAXIMUM_SLEEP_MS = 365 * 24 * 60 * 60_000;
|
|
14
|
+
const MAXIMUM_PROMISE_NAME = 128;
|
|
15
|
+
/** Bounds of one `result` wait and of the durable pause between two of them. */
|
|
16
|
+
const AWAIT_WAIT_MS = 10_000;
|
|
17
|
+
const AWAIT_MAXIMUM_PAUSE_MS = 5 * 60_000;
|
|
18
|
+
/** Engine handlers of one durable Workflow: the `run` and the shared `signal`. */
|
|
19
|
+
export function durableHandlers(input) {
|
|
20
|
+
const { engine } = input.serving;
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
run: (context, raw) => run(input, context, raw),
|
|
23
|
+
async signal(context, raw) {
|
|
24
|
+
const request = signalRequest(raw);
|
|
25
|
+
try {
|
|
26
|
+
await context.promise(request.name).resolve(request.value);
|
|
27
|
+
return { accepted: true };
|
|
28
|
+
}
|
|
29
|
+
catch (cause) {
|
|
30
|
+
// The engine refuses to resolve a promise twice with a terminal failure.
|
|
31
|
+
if (cause instanceof engine.TerminalError)
|
|
32
|
+
return { accepted: false };
|
|
33
|
+
throw cause;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async function run(input, context, raw) {
|
|
39
|
+
const { workflow, serving } = input;
|
|
40
|
+
const { engine } = serving;
|
|
41
|
+
const definition = workflow.definition;
|
|
42
|
+
// Journaled first. A redeployment that changed the token cannot replay this run: it fails every
|
|
43
|
+
// attempt with this reason until the engine pauses it, then `resume` on compatible code or cancel.
|
|
44
|
+
const token = definition.compat ?? DEFAULT_COMPAT;
|
|
45
|
+
const recorded = await context.run('compat', async () => token);
|
|
46
|
+
if (recorded !== token) {
|
|
47
|
+
throw new Error(`Workflow ${workflow.address} started under compat token ${JSON.stringify(recorded)}; ` +
|
|
48
|
+
`this deployment declares ${JSON.stringify(token)}.`);
|
|
49
|
+
}
|
|
50
|
+
// Code that cannot read what the Kernel recorded pauses the run the same way.
|
|
51
|
+
const envelope = admitted(workflow, raw, input.kernel);
|
|
52
|
+
const durable = durableContext(input, context, envelope);
|
|
53
|
+
let output;
|
|
54
|
+
try {
|
|
55
|
+
output = await definition.run(durable);
|
|
56
|
+
}
|
|
57
|
+
catch (cause) {
|
|
58
|
+
throw terminalFailure(engine, cause, definition) ?? cause;
|
|
59
|
+
}
|
|
60
|
+
return validateOutput(workflow.callable, output);
|
|
61
|
+
}
|
|
62
|
+
function admitted(workflow, raw, kernel) {
|
|
63
|
+
const envelope = decodeEnvelope(raw);
|
|
64
|
+
if (kernel !== undefined && envelope.execution.kernel !== kernel) {
|
|
65
|
+
throw new TypeError('Durable envelope Kernel differs from its authenticated endpoint.');
|
|
66
|
+
}
|
|
67
|
+
const diagnostics = workflow.callable.validateInput(envelope.input);
|
|
68
|
+
if (diagnostics.length > 0) {
|
|
69
|
+
throw new TypeError(`Workflow ${workflow.address} cannot read the recorded input.`);
|
|
70
|
+
}
|
|
71
|
+
const instance = 'static' in workflow.callable && workflow.callable.static === false;
|
|
72
|
+
if (instance !== (envelope.execution.receiver !== undefined)) {
|
|
73
|
+
throw new TypeError(`Workflow ${workflow.address} cannot use the recorded receiver.`);
|
|
74
|
+
}
|
|
75
|
+
return envelope;
|
|
76
|
+
}
|
|
77
|
+
function durableContext(input, context, envelope) {
|
|
78
|
+
const { serving, domain, workflow } = input;
|
|
79
|
+
const { engine } = serving;
|
|
80
|
+
const definition = workflow.definition;
|
|
81
|
+
const execution = envelope.execution;
|
|
82
|
+
const signal = context.request().attemptCompletedSignal;
|
|
83
|
+
const steps = new Set();
|
|
84
|
+
/** One journaled unit of work acting as the Domain, with keys derived from its name. */
|
|
85
|
+
const journal = (name, execute) => Promise.resolve(context.run(name, async () => {
|
|
86
|
+
const deadline = Date.now() + serving.stepTimeoutMs;
|
|
87
|
+
const session = input.executor(execution.kernel, { deadline, signal });
|
|
88
|
+
try {
|
|
89
|
+
const kernel = keyed(bindSessionOf(session, deadline, signal), 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
|
+
}));
|
|
95
|
+
try {
|
|
96
|
+
return acceptStepOutput(output);
|
|
97
|
+
}
|
|
98
|
+
catch (cause) {
|
|
99
|
+
throw new TypeError(`Workflow ${name} output must be portable.`, { cause });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (cause) {
|
|
103
|
+
throw terminalFailure(engine, cause, definition) ?? cause;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
session.close();
|
|
107
|
+
}
|
|
108
|
+
})).catch((cause) => {
|
|
109
|
+
throw replayedFailure(engine, cause, definition) ?? cause;
|
|
110
|
+
});
|
|
111
|
+
const step = (id, execute) => {
|
|
112
|
+
let accepted;
|
|
113
|
+
try {
|
|
114
|
+
accepted = acceptStepId(id);
|
|
115
|
+
if (steps.has(accepted))
|
|
116
|
+
throw new TypeError(`Workflow Step ${accepted} occurs more than once.`);
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
return Promise.reject(cause);
|
|
120
|
+
}
|
|
121
|
+
steps.add(accepted);
|
|
122
|
+
return journal(`step:${accepted}`, execute);
|
|
123
|
+
};
|
|
124
|
+
return Object.freeze({
|
|
125
|
+
domain,
|
|
126
|
+
input: envelope.input,
|
|
127
|
+
execution,
|
|
128
|
+
error: declaredErrorFactory(definition),
|
|
129
|
+
step: Object.freeze({
|
|
130
|
+
run: step,
|
|
131
|
+
mutate: (id, mutation, value) => step(id, (authority) => authority.mutate(mutation, value)),
|
|
132
|
+
}),
|
|
133
|
+
async sleep(durationMs) {
|
|
134
|
+
if (!Number.isSafeInteger(durationMs) || durationMs < 0 || durationMs > MAXIMUM_SLEEP_MS) {
|
|
135
|
+
throw new TypeError(`Durable sleep must be an integer between 0 and ${MAXIMUM_SLEEP_MS} ms.`);
|
|
136
|
+
}
|
|
137
|
+
await context.sleep(durationMs);
|
|
138
|
+
},
|
|
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.');
|
|
142
|
+
}
|
|
143
|
+
return (await context.promise(name).get());
|
|
144
|
+
},
|
|
145
|
+
cancelled: (error) => error instanceof engine.CancelledError,
|
|
146
|
+
async awaitExecution(target) {
|
|
147
|
+
for (let poll = 1;; poll++) {
|
|
148
|
+
const polled = await journal(`await:${target.id}:${poll}`, async ({ kernel }) => {
|
|
149
|
+
try {
|
|
150
|
+
const outcome = await kernel.invoke(executionMethods.result(target), {
|
|
151
|
+
waitMs: Math.min(AWAIT_WAIT_MS, Math.floor(serving.stepTimeoutMs / 2)),
|
|
152
|
+
});
|
|
153
|
+
return { ended: true, outcome };
|
|
154
|
+
}
|
|
155
|
+
catch (cause) {
|
|
156
|
+
if (cause instanceof ResponseError && cause.reason?.code === 'EXECUTION_PENDING') {
|
|
157
|
+
return { ended: false };
|
|
158
|
+
}
|
|
159
|
+
throw cause;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
if (polled.ended)
|
|
163
|
+
return polled.outcome;
|
|
164
|
+
await context.sleep(Math.min(AWAIT_MAXIMUM_PAUSE_MS, 1_000 * 2 ** Math.min(poll, 16)));
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
...(execution.receiver === undefined ? {} : { self: execution.receiver }),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function bindSessionOf(session, deadline, signal) {
|
|
171
|
+
return bindSession(session, { deadline, signal });
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Every `invoke` and Mutation of one Step without its own key carries the next key of the Step's
|
|
175
|
+
* sequence. The sequence restarts with every attempt, so a retried Step replays the same keys and
|
|
176
|
+
* the Kernel answers with the first outcome, or `IDEMPOTENCY_KEY_CONFLICT` if the request changed.
|
|
177
|
+
*/
|
|
178
|
+
function keyed(session, next) {
|
|
179
|
+
const invokeSource = session.invoke;
|
|
180
|
+
const mutateSource = session.mutate;
|
|
181
|
+
const invoke = ((reference, value, options) => invokeSource(reference, value, withKey(options, next)));
|
|
182
|
+
const mutate = ((author, options) => mutateSource(author, withKey(options, next)));
|
|
183
|
+
return Object.freeze({ ...session, invoke, mutate });
|
|
184
|
+
}
|
|
185
|
+
function withKey(options, next) {
|
|
186
|
+
if (options?.idempotencyKey !== undefined || options?.body !== undefined)
|
|
187
|
+
return options;
|
|
188
|
+
return { ...options, idempotencyKey: next() };
|
|
189
|
+
}
|
|
190
|
+
async function keySequence(execution, name) {
|
|
191
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(name)));
|
|
192
|
+
const prefix = `${execution}.${Array.from(digest.subarray(0, 12), hex).join('')}`;
|
|
193
|
+
let position = 0;
|
|
194
|
+
return () => `${prefix}.${++position}`;
|
|
195
|
+
}
|
|
196
|
+
function hex(byte) {
|
|
197
|
+
return byte.toString(16).padStart(2, '0');
|
|
198
|
+
}
|
|
199
|
+
function signalRequest(input) {
|
|
200
|
+
if (input === null ||
|
|
201
|
+
typeof input !== 'object' ||
|
|
202
|
+
Array.isArray(input) ||
|
|
203
|
+
typeof input.name !== 'string') {
|
|
204
|
+
throw new TypeError('Durable signal request is invalid.');
|
|
205
|
+
}
|
|
206
|
+
const { name, value } = input;
|
|
207
|
+
if (name.length === 0 || name.length > MAXIMUM_PROMISE_NAME) {
|
|
208
|
+
throw new TypeError('Durable signal name must have 1 to 128 characters.');
|
|
209
|
+
}
|
|
210
|
+
return { name, value: value ?? null };
|
|
211
|
+
}
|
|
@@ -13,9 +13,10 @@ export { error } from './error.js';
|
|
|
13
13
|
export type { DeclaredErrorDefinition, DeclaredErrorFamily } from './error.js';
|
|
14
14
|
export { classIcon } from './class-icon.js';
|
|
15
15
|
export type { ClassIcon } from './class-icon.js';
|
|
16
|
+
export type { StoredPropertyInputOf } from './property-input.js';
|
|
16
17
|
export type { CallablePolicyBuilder, CallablePolicyContext, CallableProjection, AuthoredMethodProjection, AuthoredPropertyProjection, ClassIdentityProjection, ClassOwnerProjection, CoreEdge, CoreEndpoint, CoreNode, DefinedSchema, DirectedEdgeClass, ClassReferenceProjection, ClassProjection, FunctionProjection, EdgeClass, Function, Method, MethodProjection, Lazy, NodeClass, Policy, PolicyExistsContext, PolicyNodeBinder, PolicyPatternContext, Property, PropertyProjection, ResolvedDomainOfInput, View, } from '@astrale-os/kernel-dsl/v1/builder';
|
|
17
18
|
export type { Array, Object, Value } from '@astrale-os/kernel-dsl/value';
|
|
18
|
-
export type { ClassKind, DirectedEdgeContract, EdgeClassPolicies, NodeClassDefinition, PolicyCheckObject, PolicyObjectRef, PropertyFacet, StateTopology, UndirectedEdgeContract, } from '@astrale-os/kernel-dsl/v1/language';
|
|
19
|
+
export type { CallableExecution, ClassKind, DirectedEdgeContract, EdgeClassPolicies, NodeClassDefinition, PolicyCheckObject, PolicyObjectRef, PropertyFacet, StateTopology, UndirectedEdgeContract, } from '@astrale-os/kernel-dsl/v1/language';
|
|
19
20
|
export type { DomainSchema } from '@astrale-os/kernel-dsl/v1/schema';
|
|
20
21
|
/** Kernel-owned Schema values used by authored product Schemas and graph operations. */
|
|
21
22
|
export { AuthorityOperationOrder, AuthorityOperations, AuthorityRelations, KernelRootFunctions, OperationCore, operationOf, } from '@astrale-os/kernel-core/schema';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ResolvedPropertyCollection } from '@astrale-os/kernel-dsl/v1/language';
|
|
2
|
+
type StoredValueOf<Property> = Property extends {
|
|
3
|
+
readonly __value?: infer Value;
|
|
4
|
+
} ? Value : never;
|
|
5
|
+
/** Values already held by graph Properties, including a blob's Ref rather than its write receipt. */
|
|
6
|
+
export type StoredPropertyInputOf<Class> = Class extends {
|
|
7
|
+
readonly properties: infer Collection;
|
|
8
|
+
} ? Collection extends {
|
|
9
|
+
readonly byKey: infer ByKey;
|
|
10
|
+
} ? Readonly<{
|
|
11
|
+
[Key in keyof ByKey]?: StoredValueOf<ByKey[Key]>;
|
|
12
|
+
} & {
|
|
13
|
+
[Name in Exclude<keyof Collection, keyof ResolvedPropertyCollection>]?: StoredValueOf<Collection[Name]>;
|
|
14
|
+
}> : never : never;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Revision } from '@astrale-os/kernel-dsl/v1/addressing';
|
|
2
|
+
import { type Version } from './version.js';
|
|
3
|
+
/**
|
|
4
|
+
* The highest stable, non-yanked version of a dependent Domain whose dependencies contain the target
|
|
5
|
+
* exactly, by origin and revision: the version to install beside that dependency. Semver is never a
|
|
6
|
+
* compatibility rule; only the exact revision a release was built against is.
|
|
7
|
+
*/
|
|
8
|
+
export declare function compatibleDependentVersion(index: readonly {
|
|
9
|
+
readonly version: Version;
|
|
10
|
+
readonly yanked: boolean;
|
|
11
|
+
readonly dependencies: readonly {
|
|
12
|
+
readonly origin: string;
|
|
13
|
+
readonly revision: Revision;
|
|
14
|
+
}[];
|
|
15
|
+
}[], target: {
|
|
16
|
+
readonly origin: string;
|
|
17
|
+
readonly revision: Revision;
|
|
18
|
+
}): Version | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { isStable } from './version.js';
|
|
3
|
+
/**
|
|
4
|
+
* The highest stable, non-yanked version of a dependent Domain whose dependencies contain the target
|
|
5
|
+
* exactly, by origin and revision: the version to install beside that dependency. Semver is never a
|
|
6
|
+
* compatibility rule; only the exact revision a release was built against is.
|
|
7
|
+
*/
|
|
8
|
+
export function compatibleDependentVersion(index, target) {
|
|
9
|
+
const compatible = index
|
|
10
|
+
.filter(({ version, yanked, dependencies }) => !yanked &&
|
|
11
|
+
isStable(version) &&
|
|
12
|
+
dependencies.some(({ origin, revision }) => origin === target.origin && revision === target.revision))
|
|
13
|
+
.map(({ version }) => version);
|
|
14
|
+
return semver.rsort(compatible)[0];
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Semver policy of Domain versions, shared by `diff`, `publish` and install resolution. */
|
|
2
|
+
export { compatibleDependentVersion } from './dependents.js';
|
|
3
|
+
export { deploymentName } from './name.js';
|
|
4
|
+
export { parseReference, resolveVersion, type Reference } from './reference.js';
|
|
5
|
+
export { acceptVersion, baselineVersion, effectiveLevel, judgeVersion, minimumVersion, type BumpLevel, type PublishedVersion, type Version, type VersionVerdict, } from './version.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Semver policy of Domain versions, shared by `diff`, `publish` and install resolution. */
|
|
2
|
+
export { compatibleDependentVersion } from './dependents.js';
|
|
3
|
+
export { deploymentName } from './name.js';
|
|
4
|
+
export { parseReference, resolveVersion } from './reference.js';
|
|
5
|
+
export { acceptVersion, baselineVersion, effectiveLevel, judgeVersion, minimumVersion, } from './version.js';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Digest } from '@astrale-os/kernel-protocol/artifact';
|
|
2
|
+
/**
|
|
3
|
+
* The computed, never stored, name of one deployment. A version naming its release names it
|
|
4
|
+
* (`1.5.0`); a version naming its build elsewhere names it in its environment (`1.5.0 · staging`).
|
|
5
|
+
* Otherwise its recorded commit describes it from the last release tag, like `git describe` and Go
|
|
6
|
+
* pseudo-versions (`1.4.2 + 7 commits · a1b2c3d · staging`). A modified tree suffixes the commit
|
|
7
|
+
* token with `-dirty` and keeps the base and the environment (`a1b2c3d-dirty · staging`), as
|
|
8
|
+
* `git describe --dirty` does. When several published versions name one release or build, the
|
|
9
|
+
* highest that is not yanked names it, else the highest yanked one. Every name but a release's
|
|
10
|
+
* carries its environment, and listings print the name as returned, never shortened.
|
|
11
|
+
*
|
|
12
|
+
* The deployer declares the record and anyone may read it, so only the `DeploymentCommitV1` shape
|
|
13
|
+
* enters the name. A base enters only as a canonical version, the release tag without its `v` or
|
|
14
|
+
* `<component>-v` prefix, with a non-negative integer distance. A commit whose SHA is not 40
|
|
15
|
+
* hexadecimal digits names a plain preview, and the commit token is always printed in lower case,
|
|
16
|
+
* like `git describe`. An environment outside the `defineProject` grammar is rendered as `?`, so no
|
|
17
|
+
* record can put control characters or a ` · ` of its own in the name. Likewise only canonical
|
|
18
|
+
* published versions name a deployment.
|
|
19
|
+
*/
|
|
20
|
+
export declare function deploymentName(deployment: {
|
|
21
|
+
readonly environment: string;
|
|
22
|
+
readonly releaseDigest: Digest;
|
|
23
|
+
readonly buildDigest: Digest;
|
|
24
|
+
readonly commit: {
|
|
25
|
+
readonly sha: string;
|
|
26
|
+
readonly dirty: boolean;
|
|
27
|
+
readonly base?: {
|
|
28
|
+
readonly version: string;
|
|
29
|
+
readonly distance: number;
|
|
30
|
+
};
|
|
31
|
+
} | null;
|
|
32
|
+
}, publications: readonly {
|
|
33
|
+
readonly version: string;
|
|
34
|
+
readonly releaseDigest: Digest;
|
|
35
|
+
readonly buildDigest: Digest;
|
|
36
|
+
readonly yanked: boolean;
|
|
37
|
+
}[]): string;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
import { ENVIRONMENT_NAME } from '../../deployment/environment.js';
|
|
3
|
+
import { isVersion } from './version.js';
|
|
4
|
+
/** A commit as `DeploymentCommitV1` records it: the full hexadecimal SHA-1, in either case. */
|
|
5
|
+
const SHA = /^[0-9a-f]{40}$/iu;
|
|
6
|
+
/** Stands for an environment outside the project environment-name grammar. */
|
|
7
|
+
const UNKNOWN_ENVIRONMENT = '?';
|
|
8
|
+
/**
|
|
9
|
+
* The computed, never stored, name of one deployment. A version naming its release names it
|
|
10
|
+
* (`1.5.0`); a version naming its build elsewhere names it in its environment (`1.5.0 · staging`).
|
|
11
|
+
* Otherwise its recorded commit describes it from the last release tag, like `git describe` and Go
|
|
12
|
+
* pseudo-versions (`1.4.2 + 7 commits · a1b2c3d · staging`). A modified tree suffixes the commit
|
|
13
|
+
* token with `-dirty` and keeps the base and the environment (`a1b2c3d-dirty · staging`), as
|
|
14
|
+
* `git describe --dirty` does. When several published versions name one release or build, the
|
|
15
|
+
* highest that is not yanked names it, else the highest yanked one. Every name but a release's
|
|
16
|
+
* carries its environment, and listings print the name as returned, never shortened.
|
|
17
|
+
*
|
|
18
|
+
* The deployer declares the record and anyone may read it, so only the `DeploymentCommitV1` shape
|
|
19
|
+
* enters the name. A base enters only as a canonical version, the release tag without its `v` or
|
|
20
|
+
* `<component>-v` prefix, with a non-negative integer distance. A commit whose SHA is not 40
|
|
21
|
+
* hexadecimal digits names a plain preview, and the commit token is always printed in lower case,
|
|
22
|
+
* like `git describe`. An environment outside the `defineProject` grammar is rendered as `?`, so no
|
|
23
|
+
* record can put control characters or a ` · ` of its own in the name. Likewise only canonical
|
|
24
|
+
* published versions name a deployment.
|
|
25
|
+
*/
|
|
26
|
+
export function deploymentName(deployment, publications) {
|
|
27
|
+
const { commit } = deployment;
|
|
28
|
+
const environment = typeof deployment.environment === 'string' && ENVIRONMENT_NAME.test(deployment.environment)
|
|
29
|
+
? deployment.environment
|
|
30
|
+
: UNKNOWN_ENVIRONMENT;
|
|
31
|
+
const release = highest(publications.filter(({ releaseDigest }) => releaseDigest === deployment.releaseDigest));
|
|
32
|
+
if (release !== undefined)
|
|
33
|
+
return release;
|
|
34
|
+
const build = highest(publications.filter(({ buildDigest }) => buildDigest === deployment.buildDigest));
|
|
35
|
+
if (build !== undefined)
|
|
36
|
+
return `${build} · ${environment}`;
|
|
37
|
+
if (commit === null || typeof commit.sha !== 'string' || !SHA.test(commit.sha)) {
|
|
38
|
+
return `preview · ${environment}`;
|
|
39
|
+
}
|
|
40
|
+
const sha = `${commit.sha.slice(0, 7).toLowerCase()}${commit.dirty === true ? '-dirty' : ''}`;
|
|
41
|
+
const base = describeBase(commit.base);
|
|
42
|
+
return base === undefined ? `${sha} · ${environment}` : `${base} · ${sha} · ${environment}`;
|
|
43
|
+
}
|
|
44
|
+
function describeBase(base) {
|
|
45
|
+
if (base === undefined || !isVersion(base.version))
|
|
46
|
+
return undefined;
|
|
47
|
+
const { version, distance } = base;
|
|
48
|
+
if (!Number.isSafeInteger(distance) || distance < 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
return distance === 0 ? version : `${version} + ${distance} commit${distance === 1 ? '' : 's'}`;
|
|
51
|
+
}
|
|
52
|
+
function highest(publications) {
|
|
53
|
+
const versions = (yanked) => publications
|
|
54
|
+
.filter((publication) => publication.yanked === yanked && isVersion(publication.version))
|
|
55
|
+
.map(({ version }) => version);
|
|
56
|
+
return semver.rsort(versions(false))[0] ?? semver.rsort(versions(true))[0];
|
|
57
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type PublishedVersion, type Version } from './version.js';
|
|
2
|
+
/**
|
|
3
|
+
* One install reference. The syntax decides: a version goes to the registry, a deployment URL is
|
|
4
|
+
* installed as is, without any lookup.
|
|
5
|
+
*/
|
|
6
|
+
export type Reference = {
|
|
7
|
+
readonly kind: 'url';
|
|
8
|
+
readonly url: string;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: 'exact';
|
|
11
|
+
readonly origin: string;
|
|
12
|
+
readonly version: Version;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: 'line';
|
|
15
|
+
readonly origin: string;
|
|
16
|
+
readonly line: `${number}.${number}`;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parse `<origin>@<major>.<minor>.<patch>[-<pre>]`, `<origin>@<major>.<minor>` or a deployment URL.
|
|
20
|
+
* Ranges are not references: `@1`, `^1.5` and `1.5.x` are refused, since only the exact version and
|
|
21
|
+
* the highest stable version of one minor line are ever resolved. A URL with credentials is refused:
|
|
22
|
+
* `https://issues.astrale.ai@1.5.0` would otherwise name the host `1.5.0`, not a version.
|
|
23
|
+
*
|
|
24
|
+
* A deployment URL is returned as written, so it must already be the URL the Kernel fetches: one the
|
|
25
|
+
* URL parser keeps as is, up to the `/` of an empty path. Whitespace and control characters (which
|
|
26
|
+
* the parser strips), an upper-case host, a default port or a missing `//` host are refused, as are a
|
|
27
|
+
* query and a fragment, which no deployment URL carries.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseReference(input: string): Reference;
|
|
30
|
+
/**
|
|
31
|
+
* Translate a version reference into one published version. An exact reference names any published
|
|
32
|
+
* version, a pre-release or a yanked one included (the caller warns on yanked). A line resolves to
|
|
33
|
+
* its highest stable version that is not yanked.
|
|
34
|
+
*
|
|
35
|
+
* A reference built from data rather than by `parseReference` is checked the same way: an exact
|
|
36
|
+
* version must be canonical and a line must be `<major>.<minor>`, so `1` never resolves as a major
|
|
37
|
+
* range and `1.5.3` never as an exact pin. Anything else throws a `TypeError`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveVersion(published: readonly PublishedVersion[], reference: Exclude<Reference, {
|
|
40
|
+
kind: 'url';
|
|
41
|
+
}>): {
|
|
42
|
+
readonly kind: 'resolved';
|
|
43
|
+
readonly version: Version;
|
|
44
|
+
readonly yanked: boolean;
|
|
45
|
+
} | {
|
|
46
|
+
readonly kind: 'unresolved';
|
|
47
|
+
readonly reason: 'unknown-version' | 'no-stable-match' | 'yanked-only';
|
|
48
|
+
};
|