@astrale-os/kernel-server 1.0.0-beta.1 → 1.0.0-beta.11
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/dist/errors/classify.d.ts +3 -1
- package/dist/errors/classify.js +12 -2
- package/dist/invocation/types.d.ts +2 -0
- package/dist/lifecycle/active.d.ts +2 -1
- package/dist/lifecycle/active.js +7 -3
- package/dist/routes/native.js +11 -0
- package/dist/server.js +26 -2
- package/dist/transport/http/handler.js +1 -0
- package/dist/transport/http/render.js +8 -0
- package/dist/transport/websocket/session.js +9 -0
- package/dist/types.d.ts +10 -0
- package/package.json +4 -4
|
@@ -3,7 +3,9 @@ import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
|
3
3
|
export declare function classifyError(input: unknown): errors.ErrorPayload;
|
|
4
4
|
/**
|
|
5
5
|
* Classify the failure of one request. Its own deadline or cancellation names the failure, except an
|
|
6
|
-
* outcome already unknown: ending the request proves nothing about an effect.
|
|
6
|
+
* outcome already unknown: ending the request proves nothing about an effect. A request the Server's
|
|
7
|
+
* close ended keeps the draining refusal only when that refusal is the failure itself: the Server
|
|
8
|
+
* refused before dispatch, or the Handler proved it ran nothing.
|
|
7
9
|
*/
|
|
8
10
|
export declare function classifyRequestError(input: unknown, request: {
|
|
9
11
|
readonly timedOut: boolean;
|
package/dist/errors/classify.js
CHANGED
|
@@ -19,7 +19,9 @@ export function classifyError(input) {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Classify the failure of one request. Its own deadline or cancellation names the failure, except an
|
|
22
|
-
* outcome already unknown: ending the request proves nothing about an effect.
|
|
22
|
+
* outcome already unknown: ending the request proves nothing about an effect. A request the Server's
|
|
23
|
+
* close ended keeps the draining refusal only when that refusal is the failure itself: the Server
|
|
24
|
+
* refused before dispatch, or the Handler proved it ran nothing.
|
|
23
25
|
*/
|
|
24
26
|
export function classifyRequestError(input, request) {
|
|
25
27
|
if (input instanceof errors.ProtocolError &&
|
|
@@ -28,10 +30,18 @@ export function classifyRequestError(input, request) {
|
|
|
28
30
|
}
|
|
29
31
|
if (request.timedOut)
|
|
30
32
|
return publicError('DEADLINE_EXCEEDED', 'Invocation deadline exceeded.');
|
|
31
|
-
if (request.signal.aborted)
|
|
33
|
+
if (request.signal.aborted) {
|
|
34
|
+
if (draining(request.signal.reason) && draining(input))
|
|
35
|
+
return input.payload;
|
|
32
36
|
return publicError('CANCELLED', 'Invocation was cancelled.');
|
|
37
|
+
}
|
|
33
38
|
return classifyError(input);
|
|
34
39
|
}
|
|
40
|
+
function draining(input) {
|
|
41
|
+
return (input instanceof errors.ProtocolError &&
|
|
42
|
+
errors.retryAfterOf(input.payload) !== undefined &&
|
|
43
|
+
errors.reasonOf(input.payload.data)?.code === errors.RETRY_REASONS.SERVER_DRAINING);
|
|
44
|
+
}
|
|
35
45
|
function publicError(name, message) {
|
|
36
46
|
return { code: errors.CODES[name], message };
|
|
37
47
|
}
|
|
@@ -55,6 +55,8 @@ export interface HandlerRequest {
|
|
|
55
55
|
readonly via?: protocol.Via;
|
|
56
56
|
/** Source-only local-target or remote-revision Schema Registration precondition. */
|
|
57
57
|
readonly schema?: protocol.SchemaTarget;
|
|
58
|
+
/** Source-only maintenance session guard forwarded unchanged to Runtime. */
|
|
59
|
+
readonly maintenance?: protocol.Maintenance;
|
|
58
60
|
/** Exact Server-pinned release for a request at a release-backed destination. */
|
|
59
61
|
readonly release?: ReleaseSnapshot;
|
|
60
62
|
readonly context: InvocationContext;
|
|
@@ -11,7 +11,8 @@ export interface ActiveOperation {
|
|
|
11
11
|
readonly timedOut: boolean;
|
|
12
12
|
/** Register cooperative cleanup and return an idempotent unregistration function. */
|
|
13
13
|
onCancel(cancel: CancelWork): () => void;
|
|
14
|
-
|
|
14
|
+
/** Abort with `cause` as the signal reason, or a plain Error naming `reason`. */
|
|
15
|
+
cancel(reason: string, cause?: Error): void;
|
|
15
16
|
/** Replace the remaining budget after a trusted transport binding is admitted. */
|
|
16
17
|
extend(timeoutMs: number): void;
|
|
17
18
|
settle(): void;
|
package/dist/lifecycle/active.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
2
|
+
import { admissionRefusal } from '../errors/index.js';
|
|
1
3
|
// Streaming iterators can need several microtasks to return after cancellation. Keep the
|
|
2
4
|
// cleanup join bounded even when a producer ignores its cancellation signal.
|
|
3
5
|
const CANCELLATION_CLEANUP_GRACE_MS = 250;
|
|
@@ -85,8 +87,10 @@ export class ActiveOperations {
|
|
|
85
87
|
await this.waitUntilIdleOr(deadline);
|
|
86
88
|
const hasCancellationWork = [...this.operations].some((operation) => operation.hasCancellationWork);
|
|
87
89
|
if (this.operations.size > 0) {
|
|
90
|
+
// The typed draining refusal lets a Handler that ran nothing answer it, to be retried elsewhere.
|
|
91
|
+
const closing = new errors.ProtocolError(admissionRefusal(false));
|
|
88
92
|
for (const operation of this.operations)
|
|
89
|
-
operation.cancel('server closing');
|
|
93
|
+
operation.cancel('server closing', closing);
|
|
90
94
|
}
|
|
91
95
|
this.closeSockets();
|
|
92
96
|
if (hasCancellationWork) {
|
|
@@ -196,13 +200,13 @@ class Operation {
|
|
|
196
200
|
this.cancelWork.delete(cancel);
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
|
-
cancel(reason) {
|
|
203
|
+
cancel(reason, cause) {
|
|
200
204
|
if (this.cancelled || this.settled)
|
|
201
205
|
return;
|
|
202
206
|
this.cancelled = true;
|
|
203
207
|
clearTimeout(this.timer);
|
|
204
208
|
this.detachExternal();
|
|
205
|
-
this.controller.abort(new Error(reason));
|
|
209
|
+
this.controller.abort(cause ?? new Error(reason));
|
|
206
210
|
for (const cancel of this.cancelWork)
|
|
207
211
|
void settleCancellation(cancel, reason);
|
|
208
212
|
this.cancelWork.clear();
|
package/dist/routes/native.js
CHANGED
|
@@ -67,6 +67,17 @@ export async function nativeResponse(request, snapshot, options, responseCors) {
|
|
|
67
67
|
},
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
+
if (result.kind === 'execution') {
|
|
71
|
+
// The call has started or replayed its Execution: accept it with the reference, never a
|
|
72
|
+
// failure, saying whether this call created it.
|
|
73
|
+
return new Response(encodedBody(codecs.json.encode({ id: result.execution, replayed: result.replayed })), {
|
|
74
|
+
status: 202,
|
|
75
|
+
headers: {
|
|
76
|
+
...responseCors,
|
|
77
|
+
'content-type': 'application/json',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
70
81
|
if (result.kind === 'binary') {
|
|
71
82
|
if (isStreamingBinary(result.value)) {
|
|
72
83
|
// Hand the operation off only once the streaming response is built. A refused render
|
package/dist/server.js
CHANGED
|
@@ -71,7 +71,13 @@ function createPreparedServer(input, config) {
|
|
|
71
71
|
if (config.health !== undefined) {
|
|
72
72
|
app.get(config.health.path, (context) => {
|
|
73
73
|
const ready = active.isAvailable && config.health.probe.check();
|
|
74
|
-
|
|
74
|
+
const maintenance = config.health.probe.maintenance?.();
|
|
75
|
+
const uncertified = config.health.probe.uncertified?.() === true;
|
|
76
|
+
return context.json({
|
|
77
|
+
status: ready ? 'ready' : 'not-ready',
|
|
78
|
+
...(maintenance === undefined ? {} : { maintenance }),
|
|
79
|
+
...(uncertified ? { certification: 'uncertified' } : {}),
|
|
80
|
+
}, ready ? 200 : 503, {
|
|
75
81
|
...config.cors.response(context.req.raw),
|
|
76
82
|
'cache-control': 'no-store',
|
|
77
83
|
});
|
|
@@ -224,7 +230,25 @@ function servedDocumentPaths(_kind, issuer) {
|
|
|
224
230
|
}
|
|
225
231
|
function captureHealthProbe(probe) {
|
|
226
232
|
const check = captureMethod(probe, 'check', 'Health probe is invalid.');
|
|
227
|
-
|
|
233
|
+
const maintenance = probe.maintenance === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: captureMethod(probe, 'maintenance', 'Health probe is invalid.');
|
|
236
|
+
const uncertified = probe.uncertified === undefined
|
|
237
|
+
? undefined
|
|
238
|
+
: captureMethod(probe, 'uncertified', 'Health probe is invalid.');
|
|
239
|
+
return Object.freeze({
|
|
240
|
+
check,
|
|
241
|
+
...(maintenance === undefined
|
|
242
|
+
? {}
|
|
243
|
+
: {
|
|
244
|
+
// A mode that is not a short token is not reported rather than echoed.
|
|
245
|
+
maintenance: () => {
|
|
246
|
+
const mode = maintenance();
|
|
247
|
+
return typeof mode === 'string' && /^[a-z]{1,32}$/u.test(mode) ? mode : undefined;
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
...(uncertified === undefined ? {} : { uncertified: () => uncertified() === true }),
|
|
251
|
+
});
|
|
228
252
|
}
|
|
229
253
|
function captureWebSocket(adapter) {
|
|
230
254
|
const upgrade = captureMethod(adapter, 'upgrade', 'WebSocket adapter is invalid.');
|
|
@@ -176,6 +176,7 @@ export function createHttpHandler(config) {
|
|
|
176
176
|
...(envelope.delegate === undefined ? {} : { delegate: envelope.delegate }),
|
|
177
177
|
...(envelope.routing?.kind === 'via' ? { via: envelope.routing } : {}),
|
|
178
178
|
...(envelope.schema === undefined ? {} : { schema: envelope.schema }),
|
|
179
|
+
...(envelope.maintenance === undefined ? {} : { maintenance: envelope.maintenance }),
|
|
179
180
|
...(served === undefined ? {} : pinned(served)),
|
|
180
181
|
context: invocationContext(assigned, active, config.active, host, undefined, envelope.idempotencyKey, binary),
|
|
181
182
|
}));
|
|
@@ -15,6 +15,14 @@ export function renderHttpResult(result, context) {
|
|
|
15
15
|
invocation: context.invocation,
|
|
16
16
|
};
|
|
17
17
|
break;
|
|
18
|
+
case 'execution':
|
|
19
|
+
envelope = {
|
|
20
|
+
execution: result.execution,
|
|
21
|
+
replayed: result.replayed,
|
|
22
|
+
requestId: context.requestId,
|
|
23
|
+
invocation: context.invocation,
|
|
24
|
+
};
|
|
25
|
+
break;
|
|
18
26
|
case 'redirect':
|
|
19
27
|
envelope = renderRedirect(context.requestId, context.invocation, result.redirect);
|
|
20
28
|
break;
|
|
@@ -142,6 +142,7 @@ export function createWebSocketSession(config) {
|
|
|
142
142
|
...(envelope.delegate === undefined ? {} : { delegate: envelope.delegate }),
|
|
143
143
|
...(envelope.routing?.kind === 'via' ? { via: envelope.routing } : {}),
|
|
144
144
|
...(envelope.schema === undefined ? {} : { schema: envelope.schema }),
|
|
145
|
+
...(envelope.maintenance === undefined ? {} : { maintenance: envelope.maintenance }),
|
|
145
146
|
...(served === undefined ? {} : pinned(served)),
|
|
146
147
|
context: invocationContext(assigned, operation, config.active, config.host, undefined, envelope.idempotencyKey),
|
|
147
148
|
}));
|
|
@@ -194,6 +195,14 @@ async function sendResult(socket, codec, requestId, assigned, result, operation,
|
|
|
194
195
|
invocation: assigned,
|
|
195
196
|
}, sendOutput);
|
|
196
197
|
return;
|
|
198
|
+
case 'execution':
|
|
199
|
+
await send(socket, codec, {
|
|
200
|
+
execution: result.execution,
|
|
201
|
+
replayed: result.replayed,
|
|
202
|
+
requestId,
|
|
203
|
+
invocation: assigned,
|
|
204
|
+
}, sendOutput);
|
|
205
|
+
return;
|
|
197
206
|
case 'redirect':
|
|
198
207
|
await send(socket, codec, renderRedirect(requestId, assigned, result.redirect), sendOutput);
|
|
199
208
|
return;
|
package/dist/types.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ export interface ServerCloseOptions {
|
|
|
35
35
|
export interface HealthProbe {
|
|
36
36
|
/** Non-blocking snapshot; health never performs provider I/O. */
|
|
37
37
|
check(): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The Instance admission mode the Host reports beside readiness: maintenance never lowers
|
|
40
|
+
* readiness, so health names it instead. A non-blocking snapshot, absent to report nothing.
|
|
41
|
+
*/
|
|
42
|
+
maintenance?(): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* True while the Instance carries the break-glass `uncertified` mark: no certificate covers its
|
|
45
|
+
* data until an install validates it in full. Health names it; it never lowers readiness.
|
|
46
|
+
*/
|
|
47
|
+
uncertified?(): boolean;
|
|
38
48
|
}
|
|
39
49
|
/** Static transport configuration admitted before Runtime Boot performs I/O. */
|
|
40
50
|
export interface ServerOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-server",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.11",
|
|
4
4
|
"description": "Portable HTTP and WebSocket boundary for Astrale invocation, Release, installation, and issuer protocols",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"access": "public"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@astrale-os/kernel-core": "1.0.0-beta.
|
|
29
|
-
"@astrale-os/kernel-dsl": "1.0.0-beta.
|
|
30
|
-
"@astrale-os/kernel-protocol": "1.0.0-beta.
|
|
28
|
+
"@astrale-os/kernel-core": "1.0.0-beta.11",
|
|
29
|
+
"@astrale-os/kernel-dsl": "1.0.0-beta.5",
|
|
30
|
+
"@astrale-os/kernel-protocol": "1.0.0-beta.11",
|
|
31
31
|
"hono": "^4.6.20"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|