@astrale-os/kernel-server 0.5.0-beta.13 → 0.5.0-beta.15
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/lifecycle/active.d.ts +2 -0
- package/dist/lifecycle/active.js +5 -2
- package/dist/routes/routes.js +65 -0
- package/dist/server.js +1 -1
- package/dist/transport/http/render.d.ts +5 -3
- package/dist/transport/http/render.js +70 -1
- package/dist/transport/websocket/session.js +12 -1
- package/dist/types.d.ts +1 -1
- package/package.json +5 -5
|
@@ -26,6 +26,8 @@ export declare class ActiveOperations {
|
|
|
26
26
|
private changed;
|
|
27
27
|
constructor(maximumConcurrentOperations: number);
|
|
28
28
|
get isAccepting(): boolean;
|
|
29
|
+
/** Whether a newly arriving operation can be admitted without semantic dispatch. */
|
|
30
|
+
get isAvailable(): boolean;
|
|
29
31
|
begin(timeoutMs: number, externalSignal?: AbortSignal): ActiveOperation | undefined;
|
|
30
32
|
attachSocket(socket: ActiveSocket): boolean;
|
|
31
33
|
detachSocket(socket: ActiveSocket): void;
|
package/dist/lifecycle/active.js
CHANGED
|
@@ -13,10 +13,13 @@ export class ActiveOperations {
|
|
|
13
13
|
get isAccepting() {
|
|
14
14
|
return this.accepting;
|
|
15
15
|
}
|
|
16
|
+
/** Whether a newly arriving operation can be admitted without semantic dispatch. */
|
|
17
|
+
get isAvailable() {
|
|
18
|
+
return this.accepting && this.operations.size < this.maximumConcurrentOperations;
|
|
19
|
+
}
|
|
16
20
|
begin(timeoutMs, externalSignal) {
|
|
17
|
-
if (!this.
|
|
21
|
+
if (!this.isAvailable)
|
|
18
22
|
return undefined;
|
|
19
|
-
}
|
|
20
23
|
const operation = new Operation(Date.now() + timeoutMs, () => {
|
|
21
24
|
this.operations.delete(operation);
|
|
22
25
|
this.changed?.();
|
package/dist/routes/routes.js
CHANGED
|
@@ -138,8 +138,13 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
138
138
|
});
|
|
139
139
|
}
|
|
140
140
|
if (result.kind === 'binary') {
|
|
141
|
+
if (isStreamingBinary(result.value)) {
|
|
142
|
+
handedOff = true;
|
|
143
|
+
return streamingBinaryResponse(result.value, operation, cors, request);
|
|
144
|
+
}
|
|
141
145
|
const applicationHeaders = applicationBinaryHeaders(result.value.headers);
|
|
142
146
|
return new Response(toBody(result.value.body), {
|
|
147
|
+
status: result.value.status ?? 200,
|
|
143
148
|
headers: {
|
|
144
149
|
...cors.response(request, [
|
|
145
150
|
protocol.http.BINARY_HEADERS_FIELD,
|
|
@@ -197,6 +202,66 @@ export function mountRoutes(app, source, deliveryProvider, handler, active, next
|
|
|
197
202
|
}
|
|
198
203
|
});
|
|
199
204
|
}
|
|
205
|
+
function streamingBinaryResponse(binary, operation, cors, request) {
|
|
206
|
+
const iterator = binary.body[Symbol.asyncIterator]();
|
|
207
|
+
const applicationHeaders = applicationBinaryHeaders(binary.headers);
|
|
208
|
+
let terminal = false;
|
|
209
|
+
let stopping;
|
|
210
|
+
const stop = () => {
|
|
211
|
+
stopping ??= Promise.resolve().then(async () => {
|
|
212
|
+
if (terminal)
|
|
213
|
+
return;
|
|
214
|
+
terminal = true;
|
|
215
|
+
try {
|
|
216
|
+
await iterator.return?.();
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
operation.settle();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
return stopping;
|
|
223
|
+
};
|
|
224
|
+
const body = new ReadableStream({
|
|
225
|
+
async pull(controller) {
|
|
226
|
+
if (terminal)
|
|
227
|
+
return;
|
|
228
|
+
try {
|
|
229
|
+
const next = await iterator.next();
|
|
230
|
+
if (terminal)
|
|
231
|
+
return;
|
|
232
|
+
if (next.done) {
|
|
233
|
+
terminal = true;
|
|
234
|
+
controller.close();
|
|
235
|
+
operation.settle();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
controller.enqueue(next.value);
|
|
239
|
+
}
|
|
240
|
+
catch (cause) {
|
|
241
|
+
terminal = true;
|
|
242
|
+
controller.error(cause);
|
|
243
|
+
operation.settle();
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
async cancel(reason) {
|
|
247
|
+
const message = typeof reason === 'string' ? reason : 'native binary response cancelled';
|
|
248
|
+
operation.cancel(message);
|
|
249
|
+
await stop();
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
operation.onCancel(stop);
|
|
253
|
+
return new Response(body, {
|
|
254
|
+
status: binary.status ?? 200,
|
|
255
|
+
headers: {
|
|
256
|
+
...cors.response(request, Object.keys(applicationHeaders)),
|
|
257
|
+
...applicationHeaders,
|
|
258
|
+
'content-type': binary.mediaType,
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function isStreamingBinary(input) {
|
|
263
|
+
return !(input.body instanceof Uint8Array);
|
|
264
|
+
}
|
|
200
265
|
function publicationPreflight(cors, request, snapshot, externalOrigin) {
|
|
201
266
|
if (sameRoute(request.url, snapshot.publication.schema.bundle.href, externalOrigin)) {
|
|
202
267
|
return cors.preflight(request, { methods: ['GET'], headers: ['if-none-match'] });
|
package/dist/server.js
CHANGED
|
@@ -68,7 +68,7 @@ function createPreparedServer(input, config) {
|
|
|
68
68
|
app.all(config.path, (context) => handleHttp(context.req.raw, hosts.get(context.req.raw)));
|
|
69
69
|
if (config.health !== undefined) {
|
|
70
70
|
app.get(config.health.path, (context) => {
|
|
71
|
-
const ready = active.
|
|
71
|
+
const ready = active.isAvailable && config.health.probe.check();
|
|
72
72
|
return context.json({ status: ready ? 'ready' : 'not-ready' }, ready ? 200 : 503, {
|
|
73
73
|
...config.cors.response(context.req.raw),
|
|
74
74
|
'cache-control': 'no-store',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BufferedBinaryResult, StreamingBinaryResult } from '@astrale-os/kernel-core/callable';
|
|
1
2
|
import type { Codec } from '@astrale-os/kernel-protocol/codecs';
|
|
2
3
|
import * as errors from '@astrale-os/kernel-protocol/errors';
|
|
3
4
|
import * as protocol from '@astrale-os/kernel-protocol/invocation';
|
|
@@ -19,13 +20,14 @@ export interface ErrorRenderOptions {
|
|
|
19
20
|
}
|
|
20
21
|
type HandlerResult = Awaited<ReturnType<Handler['dispatch']>>;
|
|
21
22
|
export declare function renderHttpResult(result: HandlerResult, context: RenderContext): Response;
|
|
22
|
-
export declare function renderBinary(requestId: protocol.RequestId, invocation: protocol.InvocationId, binary:
|
|
23
|
-
readonly kind: 'binary';
|
|
24
|
-
}>['value']): protocol.BinaryResponse;
|
|
23
|
+
export declare function renderBinary(requestId: protocol.RequestId, invocation: protocol.InvocationId, binary: BufferedBinaryResult): protocol.BinaryResponse;
|
|
25
24
|
export declare function renderRedirect(requestId: protocol.RequestId, invocation: protocol.InvocationId, redirect: Extract<HandlerResult, {
|
|
26
25
|
readonly kind: 'redirect';
|
|
27
26
|
}>['redirect']): protocol.RedirectResponse;
|
|
28
27
|
export declare function renderHttpError(requestId: protocol.RequestId, invocation: protocol.InvocationId, error: errors.ErrorPayload, codec: Codec, cors: CorsHeaders, options?: ErrorRenderOptions): Response;
|
|
29
28
|
export declare function renderHttpStale(requestId: protocol.RequestId, invocation: protocol.InvocationId, current: protocol.Stale['current'], codec: Codec, cors: CorsHeaders, maximumResponseBytes?: number): Response;
|
|
30
29
|
export declare function encodeEnvelope(codec: Codec, input: unknown): string | Uint8Array;
|
|
30
|
+
export declare function isStreamingBinary(input: Extract<HandlerResult, {
|
|
31
|
+
readonly kind: 'binary';
|
|
32
|
+
}>['value']): input is StreamingBinaryResult;
|
|
31
33
|
export {};
|
|
@@ -19,6 +19,8 @@ export function renderHttpResult(result, context) {
|
|
|
19
19
|
case 'miss':
|
|
20
20
|
return settled(context.active, envelopeResponse(context.codec, protocol.miss(context.requestId, context.invocation), context.cors(), 200, true, context.maximumResponseBytes));
|
|
21
21
|
case 'binary': {
|
|
22
|
+
if (isStreamingBinary(result.value))
|
|
23
|
+
return streamingBinaryHttpResponse(result.value, context);
|
|
22
24
|
const binary = renderBinary(context.requestId, context.invocation, result.value);
|
|
23
25
|
return settled(context.active, binaryHttpResponse(binary, context.cors, context.maximumResponseBytes));
|
|
24
26
|
}
|
|
@@ -41,6 +43,7 @@ export function renderBinary(requestId, invocation, binary) {
|
|
|
41
43
|
invocation,
|
|
42
44
|
body: binary.body,
|
|
43
45
|
mediaType: binary.mediaType,
|
|
46
|
+
status: binary.status ?? 200,
|
|
44
47
|
...(binary.headers === undefined ? {} : { headers: binary.headers }),
|
|
45
48
|
});
|
|
46
49
|
}
|
|
@@ -75,11 +78,74 @@ function envelopeResponse(codec, input, cors, status = 200, negotiated = true, m
|
|
|
75
78
|
}
|
|
76
79
|
function binaryHttpResponse(binary, cors, maximumResponseBytes) {
|
|
77
80
|
requireResponseBound(binary.body, maximumResponseBytes);
|
|
81
|
+
const headers = binaryHttpHeaders(binary, cors, false);
|
|
82
|
+
return new Response(toBody(binary.body), { status: binary.status, headers });
|
|
83
|
+
}
|
|
84
|
+
function streamingBinaryHttpResponse(binary, context) {
|
|
85
|
+
const iterator = binary.body[Symbol.asyncIterator]();
|
|
86
|
+
const headers = binaryHttpHeaders(Object.freeze({
|
|
87
|
+
requestId: context.requestId,
|
|
88
|
+
invocation: context.invocation,
|
|
89
|
+
mediaType: binary.mediaType,
|
|
90
|
+
headers: binary.headers,
|
|
91
|
+
}), context.cors, true);
|
|
92
|
+
let terminal = false;
|
|
93
|
+
let stopping;
|
|
94
|
+
const stop = () => {
|
|
95
|
+
stopping ??= Promise.resolve().then(async () => {
|
|
96
|
+
if (terminal)
|
|
97
|
+
return;
|
|
98
|
+
terminal = true;
|
|
99
|
+
try {
|
|
100
|
+
await iterator.return?.();
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
context.active.settle();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
return stopping;
|
|
107
|
+
};
|
|
108
|
+
const body = new ReadableStream({
|
|
109
|
+
async pull(controller) {
|
|
110
|
+
if (terminal)
|
|
111
|
+
return;
|
|
112
|
+
try {
|
|
113
|
+
const next = await iterator.next();
|
|
114
|
+
if (terminal)
|
|
115
|
+
return;
|
|
116
|
+
if (next.done) {
|
|
117
|
+
terminal = true;
|
|
118
|
+
controller.close();
|
|
119
|
+
context.active.settle();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
controller.enqueue(next.value);
|
|
123
|
+
}
|
|
124
|
+
catch (cause) {
|
|
125
|
+
terminal = true;
|
|
126
|
+
controller.error(cause);
|
|
127
|
+
context.active.settle();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
async cancel(reason) {
|
|
131
|
+
const message = typeof reason === 'string' ? reason : 'binary response consumer cancelled';
|
|
132
|
+
context.active.cancel(message);
|
|
133
|
+
await stop();
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
context.active.onCancel(stop);
|
|
137
|
+
return new Response(body, { status: binary.status ?? 200, headers });
|
|
138
|
+
}
|
|
139
|
+
export function isStreamingBinary(input) {
|
|
140
|
+
return !(input.body instanceof Uint8Array);
|
|
141
|
+
}
|
|
142
|
+
function binaryHttpHeaders(binary, cors, streaming) {
|
|
78
143
|
const applicationHeaders = Object.keys(binary.headers ?? {}).filter((name) => protocol.http.isApplicationBinaryHeader(name));
|
|
79
144
|
const headers = new Headers(cors([
|
|
80
145
|
protocol.http.REQUEST_ID_FIELD,
|
|
81
146
|
protocol.http.INVOCATION_FIELD,
|
|
82
147
|
protocol.http.BINARY_HEADERS_FIELD,
|
|
148
|
+
...(streaming ? [protocol.http.BINARY_DELIVERY_FIELD] : []),
|
|
83
149
|
...applicationHeaders,
|
|
84
150
|
]));
|
|
85
151
|
for (const [name, value] of Object.entries(binary.headers ?? {})) {
|
|
@@ -90,9 +156,12 @@ function binaryHttpResponse(binary, cors, maximumResponseBytes) {
|
|
|
90
156
|
headers.set(protocol.http.BINARY_HEADERS_FIELD, protocol.http.encodeBinaryHeaderNames(binary.headers));
|
|
91
157
|
headers.set(protocol.http.REQUEST_ID_FIELD, String(binary.requestId));
|
|
92
158
|
headers.set(protocol.http.INVOCATION_FIELD, protocol.http.encodeInvocationId(binary.invocation));
|
|
159
|
+
if (streaming) {
|
|
160
|
+
headers.set(protocol.http.BINARY_DELIVERY_FIELD, protocol.http.STREAMING_BINARY_DELIVERY);
|
|
161
|
+
}
|
|
93
162
|
headers.set('cache-control', 'no-store');
|
|
94
163
|
appendVary(headers, 'accept');
|
|
95
|
-
return
|
|
164
|
+
return headers;
|
|
96
165
|
}
|
|
97
166
|
function streamHttpResponse(stream, context) {
|
|
98
167
|
const iterator = stream[Symbol.asyncIterator]();
|
|
@@ -5,7 +5,7 @@ import { ResponseBodyTooLargeError } from '../../errors/index.js';
|
|
|
5
5
|
import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
|
|
6
6
|
import { byteLength } from '../body.js';
|
|
7
7
|
import { resolveCredential } from '../credential.js';
|
|
8
|
-
import { renderBinary, renderRedirect } from '../http/render.js';
|
|
8
|
+
import { isStreamingBinary, renderBinary, renderRedirect } from '../http/render.js';
|
|
9
9
|
import { interruptedStreamError } from '../stream-error.js';
|
|
10
10
|
const WS_OPEN = 1;
|
|
11
11
|
/** Build one socket-pinned transport session. */
|
|
@@ -211,6 +211,17 @@ async function sendResult(socket, codec, requestId, assigned, result, operation,
|
|
|
211
211
|
await send(socket, codec, protocol.miss(requestId, assigned), sendOutput);
|
|
212
212
|
return;
|
|
213
213
|
case 'binary': {
|
|
214
|
+
if (isStreamingBinary(result.value)) {
|
|
215
|
+
const streaming = result.value;
|
|
216
|
+
await Promise.allSettled([
|
|
217
|
+
sendError(socket, codec, requestId, assigned, {
|
|
218
|
+
code: errors.CODES.UNSUPPORTED,
|
|
219
|
+
message: 'Streaming binary output requires the HTTP transport.',
|
|
220
|
+
}, sendOutput),
|
|
221
|
+
Promise.resolve().then(() => streaming.body[Symbol.asyncIterator]().return?.()),
|
|
222
|
+
]);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
214
225
|
const binary = renderBinary(requestId, assigned, result.value);
|
|
215
226
|
if (socket.readyState === WS_OPEN) {
|
|
216
227
|
await sendOutput(socket, protocol.websocket.encodeBinary(binary));
|
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { PublicationSource } from './publication/index.js';
|
|
|
6
6
|
import type { Application, WebSocketAdapter } from './transport/index.js';
|
|
7
7
|
export interface ServerLimits {
|
|
8
8
|
readonly maximumRequestBytes: number;
|
|
9
|
-
/** Maximum bytes in one encoded envelope,
|
|
9
|
+
/** Maximum bytes in one encoded envelope, buffered binary body, or stream frame. */
|
|
10
10
|
readonly maximumResponseBytes?: number;
|
|
11
11
|
/** Maximum Server-owned active operations across all mounted protocol surfaces. */
|
|
12
12
|
readonly maximumConcurrentOperations?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/kernel-server",
|
|
3
|
-
"version": "0.5.0-beta.
|
|
3
|
+
"version": "0.5.0-beta.15",
|
|
4
4
|
"description": "Portable HTTP and WebSocket boundary for Astrale invocation, publication, installation, and issuer protocols",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@astrale-os/kernel-core": ">=0.9.0-beta.10 <1.0.0",
|
|
37
|
-
"@astrale-os/kernel-dsl": ">=0.2.0-beta.8 <1.0.0",
|
|
38
|
-
"@astrale-os/kernel-protocol": ">=0.5.0-beta.10 <1.0.0",
|
|
39
36
|
"hono": "^4.6.20",
|
|
40
|
-
"msgpackr": "^2.0.5"
|
|
37
|
+
"msgpackr": "^2.0.5",
|
|
38
|
+
"@astrale-os/kernel-core": "0.9.0-beta.14",
|
|
39
|
+
"@astrale-os/kernel-dsl": "0.2.0-beta.11",
|
|
40
|
+
"@astrale-os/kernel-protocol": "0.5.0-beta.14"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@astrale/typescript-config": "npm:@jsr/astrale__typescript-config@~1.1.0",
|