@astrale-os/kernel-server 0.5.0-beta.8 → 1.0.0-beta.1

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.
Files changed (241) hide show
  1. package/dist/capture.d.ts +0 -1
  2. package/dist/capture.js +0 -1
  3. package/dist/cors/cors.d.ts +0 -1
  4. package/dist/cors/cors.js +11 -4
  5. package/dist/cors/index.d.ts +0 -1
  6. package/dist/cors/index.js +0 -1
  7. package/dist/cors/types.d.ts +0 -1
  8. package/dist/cors/types.js +0 -1
  9. package/dist/errors/admission.d.ts +7 -0
  10. package/dist/errors/admission.js +22 -0
  11. package/dist/errors/availability.d.ts +0 -1
  12. package/dist/errors/availability.js +0 -1
  13. package/dist/errors/classify.d.ts +8 -1
  14. package/dist/errors/classify.js +15 -1
  15. package/dist/errors/index.d.ts +2 -2
  16. package/dist/errors/index.js +2 -2
  17. package/dist/errors/request-body.d.ts +0 -1
  18. package/dist/errors/request-body.js +0 -1
  19. package/dist/errors/response-body.d.ts +0 -1
  20. package/dist/errors/response-body.js +0 -1
  21. package/dist/index.d.ts +3 -4
  22. package/dist/index.js +2 -3
  23. package/dist/invocation/context.d.ts +3 -3
  24. package/dist/invocation/context.js +2 -2
  25. package/dist/invocation/identity.d.ts +1 -2
  26. package/dist/invocation/identity.js +1 -2
  27. package/dist/invocation/index.d.ts +1 -2
  28. package/dist/invocation/index.js +1 -2
  29. package/dist/invocation/release-route.d.ts +22 -0
  30. package/dist/invocation/release-route.js +32 -0
  31. package/dist/invocation/types.d.ts +8 -7
  32. package/dist/invocation/types.js +0 -1
  33. package/dist/issuer/exchange.d.ts +0 -1
  34. package/dist/issuer/exchange.js +20 -10
  35. package/dist/issuer/index.d.ts +0 -1
  36. package/dist/issuer/index.js +0 -1
  37. package/dist/issuer/routes.d.ts +0 -1
  38. package/dist/issuer/routes.js +57 -48
  39. package/dist/issuer/types.d.ts +5 -1
  40. package/dist/issuer/types.js +0 -1
  41. package/dist/lifecycle/active.d.ts +4 -1
  42. package/dist/lifecycle/active.js +48 -11
  43. package/dist/lifecycle/index.d.ts +0 -1
  44. package/dist/lifecycle/index.js +0 -1
  45. package/dist/release/delivery/handler.d.ts +9 -0
  46. package/dist/{publication → release}/delivery/handler.js +19 -13
  47. package/dist/release/delivery/index.d.ts +1 -0
  48. package/dist/release/delivery/index.js +1 -0
  49. package/dist/{publication → release}/delivery/types.d.ts +4 -5
  50. package/dist/release/delivery/types.js +1 -0
  51. package/dist/release/index.d.ts +3 -0
  52. package/dist/release/index.js +2 -0
  53. package/dist/release/served.d.ts +21 -0
  54. package/dist/release/served.js +10 -0
  55. package/dist/release/snapshot.d.ts +22 -0
  56. package/dist/release/snapshot.js +49 -0
  57. package/dist/release/table.d.ts +4 -0
  58. package/dist/release/table.js +11 -0
  59. package/dist/release/types.d.ts +23 -0
  60. package/dist/release/types.js +1 -0
  61. package/dist/routes/address.d.ts +2 -0
  62. package/dist/routes/address.js +6 -0
  63. package/dist/routes/binary.d.ts +9 -0
  64. package/dist/routes/binary.js +110 -0
  65. package/dist/routes/bundle.d.ts +5 -0
  66. package/dist/routes/bundle.js +50 -0
  67. package/dist/routes/csrf.d.ts +16 -0
  68. package/dist/routes/csrf.js +35 -0
  69. package/dist/routes/document.d.ts +6 -0
  70. package/dist/routes/document.js +57 -0
  71. package/dist/routes/index.d.ts +3 -2
  72. package/dist/routes/index.js +2 -2
  73. package/dist/routes/model.d.ts +15 -0
  74. package/dist/routes/model.js +1 -0
  75. package/dist/routes/native.d.ts +17 -0
  76. package/dist/routes/native.js +126 -0
  77. package/dist/routes/preflight.d.ts +4 -0
  78. package/dist/routes/preflight.js +55 -0
  79. package/dist/routes/response.d.ts +14 -0
  80. package/dist/routes/response.js +33 -0
  81. package/dist/routes/routes.d.ts +7 -24
  82. package/dist/routes/routes.js +24 -439
  83. package/dist/routes/snapshot.d.ts +13 -0
  84. package/dist/routes/snapshot.js +39 -0
  85. package/dist/routes/stream.d.ts +6 -0
  86. package/dist/routes/stream.js +122 -0
  87. package/dist/server.d.ts +0 -1
  88. package/dist/server.js +95 -51
  89. package/dist/transport/body.d.ts +0 -1
  90. package/dist/transport/body.js +0 -1
  91. package/dist/transport/codecs.d.ts +2 -3
  92. package/dist/transport/codecs.js +4 -4
  93. package/dist/transport/credential.d.ts +0 -1
  94. package/dist/transport/credential.js +0 -1
  95. package/dist/transport/http/binary-request.d.ts +11 -0
  96. package/dist/transport/http/binary-request.js +102 -0
  97. package/dist/transport/http/binary.d.ts +6 -0
  98. package/dist/transport/http/binary.js +35 -0
  99. package/dist/transport/http/byte-stream.d.ts +4 -0
  100. package/dist/transport/http/byte-stream.js +123 -0
  101. package/dist/transport/http/control.d.ts +12 -0
  102. package/dist/transport/http/control.js +24 -0
  103. package/dist/transport/http/handler.d.ts +8 -3
  104. package/dist/transport/http/handler.js +97 -45
  105. package/dist/transport/http/index.d.ts +3 -2
  106. package/dist/transport/http/index.js +2 -2
  107. package/dist/transport/http/model.d.ts +20 -0
  108. package/dist/transport/http/model.js +1 -0
  109. package/dist/transport/http/render.d.ts +3 -31
  110. package/dist/transport/http/render.js +18 -269
  111. package/dist/transport/http/response.d.ts +14 -0
  112. package/dist/transport/http/response.js +77 -0
  113. package/dist/transport/http/stream.d.ts +5 -0
  114. package/dist/transport/http/stream.js +134 -0
  115. package/dist/transport/index.d.ts +2 -2
  116. package/dist/transport/index.js +2 -2
  117. package/dist/transport/result.d.ts +13 -0
  118. package/dist/transport/result.js +35 -0
  119. package/dist/transport/router.d.ts +0 -1
  120. package/dist/transport/router.js +0 -1
  121. package/dist/transport/stream-error.d.ts +0 -1
  122. package/dist/transport/stream-error.js +0 -1
  123. package/dist/transport/types.d.ts +1 -2
  124. package/dist/transport/types.js +0 -1
  125. package/dist/transport/websocket/index.d.ts +0 -1
  126. package/dist/transport/websocket/index.js +0 -1
  127. package/dist/transport/websocket/session.d.ts +3 -3
  128. package/dist/transport/websocket/session.js +30 -30
  129. package/dist/transport/websocket/upgrade.d.ts +2 -3
  130. package/dist/transport/websocket/upgrade.js +7 -7
  131. package/dist/types.d.ts +33 -10
  132. package/dist/types.js +0 -1
  133. package/package.json +11 -22
  134. package/dist/capture.d.ts.map +0 -1
  135. package/dist/capture.js.map +0 -1
  136. package/dist/cors/cors.d.ts.map +0 -1
  137. package/dist/cors/cors.js.map +0 -1
  138. package/dist/cors/index.d.ts.map +0 -1
  139. package/dist/cors/index.js.map +0 -1
  140. package/dist/cors/types.d.ts.map +0 -1
  141. package/dist/cors/types.js.map +0 -1
  142. package/dist/errors/availability.d.ts.map +0 -1
  143. package/dist/errors/availability.js.map +0 -1
  144. package/dist/errors/classify.d.ts.map +0 -1
  145. package/dist/errors/classify.js.map +0 -1
  146. package/dist/errors/index.d.ts.map +0 -1
  147. package/dist/errors/index.js.map +0 -1
  148. package/dist/errors/request-body.d.ts.map +0 -1
  149. package/dist/errors/request-body.js.map +0 -1
  150. package/dist/errors/response-body.d.ts.map +0 -1
  151. package/dist/errors/response-body.js.map +0 -1
  152. package/dist/index.d.ts.map +0 -1
  153. package/dist/index.js.map +0 -1
  154. package/dist/invocation/context.d.ts.map +0 -1
  155. package/dist/invocation/context.js.map +0 -1
  156. package/dist/invocation/identity.d.ts.map +0 -1
  157. package/dist/invocation/identity.js.map +0 -1
  158. package/dist/invocation/index.d.ts.map +0 -1
  159. package/dist/invocation/index.js.map +0 -1
  160. package/dist/invocation/publication-route.d.ts +0 -14
  161. package/dist/invocation/publication-route.d.ts.map +0 -1
  162. package/dist/invocation/publication-route.js +0 -45
  163. package/dist/invocation/publication-route.js.map +0 -1
  164. package/dist/invocation/types.d.ts.map +0 -1
  165. package/dist/invocation/types.js.map +0 -1
  166. package/dist/issuer/exchange.d.ts.map +0 -1
  167. package/dist/issuer/exchange.js.map +0 -1
  168. package/dist/issuer/index.d.ts.map +0 -1
  169. package/dist/issuer/index.js.map +0 -1
  170. package/dist/issuer/routes.d.ts.map +0 -1
  171. package/dist/issuer/routes.js.map +0 -1
  172. package/dist/issuer/types.d.ts.map +0 -1
  173. package/dist/issuer/types.js.map +0 -1
  174. package/dist/lifecycle/active.d.ts.map +0 -1
  175. package/dist/lifecycle/active.js.map +0 -1
  176. package/dist/lifecycle/index.d.ts.map +0 -1
  177. package/dist/lifecycle/index.js.map +0 -1
  178. package/dist/publication/delivery/handler.d.ts +0 -10
  179. package/dist/publication/delivery/handler.d.ts.map +0 -1
  180. package/dist/publication/delivery/handler.js.map +0 -1
  181. package/dist/publication/delivery/index.d.ts +0 -3
  182. package/dist/publication/delivery/index.d.ts.map +0 -1
  183. package/dist/publication/delivery/index.js +0 -2
  184. package/dist/publication/delivery/index.js.map +0 -1
  185. package/dist/publication/delivery/types.d.ts.map +0 -1
  186. package/dist/publication/delivery/types.js +0 -2
  187. package/dist/publication/delivery/types.js.map +0 -1
  188. package/dist/publication/index.d.ts +0 -4
  189. package/dist/publication/index.d.ts.map +0 -1
  190. package/dist/publication/index.js +0 -3
  191. package/dist/publication/index.js.map +0 -1
  192. package/dist/publication/snapshot.d.ts +0 -16
  193. package/dist/publication/snapshot.d.ts.map +0 -1
  194. package/dist/publication/snapshot.js +0 -39
  195. package/dist/publication/snapshot.js.map +0 -1
  196. package/dist/publication/table.d.ts +0 -4
  197. package/dist/publication/table.d.ts.map +0 -1
  198. package/dist/publication/table.js +0 -17
  199. package/dist/publication/table.js.map +0 -1
  200. package/dist/publication/types.d.ts +0 -23
  201. package/dist/publication/types.d.ts.map +0 -1
  202. package/dist/publication/types.js +0 -2
  203. package/dist/publication/types.js.map +0 -1
  204. package/dist/publication/validator.d.ts +0 -4
  205. package/dist/publication/validator.d.ts.map +0 -1
  206. package/dist/publication/validator.js +0 -17
  207. package/dist/publication/validator.js.map +0 -1
  208. package/dist/routes/index.d.ts.map +0 -1
  209. package/dist/routes/index.js.map +0 -1
  210. package/dist/routes/routes.d.ts.map +0 -1
  211. package/dist/routes/routes.js.map +0 -1
  212. package/dist/server.d.ts.map +0 -1
  213. package/dist/server.js.map +0 -1
  214. package/dist/transport/body.d.ts.map +0 -1
  215. package/dist/transport/body.js.map +0 -1
  216. package/dist/transport/codecs.d.ts.map +0 -1
  217. package/dist/transport/codecs.js.map +0 -1
  218. package/dist/transport/credential.d.ts.map +0 -1
  219. package/dist/transport/credential.js.map +0 -1
  220. package/dist/transport/http/handler.d.ts.map +0 -1
  221. package/dist/transport/http/handler.js.map +0 -1
  222. package/dist/transport/http/index.d.ts.map +0 -1
  223. package/dist/transport/http/index.js.map +0 -1
  224. package/dist/transport/http/render.d.ts.map +0 -1
  225. package/dist/transport/http/render.js.map +0 -1
  226. package/dist/transport/index.d.ts.map +0 -1
  227. package/dist/transport/index.js.map +0 -1
  228. package/dist/transport/router.d.ts.map +0 -1
  229. package/dist/transport/router.js.map +0 -1
  230. package/dist/transport/stream-error.d.ts.map +0 -1
  231. package/dist/transport/stream-error.js.map +0 -1
  232. package/dist/transport/types.d.ts.map +0 -1
  233. package/dist/transport/types.js.map +0 -1
  234. package/dist/transport/websocket/index.d.ts.map +0 -1
  235. package/dist/transport/websocket/index.js.map +0 -1
  236. package/dist/transport/websocket/session.d.ts.map +0 -1
  237. package/dist/transport/websocket/session.js.map +0 -1
  238. package/dist/transport/websocket/upgrade.d.ts.map +0 -1
  239. package/dist/transport/websocket/upgrade.js.map +0 -1
  240. package/dist/types.d.ts.map +0 -1
  241. package/dist/types.js.map +0 -1
@@ -1,18 +1,23 @@
1
1
  import type { CorsPolicy } from '../../cors/index.js';
2
2
  import type { NextInvocationId } from '../../invocation/index.js';
3
3
  import type { ActiveOperations } from '../../lifecycle/index.js';
4
- import type { PublicationSnapshot } from '../../publication/index.js';
4
+ import type { RequestServing } from '../../release/served.js';
5
5
  import { type ApplicationHost, type Handler } from '../../invocation/index.js';
6
6
  export interface HttpHandlerConfig {
7
7
  readonly handler: Handler;
8
8
  readonly active: ActiveOperations;
9
9
  readonly invocation: NextInvocationId;
10
10
  readonly maximumRequestBytes: number;
11
+ readonly maximumBinaryRequestBytes: number;
11
12
  readonly maximumResponseBytes: number;
12
13
  readonly invocationTimeoutMs: number;
14
+ readonly binaryRequestTimeoutMs: number;
15
+ readonly binaryRequestIdleTimeoutMs?: number;
16
+ readonly binaryDownloadTarget?: string;
17
+ readonly binaryDownloadTimeoutMs?: number;
13
18
  readonly cors: CorsPolicy;
14
19
  readonly websocket: boolean;
15
- readonly currentPublication?: (request: Request) => PublicationSnapshot | undefined;
20
+ /** Present exactly when the Server serves a Release. */
21
+ readonly serving?: (request: Request) => RequestServing;
16
22
  }
17
23
  export declare function createHttpHandler(config: HttpHandlerConfig): (request: Request, host?: ApplicationHost) => Promise<Response>;
18
- //# sourceMappingURL=handler.d.ts.map
@@ -1,12 +1,16 @@
1
1
  import * as codecs from '@astrale-os/kernel-protocol/codecs';
2
2
  import * as errors from '@astrale-os/kernel-protocol/errors';
3
3
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import { classifyError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
5
- import { invocationContext, publicationRouteDecision, } from '../../invocation/index.js';
4
+ import { admissionRefusal, classifyError, classifyRequestError, RequestBodyInvalidError, RequestBodyTooLargeError, } from '../../errors/index.js';
5
+ import { admitVia, invocationContext, } from '../../invocation/index.js';
6
+ import { pinned } from '../../release/served.js';
6
7
  import { readBoundedBody } from '../body.js';
7
8
  import { loadCodecs } from '../codecs.js';
8
9
  import { resolveCredential } from '../credential.js';
9
- import { renderHttpError, renderHttpResult } from './render.js';
10
+ import { isStreamingBinary } from '../result.js';
11
+ import { binaryRequestBody } from './binary-request.js';
12
+ import { renderDeploymentRetired, renderHttpError } from './control.js';
13
+ import { renderHttpResult } from './render.js';
10
14
  export function createHttpHandler(config) {
11
15
  return async (request, host) => {
12
16
  const cors = (exposedHeaders) => config.cors.response(request, exposedHeaders);
@@ -21,6 +25,8 @@ export function createHttpHandler(config) {
21
25
  headers: [
22
26
  'authorization',
23
27
  'content-type',
28
+ protocol.http.REQUEST_ENVELOPE_FIELD,
29
+ protocol.http.BINARY_LENGTH_FIELD,
24
30
  protocol.http.TRACEPARENT_FIELD,
25
31
  protocol.http.TRACESTATE_FIELD,
26
32
  ],
@@ -37,24 +43,34 @@ export function createHttpHandler(config) {
37
43
  ...options,
38
44
  maximumResponseBytes: config.maximumResponseBytes,
39
45
  });
40
- const active = config.active.begin(config.invocationTimeoutMs, request.signal);
46
+ const binaryRequest = request.headers.get('content-type')?.trim().toLowerCase() ===
47
+ protocol.http.BINARY_REQUEST_CONTENT_TYPE;
48
+ const active = config.active.begin(binaryRequest ? config.binaryRequestTimeoutMs : config.invocationTimeoutMs, request.signal);
41
49
  if (active === undefined) {
42
- return renderError(null, {
43
- code: errors.CODES.BACKEND_UNAVAILABLE,
44
- message: 'server is unavailable.',
45
- }, codecs.json, { status: 503, negotiated: false });
50
+ if (binaryRequest && request.body !== null && !request.body.locked) {
51
+ void request.body.cancel('binary request rejected').catch(() => undefined);
52
+ }
53
+ return renderError(null, admissionRefusal(config.active.isAccepting), codecs.json, {
54
+ status: 503,
55
+ negotiated: false,
56
+ });
46
57
  }
47
58
  let handedOff = false;
59
+ let binary;
48
60
  try {
49
- const supported = await loadCodecs();
61
+ const supported = loadCodecs();
50
62
  const contentType = request.headers.get('content-type');
51
- const incoming = contentType === null || contentType.trim() === ''
63
+ // No default representation: a body without a declared Astrale type is never decoded, so a
64
+ // browser cannot send an admissible invocation from another origin without a CORS preflight.
65
+ const incoming = binaryRequest
52
66
  ? codecs.json
53
67
  : protocol.http.codecForContentType(contentType, supported);
54
68
  if (incoming === undefined) {
55
69
  return renderError(null, {
56
70
  code: errors.CODES.UNSUPPORTED,
57
- message: 'Unsupported request Content-Type.',
71
+ message: contentType === null || contentType.trim() === ''
72
+ ? 'Missing request Content-Type.'
73
+ : 'Unsupported request Content-Type.',
58
74
  }, codecs.json, { status: 415, negotiated: false });
59
75
  }
60
76
  const outgoing = protocol.http.negotiateResponseCodec(request.headers.get('accept'), incoming, supported);
@@ -66,7 +82,15 @@ export function createHttpHandler(config) {
66
82
  }
67
83
  let body;
68
84
  try {
69
- body = await readBoundedBody(request, config.maximumRequestBytes, active.signal);
85
+ if (binaryRequest)
86
+ binary = binaryRequestBody(request, config.maximumBinaryRequestBytes, active.signal, config.binaryRequestIdleTimeoutMs === undefined
87
+ ? undefined
88
+ : {
89
+ timeoutMs: config.binaryRequestIdleTimeoutMs,
90
+ cancel: () => active.cancel('binary request idle timeout exceeded'),
91
+ });
92
+ else
93
+ body = await readBoundedBody(request, config.maximumRequestBytes, active.signal);
70
94
  }
71
95
  catch (input) {
72
96
  const payload = input instanceof RequestBodyTooLargeError
@@ -87,37 +111,54 @@ export function createHttpHandler(config) {
87
111
  status: input instanceof RequestBodyTooLargeError ? 413 : errors.httpStatusOf(payload),
88
112
  });
89
113
  }
90
- let decoded;
91
- try {
92
- decoded = incoming.decode(body);
93
- }
94
- catch (input) {
95
- if (!(input instanceof codecs.DecodeError))
96
- throw input;
97
- return renderError(null, { code: errors.CODES.PARSE_ERROR, message: 'Request body is invalid.' }, outgoing);
98
- }
99
114
  let envelope;
100
115
  try {
101
- envelope = protocol.decodeRequest(decoded);
116
+ if (binaryRequest) {
117
+ envelope = protocol.http.decodeBinaryRequestEnvelope(request.headers.get(protocol.http.REQUEST_ENVELOPE_FIELD));
118
+ if (envelope.idempotencyKey !== undefined) {
119
+ return renderError(envelope.requestId, {
120
+ code: errors.CODES.INVALID_REQUEST,
121
+ message: 'Binary requests forbid idempotencyKey.',
122
+ }, outgoing);
123
+ }
124
+ }
125
+ else {
126
+ let decoded;
127
+ try {
128
+ decoded = incoming.decode(body);
129
+ }
130
+ catch (input) {
131
+ if (!(input instanceof codecs.DecodeError))
132
+ throw input;
133
+ return renderError(null, { code: errors.CODES.PARSE_ERROR, message: 'Request body is invalid.' }, outgoing);
134
+ }
135
+ envelope = protocol.decodeRequest(decoded);
136
+ }
102
137
  }
103
138
  catch (input) {
104
139
  return renderError(null, classifyError(input), outgoing);
105
140
  }
106
- const publication = config.currentPublication?.(request);
107
- if (config.currentPublication !== undefined && publication === undefined) {
141
+ if (!binaryRequest &&
142
+ config.binaryDownloadTarget !== undefined &&
143
+ protocol.targetPath(envelope.call.target) === config.binaryDownloadTarget) {
144
+ active.extend(config.binaryDownloadTimeoutMs);
145
+ }
146
+ if (active.signal.aborted) {
147
+ return renderError(envelope.requestId, classifyRequestError(active.signal.reason, active), outgoing);
148
+ }
149
+ const serving = config.serving?.(request);
150
+ const served = serving?.current();
151
+ if (serving !== undefined && served === undefined) {
108
152
  return renderError(envelope.requestId, {
109
153
  code: errors.CODES.BACKEND_UNAVAILABLE,
110
- message: 'Publication is unavailable.',
154
+ message: 'Release is unavailable.',
111
155
  }, outgoing, { status: 503 });
112
156
  }
113
157
  if (envelope.routing?.kind === 'via') {
114
- if (publication === undefined) {
115
- return renderError(envelope.requestId, {
116
- code: errors.CODES.INVALID_REQUEST,
117
- message: 'Publication routing requires a Publication-backed destination.',
118
- }, outgoing);
158
+ const decision = admitVia(served, envelope.routing);
159
+ if (decision.kind === 'retired') {
160
+ return renderDeploymentRetired(serving.deployment(), decision.release, cors);
119
161
  }
120
- const decision = publicationRouteDecision(publication.publication, envelope.routing.publication);
121
162
  if (decision.kind === 'rejected') {
122
163
  return renderError(envelope.requestId, decision.error, outgoing);
123
164
  }
@@ -135,9 +176,20 @@ export function createHttpHandler(config) {
135
176
  ...(envelope.delegate === undefined ? {} : { delegate: envelope.delegate }),
136
177
  ...(envelope.routing?.kind === 'via' ? { via: envelope.routing } : {}),
137
178
  ...(envelope.schema === undefined ? {} : { schema: envelope.schema }),
138
- ...(publication === undefined ? {} : { publication }),
139
- context: invocationContext(assigned, active, config.active, host, undefined, envelope.idempotencyKey),
179
+ ...(served === undefined ? {} : pinned(served)),
180
+ context: invocationContext(assigned, active, config.active, host, undefined, envelope.idempotencyKey, binary),
140
181
  }));
182
+ if (binary !== undefined && result.kind !== 'redirect' && result.kind !== 'miss') {
183
+ if (!binary.completed()) {
184
+ if (result.kind === 'stream') {
185
+ await result.stream.cancel('Binary request body was not fully consumed.');
186
+ }
187
+ else if (result.kind === 'binary' && isStreamingBinary(result.value)) {
188
+ await result.value.body[Symbol.asyncIterator]().return?.();
189
+ }
190
+ throw new Error('Binary Function returned before consuming its request body.');
191
+ }
192
+ }
141
193
  const response = renderHttpResult(result, {
142
194
  requestId: envelope.requestId,
143
195
  codec: outgoing,
@@ -146,20 +198,12 @@ export function createHttpHandler(config) {
146
198
  active,
147
199
  maximumResponseBytes: config.maximumResponseBytes,
148
200
  });
149
- handedOff = result.kind === 'stream';
201
+ handedOff =
202
+ result.kind === 'stream' || (result.kind === 'binary' && isStreamingBinary(result.value));
150
203
  return response;
151
204
  }
152
205
  catch (input) {
153
- if (active.timedOut) {
154
- return renderError(envelope.requestId, {
155
- code: errors.CODES.DEADLINE_EXCEEDED,
156
- message: 'Invocation deadline exceeded.',
157
- }, outgoing, { status: 504 });
158
- }
159
- if (active.signal.aborted) {
160
- return renderError(envelope.requestId, { code: errors.CODES.CANCELLED, message: 'Invocation was cancelled.' }, outgoing, { status: 408 });
161
- }
162
- return renderError(envelope.requestId, classifyError(input), outgoing);
206
+ return renderError(envelope.requestId, classifyRequestError(input, active), outgoing);
163
207
  }
164
208
  }
165
209
  catch (input) {
@@ -168,6 +212,15 @@ export function createHttpHandler(config) {
168
212
  });
169
213
  }
170
214
  finally {
215
+ if (binary !== undefined) {
216
+ binary.cancel('binary invocation completed');
217
+ }
218
+ else if (binaryRequest &&
219
+ binary === undefined &&
220
+ request.body !== null &&
221
+ !request.body.locked) {
222
+ void request.body.cancel('binary request rejected').catch(() => undefined);
223
+ }
171
224
  if (!handedOff)
172
225
  active.settle();
173
226
  }
@@ -178,4 +231,3 @@ function noStore(response) {
178
231
  headers.set('cache-control', 'no-store');
179
232
  return new Response(response.body, { status: response.status, headers });
180
233
  }
181
- //# sourceMappingURL=handler.js.map
@@ -1,3 +1,4 @@
1
1
  export { createHttpHandler, type HttpHandlerConfig } from './handler.js';
2
- export { encodeEnvelope, renderBinary, renderHttpError, renderHttpResult, renderHttpStale, renderRedirect, type CorsHeaders, type ErrorRenderOptions, type RenderContext, } from './render.js';
3
- //# sourceMappingURL=index.d.ts.map
2
+ export { renderHttpResult } from './render.js';
3
+ export { renderHttpError, renderHttpStale } from './control.js';
4
+ export type { CorsHeaders, ErrorRenderOptions, RenderContext } from './model.js';
@@ -1,3 +1,3 @@
1
1
  export { createHttpHandler } from './handler.js';
2
- export { encodeEnvelope, renderBinary, renderHttpError, renderHttpResult, renderHttpStale, renderRedirect, } from './render.js';
3
- //# sourceMappingURL=index.js.map
2
+ export { renderHttpResult } from './render.js';
3
+ export { renderHttpError, renderHttpStale } from './control.js';
@@ -0,0 +1,20 @@
1
+ import type { Codec } from '@astrale-os/kernel-protocol/codecs';
2
+ import type * as protocol from '@astrale-os/kernel-protocol/invocation';
3
+ import type { ActiveOperation } from '../../lifecycle/index.js';
4
+ /** Correlation, binding and active lifetime retained while presenting one result. */
5
+ export interface RenderContext {
6
+ readonly requestId: protocol.RequestId;
7
+ readonly codec: Codec;
8
+ readonly cors: CorsHeaders;
9
+ readonly invocation: protocol.InvocationId;
10
+ readonly active: ActiveOperation;
11
+ readonly maximumResponseBytes: number;
12
+ }
13
+ /** Derive response headers with the transport fields exposed to the browser. */
14
+ export type CorsHeaders = (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>;
15
+ /** Bound and negotiate an error using the admitted request presentation. */
16
+ export interface ErrorRenderOptions {
17
+ readonly status?: number;
18
+ readonly negotiated?: boolean;
19
+ readonly maximumResponseBytes?: number;
20
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,32 +1,4 @@
1
- import type { Codec } from '@astrale-os/kernel-protocol/codecs';
2
- import * as errors from '@astrale-os/kernel-protocol/errors';
3
- import * as protocol from '@astrale-os/kernel-protocol/invocation';
4
- import type { Handler } from '../../invocation/index.js';
5
- import type { ActiveOperation } from '../../lifecycle/index.js';
6
- export interface RenderContext {
7
- readonly requestId: protocol.RequestId;
8
- readonly codec: Codec;
9
- readonly cors: CorsHeaders;
10
- readonly invocation: protocol.InvocationId;
11
- readonly active: ActiveOperation;
12
- readonly maximumResponseBytes: number;
13
- }
14
- export type CorsHeaders = (exposedHeaders?: readonly string[]) => Readonly<Record<string, string>>;
15
- export interface ErrorRenderOptions {
16
- readonly status?: number;
17
- readonly negotiated?: boolean;
18
- readonly maximumResponseBytes?: number;
19
- }
20
- type HandlerResult = Awaited<ReturnType<Handler['dispatch']>>;
1
+ import type { HandlerResult } from '../../invocation/index.js';
2
+ import type { RenderContext } from './model.js';
3
+ /** Render one admitted Handler result and retain only live producers in the active operation. */
21
4
  export declare function renderHttpResult(result: HandlerResult, context: RenderContext): Response;
22
- export declare function renderBinary(requestId: protocol.RequestId, invocation: protocol.InvocationId, binary: Extract<HandlerResult, {
23
- readonly kind: 'binary';
24
- }>['value']): protocol.BinaryResponse;
25
- export declare function renderRedirect(requestId: protocol.RequestId, invocation: protocol.InvocationId, redirect: Extract<HandlerResult, {
26
- readonly kind: 'redirect';
27
- }>['redirect']): protocol.RedirectResponse;
28
- export declare function renderHttpError(requestId: protocol.RequestId, invocation: protocol.InvocationId, error: errors.ErrorPayload, codec: Codec, cors: CorsHeaders, options?: ErrorRenderOptions): Response;
29
- export declare function renderHttpStale(requestId: protocol.RequestId, invocation: protocol.InvocationId, current: protocol.Stale['current'], codec: Codec, cors: CorsHeaders, maximumResponseBytes?: number): Response;
30
- export declare function encodeEnvelope(codec: Codec, input: unknown): string | Uint8Array;
31
- export {};
32
- //# sourceMappingURL=render.d.ts.map
@@ -1,289 +1,38 @@
1
- import * as errors from '@astrale-os/kernel-protocol/errors';
2
1
  import * as protocol from '@astrale-os/kernel-protocol/invocation';
3
- import { HTTP_ROUTE_MISS, HTTP_ROUTE_STALE } from '@astrale-os/kernel-protocol/routes/routing';
4
- import { classifyError } from '../../errors/index.js';
5
- import { ResponseBodyTooLargeError } from '../../errors/index.js';
6
- import { interruptedStreamError } from '../stream-error.js';
2
+ import { isStreamingBinary, renderBinary, renderRedirect } from '../result.js';
3
+ import { binaryHttpResponse } from './binary.js';
4
+ import { streamingBinaryHttpResponse } from './byte-stream.js';
5
+ import { envelopeResponse } from './response.js';
6
+ import { streamHttpResponse } from './stream.js';
7
+ /** Render one admitted Handler result and retain only live producers in the active operation. */
7
8
  export function renderHttpResult(result, context) {
9
+ let envelope;
8
10
  switch (result.kind) {
9
11
  case 'value':
10
- return settled(context.active, envelopeResponse(context.codec, {
12
+ envelope = {
11
13
  result: result.value,
12
14
  requestId: context.requestId,
13
15
  invocation: context.invocation,
14
- }, context.cors(), 200, true, context.maximumResponseBytes));
15
- case 'redirect': {
16
- const response = renderRedirect(context.requestId, context.invocation, result.redirect);
17
- return settled(context.active, envelopeResponse(context.codec, response, context.cors(), 200, true, context.maximumResponseBytes));
18
- }
16
+ };
17
+ break;
18
+ case 'redirect':
19
+ envelope = renderRedirect(context.requestId, context.invocation, result.redirect);
20
+ break;
19
21
  case 'miss':
20
- return settled(context.active, envelopeResponse(context.codec, protocol.miss(context.requestId, context.invocation), context.cors(), 200, true, context.maximumResponseBytes));
22
+ envelope = protocol.miss(context.requestId, context.invocation);
23
+ break;
21
24
  case 'binary': {
25
+ if (isStreamingBinary(result.value))
26
+ return streamingBinaryHttpResponse(result.value, context);
22
27
  const binary = renderBinary(context.requestId, context.invocation, result.value);
23
28
  return settled(context.active, binaryHttpResponse(binary, context.cors, context.maximumResponseBytes));
24
29
  }
25
30
  case 'stream':
26
31
  return streamHttpResponse(result.stream, context);
27
32
  }
28
- }
29
- export function renderBinary(requestId, invocation, binary) {
30
- const reserved = Object.keys(binary.headers ?? {}).find((name) => {
31
- const normalized = name.toLowerCase();
32
- return (normalized === protocol.http.INVOCATION_FIELD ||
33
- normalized === HTTP_ROUTE_STALE ||
34
- normalized === HTTP_ROUTE_MISS);
35
- });
36
- if (reserved !== undefined)
37
- throw new TypeError(`${reserved} is reserved by Protocol.`);
38
- return protocol.decodeBinaryResponse({
39
- kind: 'binary',
40
- requestId,
41
- invocation,
42
- body: binary.body,
43
- mediaType: binary.mediaType,
44
- ...(binary.headers === undefined ? {} : { headers: binary.headers }),
45
- });
46
- }
47
- export function renderRedirect(requestId, invocation, redirect) {
48
- return protocol.decodeRedirectResponse({
49
- kind: 'redirect',
50
- requestId,
51
- invocation,
52
- redirect,
53
- });
54
- }
55
- export function renderHttpError(requestId, invocation, error, codec, cors, options = {}) {
56
- const status = options.status ?? errors.httpStatusOf(error);
57
- const extra = status === 401 ? { 'www-authenticate': 'Bearer realm="astrale-invocation"' } : {};
58
- return errorEnvelopeResponse(codec, { error, requestId, invocation }, { ...cors(), ...extra }, status, options.negotiated ?? true, options.maximumResponseBytes);
59
- }
60
- export function renderHttpStale(requestId, invocation, current, codec, cors, maximumResponseBytes) {
61
- return envelopeResponse(codec, protocol.stale(requestId, invocation, current), cors(), 200, true, maximumResponseBytes);
62
- }
63
- export function encodeEnvelope(codec, input) {
64
- return codec.encode(input);
65
- }
66
- function envelopeResponse(codec, input, cors, status = 200, negotiated = true, maximumResponseBytes) {
67
- const encoded = codec.encode(input);
68
- requireResponseBound(encoded, maximumResponseBytes);
69
- const headers = bindingHeaders(cors, negotiated);
70
- headers.set('content-type', codec.contentType);
71
- return new Response(toBody(encoded), {
72
- status,
73
- headers,
74
- });
75
- }
76
- function binaryHttpResponse(binary, cors, maximumResponseBytes) {
77
- requireResponseBound(binary.body, maximumResponseBytes);
78
- const applicationHeaders = Object.keys(binary.headers ?? {}).filter((name) => protocol.http.isApplicationBinaryHeader(name));
79
- const headers = new Headers(cors([
80
- protocol.http.REQUEST_ID_FIELD,
81
- protocol.http.INVOCATION_FIELD,
82
- protocol.http.BINARY_HEADERS_FIELD,
83
- ...applicationHeaders,
84
- ]));
85
- for (const [name, value] of Object.entries(binary.headers ?? {})) {
86
- if (protocol.http.isApplicationBinaryHeader(name))
87
- headers.set(name, value);
88
- }
89
- headers.set('content-type', binary.mediaType);
90
- headers.set(protocol.http.BINARY_HEADERS_FIELD, protocol.http.encodeBinaryHeaderNames(binary.headers));
91
- headers.set(protocol.http.REQUEST_ID_FIELD, String(binary.requestId));
92
- headers.set(protocol.http.INVOCATION_FIELD, protocol.http.encodeInvocationId(binary.invocation));
93
- headers.set('cache-control', 'no-store');
94
- appendVary(headers, 'accept');
95
- return new Response(toBody(binary.body), { status: 200, headers });
96
- }
97
- function streamHttpResponse(stream, context) {
98
- const iterator = stream[Symbol.asyncIterator]();
99
- const startFrame = encodeBoundedStreamFrame(context.codec, { kind: 'stream.start', requestId: context.requestId, invocation: context.invocation }, context.maximumResponseBytes);
100
- let count = 0;
101
- let terminal = false;
102
- let controller;
103
- let stopping;
104
- const stop = (reason) => {
105
- stopping ??= stopStream(reason);
106
- return stopping;
107
- };
108
- async function stopStream(reason) {
109
- if (terminal)
110
- return;
111
- terminal = true;
112
- // End the wire response immediately. Producer cleanup remains part of the active operation,
113
- // but an uncooperative producer must not leave the client waiting for a terminal frame.
114
- if (controller !== undefined) {
115
- try {
116
- controller.enqueue(encodeBoundedStreamFrame(context.codec, cancellationFrame(context), context.maximumResponseBytes));
117
- }
118
- catch {
119
- // The transport consumer may already have disconnected.
120
- }
121
- try {
122
- controller.close();
123
- }
124
- catch {
125
- // The stream may already be closed or cancelled by the consumer.
126
- }
127
- }
128
- try {
129
- await Promise.allSettled([
130
- Promise.resolve().then(() => stream.cancel(reason)),
131
- Promise.resolve().then(() => iterator.return?.()),
132
- ]);
133
- }
134
- finally {
135
- context.active.settle();
136
- }
137
- }
138
- const body = new ReadableStream({
139
- start(output) {
140
- controller = output;
141
- output.enqueue(startFrame);
142
- },
143
- async pull(output) {
144
- if (terminal)
145
- return;
146
- try {
147
- const next = await iterator.next();
148
- if (terminal)
149
- return;
150
- if (next.done) {
151
- terminal = true;
152
- output.enqueue(encodeBoundedStreamFrame(context.codec, {
153
- kind: 'stream.end',
154
- requestId: context.requestId,
155
- count,
156
- }, context.maximumResponseBytes));
157
- output.close();
158
- context.active.settle();
159
- return;
160
- }
161
- const frame = protocol.decodeStreamFrame({
162
- kind: 'stream.item',
163
- requestId: context.requestId,
164
- index: count,
165
- value: next.value,
166
- });
167
- output.enqueue(encodeBoundedStreamFrame(context.codec, frame, context.maximumResponseBytes));
168
- count += 1;
169
- }
170
- catch (input) {
171
- if (terminal)
172
- return;
173
- terminal = true;
174
- if (!context.active.signal.aborted) {
175
- try {
176
- output.enqueue(encodeBoundedStreamFrame(context.codec, {
177
- kind: 'stream.error',
178
- requestId: context.requestId,
179
- error: classifyError(input),
180
- }, context.maximumResponseBytes));
181
- }
182
- catch {
183
- // No valid terminal frame can be formed; closing is the only remaining signal.
184
- }
185
- }
186
- output.close();
187
- await Promise.allSettled([
188
- Promise.resolve().then(() => stream.cancel('stream failed')),
189
- Promise.resolve().then(() => iterator.return?.()),
190
- ]);
191
- context.active.settle();
192
- }
193
- },
194
- async cancel(reason) {
195
- const message = typeof reason === 'string' ? reason : 'response consumer cancelled';
196
- context.active.cancel(message);
197
- await stop(message);
198
- },
199
- });
200
- context.active.onCancel(stop);
201
- return new Response(body, {
202
- status: 200,
203
- headers: {
204
- ...context.cors([protocol.http.STREAM_FRAMING_FIELD]),
205
- 'cache-control': 'no-store',
206
- 'content-type': context.codec.contentType,
207
- vary: mergeVary(context.cors(), 'accept'),
208
- [protocol.http.STREAM_FRAMING_FIELD]: protocol.http.LENGTH_PREFIX_V1,
209
- },
210
- });
211
- }
212
- function bindingHeaders(input, negotiated) {
213
- const headers = new Headers(input);
214
- headers.set('cache-control', 'no-store');
215
- if (negotiated)
216
- appendVary(headers, 'accept');
217
- return headers;
218
- }
219
- function errorEnvelopeResponse(codec, response, cors, status, negotiated, maximumResponseBytes) {
220
- try {
221
- return envelopeResponse(codec, response, cors, status, negotiated, maximumResponseBytes);
222
- }
223
- catch (error) {
224
- if (!(error instanceof ResponseBodyTooLargeError))
225
- throw error;
226
- const fallback = {
227
- requestId: response.requestId,
228
- invocation: response.invocation,
229
- error: {
230
- code: errors.CODES.LIMIT_EXCEEDED,
231
- message: 'Response body exceeds the configured limit.',
232
- },
233
- };
234
- try {
235
- return envelopeResponse(codec, fallback, cors, errors.httpStatusOf(fallback.error), negotiated, maximumResponseBytes);
236
- }
237
- catch (fallbackError) {
238
- if (!(fallbackError instanceof ResponseBodyTooLargeError))
239
- throw fallbackError;
240
- return new Response(null, {
241
- status: 500,
242
- headers: bindingHeaders(cors, negotiated),
243
- });
244
- }
245
- }
246
- }
247
- function encodeBoundedStreamFrame(codec, frame, maximumResponseBytes) {
248
- const encoded = protocol.http.encodeStreamFrame(codec, frame);
249
- if (encoded.byteLength - 4 > maximumResponseBytes) {
250
- throw new ResponseBodyTooLargeError(maximumResponseBytes);
251
- }
252
- return encoded;
253
- }
254
- function requireResponseBound(input, maximumResponseBytes) {
255
- if (maximumResponseBytes === undefined)
256
- return;
257
- const bytes = typeof input === 'string' ? new TextEncoder().encode(input).byteLength : input.byteLength;
258
- if (bytes > maximumResponseBytes)
259
- throw new ResponseBodyTooLargeError(maximumResponseBytes);
33
+ return settled(context.active, envelopeResponse(context.codec, envelope, context.cors(), 200, true, context.maximumResponseBytes));
260
34
  }
261
35
  function settled(operation, response) {
262
36
  operation.settle();
263
37
  return response;
264
38
  }
265
- function appendVary(headers, value) {
266
- headers.set('vary', mergeVary(Object.fromEntries(headers), value));
267
- }
268
- function mergeVary(headers, value) {
269
- const names = new Set((headers.vary ?? '')
270
- .split(',')
271
- .map((name) => name.trim().toLowerCase())
272
- .filter(Boolean));
273
- names.add(value.toLowerCase());
274
- return [...names].join(', ');
275
- }
276
- function cancellationFrame(context) {
277
- return {
278
- kind: 'stream.error',
279
- requestId: context.requestId,
280
- error: interruptedStreamError(context.active.timedOut),
281
- };
282
- }
283
- function toBody(input) {
284
- if (typeof input === 'string')
285
- return input;
286
- const copy = new Uint8Array(input);
287
- return copy.buffer;
288
- }
289
- //# sourceMappingURL=render.js.map
@@ -0,0 +1,14 @@
1
+ import type { Codec } from '@astrale-os/kernel-protocol/codecs';
2
+ import type * as protocol from '@astrale-os/kernel-protocol/invocation';
3
+ /** Encode a bounded envelope with its binding content type and cache policy. */
4
+ export declare function envelopeResponse(codec: Codec, input: unknown, cors: Readonly<Record<string, string>>, status?: number, negotiated?: boolean, maximumResponseBytes?: number): Response;
5
+ /** Bound an error, falling back to a correlated limit refusal or an empty response. */
6
+ export declare function errorEnvelopeResponse(codec: Codec, response: protocol.ErrorResponse, cors: Readonly<Record<string, string>>, status: number, negotiated: boolean, maximumResponseBytes: number | undefined): Response;
7
+ /** Enforce the configured byte budget before a buffered response is handed off. */
8
+ export declare function requireResponseBound(input: string | Uint8Array, maximumResponseBytes: number | undefined): void;
9
+ /** Retain existing Vary fields while declaring another response selector. */
10
+ export declare function appendVary(headers: Headers, value: string): void;
11
+ /** Merge one case-insensitive selector into the response Vary field. */
12
+ export declare function mergeVary(headers: Readonly<Record<string, string>>, value: string): string;
13
+ /** Detach encoded bytes for Fetch while retaining encoded text as text. */
14
+ export declare function toBody(input: string | Uint8Array): BodyInit;