@supacloud/elysia 0.10.0 → 0.14.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.
package/dist/index.js CHANGED
@@ -1,5 +1,29 @@
1
1
  // src/index.ts
2
- import { Elysia } from "elysia";
2
+ import { Elysia as Elysia2 } from "elysia";
3
+ import { CommandError as CommandError2, decodeCommandPreview } from "@supacloud/contracts";
4
+ import {
5
+ provideToken,
6
+ runInRequestContext
7
+ } from "@supacloud/app";
8
+ import { REQUEST_CONTEXT } from "@supacloud/app";
9
+
10
+ // src/command-errors.ts
11
+ function commandErrorStatus(code) {
12
+ switch (code) {
13
+ case "COMMAND_INPUT_INVALID":
14
+ return 400;
15
+ case "COMMAND_REJECTED":
16
+ return 403;
17
+ case "COMMAND_IDEMPOTENCY_CONFLICT":
18
+ return 409;
19
+ case "COMMAND_INPUT_EXPIRED":
20
+ return 410;
21
+ case "COMMAND_UNAVAILABLE":
22
+ case "COMMAND_RECEIPT_INVALID":
23
+ case "COMMAND_OUTCOME_UNKNOWN":
24
+ return 503;
25
+ }
26
+ }
3
27
 
4
28
  // src/execution.ts
5
29
  async function observeExecution(observer, event, next) {
@@ -7,7 +31,11 @@ async function observeExecution(observer, event, next) {
7
31
  if (!observer)
8
32
  return;
9
33
  try {
10
- Promise.resolve(observer(Object.freeze({ ...event, phase, durationMs }))).catch(() => {});
34
+ Promise.resolve(observer(Object.freeze({
35
+ ...event,
36
+ phase,
37
+ ...durationMs === undefined ? {} : { durationMs }
38
+ }))).catch(() => {});
11
39
  } catch {}
12
40
  };
13
41
  const started = performance.now();
@@ -27,7 +55,374 @@ function executionRequestId(context) {
27
55
  const value = context.requestId;
28
56
  return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
29
57
  }
58
+ function executionTrace(context) {
59
+ const requestId = executionRequestId(context);
60
+ return requestId === undefined ? {} : { requestId };
61
+ }
62
+
63
+ // src/schema_contract.ts
64
+ import {
65
+ getSchemaValidator,
66
+ StatusMap,
67
+ ElysiaCustomStatusResponse
68
+ } from "elysia";
69
+ function defineRouteContract(schemas) {
70
+ return Object.freeze({ ...schemas });
71
+ }
72
+ var TYPEBOX_KIND = Symbol.for("TypeBox.Kind");
73
+ var RESPONSE_STATUS_MIN = 100;
74
+ var RESPONSE_STATUS_MAX = 599;
75
+ var RESPONSE_FAMILY_PATTERN = /^([1-5])(?:xx|XX)$/;
76
+ var RESPONSE_STATUS_PATTERN = /^[1-5]\d{2}$/;
77
+ function isSchema(value) {
78
+ return value !== null && typeof value === "object" && ((TYPEBOX_KIND in value) || ("~standard" in value));
79
+ }
80
+ function isJsonSchemaObject(value) {
81
+ if (value === null || typeof value !== "object" || Array.isArray(value))
82
+ return false;
83
+ return [
84
+ "$schema",
85
+ "$id",
86
+ "$ref",
87
+ "$defs",
88
+ "definitions",
89
+ "type",
90
+ "properties",
91
+ "patternProperties",
92
+ "required",
93
+ "items",
94
+ "prefixItems",
95
+ "contains",
96
+ "allOf",
97
+ "anyOf",
98
+ "oneOf",
99
+ "not",
100
+ "enum",
101
+ "const",
102
+ "additionalProperties",
103
+ "minLength",
104
+ "maxLength",
105
+ "minimum",
106
+ "maximum",
107
+ "format",
108
+ "pattern"
109
+ ].some((key) => (key in value));
110
+ }
111
+ function invalidResponseSelector(selector) {
112
+ throw new TypeError(`Unsupported response selector "${selector}". Use an HTTP status (100-599), ` + "a status family such as 4XX/5XX, or default.");
113
+ }
114
+ function conflictingResponseSchemas() {
115
+ throw new TypeError("Route contracts cannot declare both response and responses. " + "Remove response and use responses: { 200: Schema }.");
116
+ }
117
+ function duplicateResponseSelector(selector, existing) {
118
+ throw new TypeError(`Duplicate response selectors "${existing}" and "${selector}" differ only by case. ` + `Keep one ${selector.toUpperCase()} selector.`);
119
+ }
120
+ function responseSchemaForStatus(responses, status) {
121
+ const exact = Object.hasOwn(responses, status) ? responses[status] : Object.hasOwn(responses, String(status)) ? responses[String(status)] : undefined;
122
+ if (exact !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status))) {
123
+ return exact;
124
+ }
125
+ const family = `${Math.floor(status / 100)}XX`;
126
+ if (Object.hasOwn(responses, family))
127
+ return responses[family];
128
+ const lowerFamily = family.toLowerCase();
129
+ if (Object.hasOwn(responses, lowerFamily))
130
+ return responses[lowerFamily];
131
+ return Object.hasOwn(responses, "default") ? responses.default : undefined;
132
+ }
133
+ function responseStatusDeclared(responses, status) {
134
+ return responseSchemaForStatus(responses, status) !== undefined || Object.hasOwn(responses, status) || Object.hasOwn(responses, String(status));
135
+ }
136
+ function responseStatusOf(value, configuredStatus) {
137
+ if (value instanceof Response)
138
+ return value.status;
139
+ if (value instanceof ElysiaCustomStatusResponse) {
140
+ const code = value.code;
141
+ if (typeof code === "number" && Number.isInteger(code) && code >= RESPONSE_STATUS_MIN && code <= RESPONSE_STATUS_MAX) {
142
+ return code;
143
+ }
144
+ }
145
+ if (typeof configuredStatus === "number")
146
+ return configuredStatus;
147
+ if (typeof configuredStatus === "string") {
148
+ const mapped = StatusMap[configuredStatus];
149
+ if (typeof mapped === "number")
150
+ return mapped;
151
+ }
152
+ return 200;
153
+ }
154
+ function assertResponseStatusDeclared(responses, status) {
155
+ if (responseStatusDeclared(responses, status))
156
+ return;
157
+ throw new TypeError("Response validation failed");
158
+ }
159
+ function toElysiaResponseMap(responses) {
160
+ const exact = new Map;
161
+ const families = new Map;
162
+ const familySelectors = new Map;
163
+ let defaultSchema;
164
+ let hasDefault = false;
165
+ let requiresExpansion = false;
166
+ for (const [selector, schema] of Object.entries(responses)) {
167
+ if (RESPONSE_STATUS_PATTERN.test(selector)) {
168
+ exact.set(Number(selector), schema);
169
+ continue;
170
+ }
171
+ if (selector === "default") {
172
+ defaultSchema = schema;
173
+ hasDefault = true;
174
+ requiresExpansion = true;
175
+ continue;
176
+ }
177
+ const family = RESPONSE_FAMILY_PATTERN.exec(selector);
178
+ if (family) {
179
+ const canonicalSelector = selector.toLowerCase();
180
+ const existingSelector = familySelectors.get(canonicalSelector);
181
+ if (existingSelector !== undefined)
182
+ duplicateResponseSelector(selector, existingSelector);
183
+ familySelectors.set(canonicalSelector, selector);
184
+ families.set(Number(family[1]), schema);
185
+ requiresExpansion = true;
186
+ continue;
187
+ }
188
+ invalidResponseSelector(selector);
189
+ }
190
+ if (!requiresExpansion)
191
+ return responses;
192
+ const expanded = {};
193
+ if (hasDefault) {
194
+ for (let status = RESPONSE_STATUS_MIN;status <= RESPONSE_STATUS_MAX; status++) {
195
+ expanded[status] = defaultSchema;
196
+ }
197
+ }
198
+ for (const [family, schema] of families) {
199
+ const first = family * 100;
200
+ for (let status = first;status < first + 100; status++)
201
+ expanded[status] = schema;
202
+ }
203
+ for (const [status, schema] of exact)
204
+ expanded[status] = schema;
205
+ return expanded;
206
+ }
207
+ function toElysiaResponseSchema(response) {
208
+ if (typeof response === "string" || isSchema(response) || isJsonSchemaObject(response))
209
+ return response;
210
+ if (response === null || typeof response !== "object" || Array.isArray(response))
211
+ return response;
212
+ return toElysiaResponseMap(response);
213
+ }
214
+ function toElysiaRouteSchema(contract) {
215
+ if (contract.response !== undefined && contract.responses !== undefined) {
216
+ conflictingResponseSchemas();
217
+ }
218
+ const schema = {};
219
+ if (contract.body !== undefined)
220
+ schema.body = contract.body;
221
+ if (contract.params !== undefined)
222
+ schema.params = contract.params;
223
+ if (contract.query !== undefined)
224
+ schema.query = contract.query;
225
+ if (contract.headers !== undefined)
226
+ schema.headers = contract.headers;
227
+ if (contract.cookie !== undefined)
228
+ schema.cookie = contract.cookie;
229
+ if (contract.responses !== undefined)
230
+ schema.response = toElysiaResponseMap(contract.responses);
231
+ else if (contract.response !== undefined)
232
+ schema.response = toElysiaResponseSchema(contract.response);
233
+ return schema;
234
+ }
235
+ function defineElysiaRoute(method, path, contract, handler) {
236
+ toElysiaRouteSchema(contract);
237
+ const stableContract = Object.isFrozen(contract) ? contract : Object.freeze(contract);
238
+ return Object.freeze({
239
+ method,
240
+ path,
241
+ contract: stableContract,
242
+ handler
243
+ });
244
+ }
245
+ function registerElysiaRoute(app, route) {
246
+ const handler = async (context) => {
247
+ const value = await route.handler(context);
248
+ if (route.contract.responses !== undefined) {
249
+ const status = responseStatusOf(value, context.set.status);
250
+ assertResponseStatusDeclared(route.contract.responses, status);
251
+ }
252
+ return value;
253
+ };
254
+ app.route(route.method, route.path, handler, toElysiaRouteSchema(route.contract));
255
+ return app;
256
+ }
257
+
258
+ class SchemaContractError extends Error {
259
+ code = "SCHEMA_CONTRACT_INVALID";
260
+ constructor() {
261
+ super("Schema contract validation failed");
262
+ this.name = "SchemaContractError";
263
+ }
264
+ }
265
+ function createSchemaDecoder(schema, options = {}) {
266
+ const validator = getSchemaValidator(schema, {
267
+ coerce: options.coerce ?? false,
268
+ normalize: options.normalize ?? true
269
+ });
270
+ return (value) => {
271
+ try {
272
+ return validator.parse(value);
273
+ } catch {
274
+ throw new SchemaContractError;
275
+ }
276
+ };
277
+ }
278
+ function defineJsonContract(schemas, request, options = {}) {
279
+ return Object.freeze({
280
+ ...schemas,
281
+ input: createSchemaDecoder(schemas.body, options),
282
+ result: createSchemaDecoder(schemas.response, options),
283
+ request
284
+ });
285
+ }
30
286
 
287
+ // src/documentation.ts
288
+ import { Elysia } from "elysia";
289
+ function normalizePath(value, fallback) {
290
+ const path = value ?? fallback;
291
+ if (!path.trim() || !path.startsWith("/") || path.includes("?") || path.includes("#")) {
292
+ throw new Error(`Documentation path must be an absolute path without query or fragment: ${path}`);
293
+ }
294
+ return path.length > 1 ? path.replace(/\/+$/, "") : path;
295
+ }
296
+ function resolveSource(source) {
297
+ if (typeof source === "function") {
298
+ return Promise.resolve(source());
299
+ }
300
+ return Promise.resolve(source);
301
+ }
302
+ function jsonResponse(document) {
303
+ if (document === null || typeof document !== "object" || Array.isArray(document)) {
304
+ throw new Error("OpenAPI documentation must be a JSON object");
305
+ }
306
+ const serialized = JSON.stringify(document);
307
+ if (serialized === undefined)
308
+ throw new Error("OpenAPI documentation must be JSON-serializable");
309
+ return new Response(serialized, {
310
+ headers: { "content-type": "application/json; charset=utf-8" }
311
+ });
312
+ }
313
+ function textResponse(schema) {
314
+ return new Response(schema, {
315
+ headers: { "content-type": "text/plain; charset=utf-8" }
316
+ });
317
+ }
318
+ function escapeHtml(value) {
319
+ return value.replace(/[&<>"']/g, (character) => ({
320
+ "&": "&amp;",
321
+ "<": "&lt;",
322
+ ">": "&gt;",
323
+ '"': "&quot;",
324
+ "'": "&#39;"
325
+ })[character] ?? character);
326
+ }
327
+ function scriptString(value) {
328
+ return JSON.stringify(value).replace(/</g, "\\u003c");
329
+ }
330
+ function renderViewerPage(title, resourcePath, mode) {
331
+ const parse = mode === "json" ? "const value = await response.json(); output.textContent = JSON.stringify(value, null, 2);" : "output.textContent = await response.text();";
332
+ const safeTitle = escapeHtml(title);
333
+ const safeResourcePath = escapeHtml(resourcePath);
334
+ return `<!doctype html>
335
+ <html lang="en">
336
+ <head>
337
+ <meta charset="utf-8">
338
+ <meta name="viewport" content="width=device-width, initial-scale=1">
339
+ <title>${safeTitle}</title>
340
+ <style>
341
+ :root { color-scheme: light dark; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
342
+ body { margin: 0; padding: 24px; background: Canvas; color: CanvasText; }
343
+ header { display: flex; gap: 16px; align-items: baseline; flex-wrap: wrap; }
344
+ h1 { font: 600 20px system-ui, sans-serif; margin: 0; }
345
+ a { color: LinkText; }
346
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.5; }
347
+ </style>
348
+ </head>
349
+ <body>
350
+ <header><h1>${safeTitle}</h1><a href="${safeResourcePath}">${safeResourcePath}</a></header>
351
+ <pre id="output">Loading...</pre>
352
+ <script>
353
+ const output = document.getElementById("output");
354
+ fetch(${scriptString(resourcePath)}, { headers: { "accept": "${mode === "json" ? "application/json" : "text/plain"}" } })
355
+ .then(async (response) => {
356
+ if (!response.ok) throw new Error("HTTP " + response.status);
357
+ ${parse}
358
+ })
359
+ .catch((error) => { output.textContent = "Unable to load documentation: " + error.message; });
360
+ </script>
361
+ </body>
362
+ </html>`;
363
+ }
364
+ function createDocumentationPlugin(options = {}) {
365
+ const app = new Elysia({ name: "supacloud:documentation" });
366
+ const paths = new Set;
367
+ const reserve = (path) => {
368
+ if (paths.has(path))
369
+ throw new Error(`Duplicate documentation path: ${path}`);
370
+ paths.add(path);
371
+ };
372
+ if (options.openApi) {
373
+ const specPath = normalizePath(options.openApi.specPath, "/openapi.json");
374
+ const uiPath = normalizePath(options.openApi.uiPath, "/docs");
375
+ reserve(specPath);
376
+ reserve(uiPath);
377
+ const title = options.openApi.title ?? "OpenAPI Documentation";
378
+ app.get(specPath, async () => jsonResponse(await resolveSource(options.openApi.document)), {
379
+ detail: { hide: true }
380
+ });
381
+ app.get(uiPath, () => new Response(renderViewerPage(title, specPath, "json"), {
382
+ headers: { "content-type": "text/html; charset=utf-8" }
383
+ }), { detail: { hide: true } });
384
+ }
385
+ if (options.graphql) {
386
+ const schemaPath = normalizePath(options.graphql.schemaPath, "/graphql/schema.graphql");
387
+ const uiPath = normalizePath(options.graphql.uiPath, "/graphql/docs");
388
+ reserve(schemaPath);
389
+ reserve(uiPath);
390
+ const title = options.graphql.title ?? "GraphQL Schema";
391
+ app.get(schemaPath, async () => textResponse(await resolveSource(options.graphql.schema)), {
392
+ detail: { hide: true }
393
+ });
394
+ app.get(uiPath, () => new Response(renderViewerPage(title, schemaPath, "text"), {
395
+ headers: { "content-type": "text/html; charset=utf-8" }
396
+ }), { detail: { hide: true } });
397
+ }
398
+ return app;
399
+ }
400
+ // src/persistent-command.ts
401
+ import {
402
+ CommandError,
403
+ decodeCommandIdentity
404
+ } from "@supacloud/contracts";
405
+ function createPersistentCommandAdapter(command, options) {
406
+ return {
407
+ capabilities: {
408
+ audit: true,
409
+ idempotency: true,
410
+ transaction: command.kind === "transactional",
411
+ boundary: command.kind === "transactional" ? "database" : "external"
412
+ },
413
+ async execute(invocation) {
414
+ try {
415
+ const identity = decodeCommandIdentity(await options.identity(invocation));
416
+ return await command.execute(identity, requireIdempotencyKey(invocation), options.input(invocation));
417
+ } catch (error) {
418
+ if (!(error instanceof CommandError))
419
+ throw error;
420
+ const status = commandErrorStatus(error.code);
421
+ throw new ApplicationError(error.code, { code: error.code, status });
422
+ }
423
+ }
424
+ };
425
+ }
31
426
  // src/identity.ts
32
427
  import { createRemoteJWKSet, jwtVerify, errors } from "jose";
33
428
  function httpsUrl(value) {
@@ -449,6 +844,401 @@ function cloneTables(tables) {
449
844
  function cloneValue(value) {
450
845
  return structuredClone(value);
451
846
  }
847
+ // src/worker.ts
848
+ function workerFailureMessage(error) {
849
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Worker job failed";
850
+ const normalized = message.replace(/[\u0000-\u001f\u007f]/g, " ").trim();
851
+ return (normalized || "Worker job failed").slice(0, 4096);
852
+ }
853
+ function createQueueWorkerTransport(options) {
854
+ if (!options.queue || typeof options.queue.receive !== "function" || typeof options.queue.ack !== "function" || typeof options.queue.fail !== "function" || typeof options.decodeClaim !== "function" || typeof options.messageId !== "function") {
855
+ throw new TypeError("Queue worker transport requires queue, decodeClaim and messageId");
856
+ }
857
+ const receiveOptions = options.receive === undefined ? undefined : { ...options.receive };
858
+ return {
859
+ claim: async (signal) => {
860
+ signal.throwIfAborted();
861
+ const message = await options.queue.receive(receiveOptions);
862
+ signal.throwIfAborted();
863
+ return message === null ? null : options.decodeClaim(message);
864
+ },
865
+ ack: (claim) => options.queue.ack(options.messageId(claim)),
866
+ fail: (claim, error) => options.queue.fail(options.messageId(claim), {
867
+ error: workerFailureMessage(error)
868
+ })
869
+ };
870
+ }
871
+
872
+ class WorkerRegistrationError extends Error {
873
+ code;
874
+ constructor(code, message) {
875
+ super(message);
876
+ this.name = "WorkerRegistrationError";
877
+ this.code = code;
878
+ }
879
+ }
880
+
881
+ class WorkerReceiptUnconfirmedError extends Error {
882
+ operation;
883
+ claimId;
884
+ code = "WORKER_RECEIPT_UNCONFIRMED";
885
+ mutationMayHaveApplied = true;
886
+ constructor(operation, claimId) {
887
+ super(`Worker ${operation} receipt could not be confirmed for claim "${claimId}"`);
888
+ this.operation = operation;
889
+ this.claimId = claimId;
890
+ this.name = "WorkerReceiptUnconfirmedError";
891
+ }
892
+ }
893
+ function safeWorkerId(value) {
894
+ const workerId = value ?? `worker-${crypto.randomUUID()}`;
895
+ if (workerId.length === 0 || workerId.length > 256 || /[\u0000-\u001f\u007f]/.test(workerId)) {
896
+ throw new TypeError("Worker workerId must be a safe non-empty string");
897
+ }
898
+ return workerId;
899
+ }
900
+ function safeDescriptorText(value, maxLength) {
901
+ return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength && !/[\u0000-\u001f\u007f]/.test(value);
902
+ }
903
+ function validJobScope(value) {
904
+ return value === "application" || value === "request" || value === "job";
905
+ }
906
+ function validJobMode(value) {
907
+ return value === "task" || value === "workflow";
908
+ }
909
+ function validJobIdempotency(value) {
910
+ return value === "required" || value === "none";
911
+ }
912
+ function validPositiveInteger(value) {
913
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
914
+ }
915
+ function safeClaim(claim) {
916
+ if (!claim || typeof claim !== "object") {
917
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim is invalid");
918
+ }
919
+ if (typeof claim.id !== "string" || claim.id.length === 0 || claim.id.length > 512 || /[\u0000-\u001f\u007f]/.test(claim.id) || typeof claim.jobName !== "string" || claim.jobName.length === 0 || claim.jobName.length > 256 || /[\u0000-\u001f\u007f]/.test(claim.jobName)) {
920
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim identity is invalid");
921
+ }
922
+ const attempt = claim.attempt ?? 1;
923
+ if (!Number.isSafeInteger(attempt) || attempt < 1) {
924
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Worker claim attempt is invalid");
925
+ }
926
+ return { ...claim, attempt };
927
+ }
928
+ function delay(milliseconds, signal) {
929
+ if (milliseconds <= 0)
930
+ return Promise.resolve();
931
+ return new Promise((resolve, reject) => {
932
+ let settled = false;
933
+ const finish = (callback) => {
934
+ if (settled)
935
+ return;
936
+ settled = true;
937
+ clearTimeout(timer);
938
+ signal.removeEventListener("abort", onAbort);
939
+ callback();
940
+ };
941
+ const onAbort = () => finish(() => reject(signal.reason ?? new DOMException("Aborted", "AbortError")));
942
+ const timer = setTimeout(() => finish(resolve), milliseconds);
943
+ signal.addEventListener("abort", onAbort, { once: true });
944
+ if (signal.aborted)
945
+ onAbort();
946
+ });
947
+ }
948
+ async function destroyServiceValues(services) {
949
+ const errors = [];
950
+ const seen = new Set;
951
+ for (const value of Object.values(services).reverse()) {
952
+ if (seen.has(value) || !value || typeof value !== "object")
953
+ continue;
954
+ seen.add(value);
955
+ const candidate = value;
956
+ const hook = typeof candidate.onDestroy === "function" ? candidate.onDestroy : typeof candidate.ngOnDestroy === "function" ? candidate.ngOnDestroy : undefined;
957
+ if (!hook)
958
+ continue;
959
+ try {
960
+ await Reflect.apply(hook, value, []);
961
+ } catch (error) {
962
+ errors.push(error);
963
+ }
964
+ }
965
+ if (errors.length > 0)
966
+ throw new AggregateError(errors, "Worker service destruction failed");
967
+ }
968
+
969
+ class SupaCloudWorker {
970
+ registry = new Map;
971
+ modules = [];
972
+ moduleServices = new Map;
973
+ deps;
974
+ transport;
975
+ mapClaim;
976
+ requestContextFactory;
977
+ workerId;
978
+ concurrency;
979
+ pollIntervalMs;
980
+ executor;
981
+ observer;
982
+ onError;
983
+ destroyServices;
984
+ stateValue = "idle";
985
+ claimController;
986
+ loopPromise;
987
+ startPromise;
988
+ stopPromise;
989
+ stopRequested = false;
990
+ inFlight = new Set;
991
+ services = {};
992
+ imported = {};
993
+ constructor(options) {
994
+ if (!options.transport || typeof options.transport.claim !== "function" || typeof options.transport.fail !== "function" || typeof options.transport.ack !== "function" && typeof options.transport.acknowledge !== "function") {
995
+ throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker transport must provide claim, ack and fail");
996
+ }
997
+ this.transport = options.transport;
998
+ this.deps = options.deps ?? {};
999
+ this.workerId = safeWorkerId(options.workerId);
1000
+ this.mapClaim = options.mapClaim ?? ((claim) => claim);
1001
+ this.requestContextFactory = options.requestContext ?? ((claim, signal) => ({
1002
+ workerId: this.workerId,
1003
+ jobId: claim.id,
1004
+ jobName: claim.jobName,
1005
+ attempt: claim.attempt,
1006
+ signal
1007
+ }));
1008
+ this.concurrency = this.capturePositiveInteger(options.concurrency, 1, 1000, "concurrency");
1009
+ this.pollIntervalMs = this.captureNonNegativeInteger(options.pollIntervalMs, 1000, 300000, "pollIntervalMs");
1010
+ this.executor = options.executor;
1011
+ this.observer = options.onExecution;
1012
+ this.onError = options.onError;
1013
+ this.destroyServices = options.destroyServices;
1014
+ for (const module of options.modules ?? [])
1015
+ this.registerModule(module);
1016
+ }
1017
+ get state() {
1018
+ return this.stateValue;
1019
+ }
1020
+ get id() {
1021
+ return this.workerId;
1022
+ }
1023
+ get jobNames() {
1024
+ return Object.freeze([...this.registry.keys()]);
1025
+ }
1026
+ get jobs() {
1027
+ return Object.freeze([...this.registry.values()].map((entry) => entry.job));
1028
+ }
1029
+ registerModule(module) {
1030
+ if (this.stateValue !== "idle" && this.stateValue !== "stopped") {
1031
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "Jobs cannot be registered while the worker is active");
1032
+ }
1033
+ if (!module || typeof module !== "object" || typeof module.name !== "string" || module.name.length === 0 || module.name.length > 256 || /[\u0000-\u001f\u007f]/.test(module.name) || typeof module.createServices !== "function" || module.jobs !== undefined && !Array.isArray(module.jobs)) {
1034
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered module is invalid");
1035
+ }
1036
+ if (this.modules.some((candidate) => candidate.name === module.name)) {
1037
+ throw new WorkerRegistrationError("WORKER_DUPLICATE_MODULE", `Module "${module.name}" is registered more than once`);
1038
+ }
1039
+ const jobs = module.jobs ?? [];
1040
+ const names = new Set;
1041
+ for (const job of jobs) {
1042
+ if (!job || typeof job !== "object" || !safeDescriptorText(job.className, 256) || !safeDescriptorText(job.name, 256) || !safeDescriptorText(job.serviceKey, 256) || !validJobScope(job.scope) || job.mode !== undefined && !validJobMode(job.mode) || job.idempotency !== undefined && !validJobIdempotency(job.idempotency) || job.timeoutSec !== undefined && !validPositiveInteger(job.timeoutSec) || job.maxAttempts !== undefined && !validPositiveInteger(job.maxAttempts)) {
1043
+ throw new WorkerRegistrationError("WORKER_INVALID_JOB", "A registered Job has invalid identity or execution metadata");
1044
+ }
1045
+ if (this.registry.has(job.name) || names.has(job.name)) {
1046
+ throw new WorkerRegistrationError("WORKER_DUPLICATE_JOB", `Job "${job.name}" is registered more than once`);
1047
+ }
1048
+ names.add(job.name);
1049
+ }
1050
+ this.modules.push(module);
1051
+ for (const job of jobs)
1052
+ this.registry.set(job.name, { module, job });
1053
+ }
1054
+ register(module) {
1055
+ this.registerModule(module);
1056
+ }
1057
+ async start() {
1058
+ if (this.stateValue === "running")
1059
+ return;
1060
+ if (this.stateValue === "starting" && this.startPromise)
1061
+ return this.startPromise;
1062
+ if (this.stateValue === "stopping" && this.stopPromise)
1063
+ await this.stopPromise;
1064
+ this.stopRequested = false;
1065
+ this.stateValue = "starting";
1066
+ const startPromise = Promise.resolve().then(() => this.initialize()).then(async () => {
1067
+ if (this.stopRequested || this.stateValue !== "starting") {
1068
+ await this.releaseServices();
1069
+ return;
1070
+ }
1071
+ this.claimController = new AbortController;
1072
+ this.stateValue = "running";
1073
+ this.loopPromise = this.pollLoop(this.claimController.signal);
1074
+ }).catch(async (error) => {
1075
+ this.stateValue = "stopped";
1076
+ await this.releaseServices();
1077
+ throw error;
1078
+ });
1079
+ this.startPromise = startPromise;
1080
+ return startPromise;
1081
+ }
1082
+ async stop() {
1083
+ if (this.stateValue === "idle" || this.stateValue === "stopped")
1084
+ return;
1085
+ if (this.stopPromise)
1086
+ return this.stopPromise;
1087
+ this.stopRequested = true;
1088
+ this.stateValue = "stopping";
1089
+ this.claimController?.abort(new DOMException("Worker stopped", "AbortError"));
1090
+ const startPromise = this.startPromise;
1091
+ this.stopPromise = (async () => {
1092
+ await startPromise?.catch(() => {
1093
+ return;
1094
+ });
1095
+ await this.loopPromise?.catch((error) => this.reportError(error));
1096
+ await Promise.allSettled([...this.inFlight]);
1097
+ await this.releaseServices();
1098
+ this.stateValue = "stopped";
1099
+ this.claimController = undefined;
1100
+ this.loopPromise = undefined;
1101
+ this.startPromise = undefined;
1102
+ this.stopPromise = undefined;
1103
+ })();
1104
+ return this.stopPromise;
1105
+ }
1106
+ async runOnce() {
1107
+ if (this.stateValue !== "running") {
1108
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker must be started before runOnce");
1109
+ }
1110
+ const controller = this.claimController;
1111
+ if (!controller)
1112
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker claim lifecycle is unavailable");
1113
+ controller.signal.throwIfAborted();
1114
+ const claim = await this.transport.claim(controller.signal);
1115
+ if (claim === null)
1116
+ return null;
1117
+ return this.processClaim(claim);
1118
+ }
1119
+ async processClaim(rawClaim) {
1120
+ const claim = safeClaim(this.mapClaim(rawClaim));
1121
+ const entry = this.registry.get(claim.jobName);
1122
+ const controller = new AbortController;
1123
+ const context = Object.freeze({
1124
+ jobId: claim.id,
1125
+ jobName: claim.jobName,
1126
+ attempt: claim.attempt ?? 1,
1127
+ signal: controller.signal
1128
+ });
1129
+ if (!entry) {
1130
+ const receipt = await this.failClaim(rawClaim, claim, new WorkerRegistrationError("WORKER_INVALID_JOB", `Job "${claim.jobName}" is not registered`), context);
1131
+ return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
1132
+ }
1133
+ let output;
1134
+ try {
1135
+ if (this.stateValue !== "running" && this.stateValue !== "stopping") {
1136
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker is not active");
1137
+ }
1138
+ const requestContext = claim.requestContext ?? await this.requestContextFactory(claim, controller.signal);
1139
+ const moduleServices = this.moduleServices.get(entry.module.name);
1140
+ if (!moduleServices)
1141
+ throw new WorkerRegistrationError("WORKER_NOT_STARTED", "Worker services are unavailable");
1142
+ output = await executeJob(entry.module, moduleServices, entry.job, claim.input, requestContext, this.imported, this.executor, this.observer);
1143
+ } catch (error) {
1144
+ const receipt = await this.failClaim(rawClaim, claim, error, context);
1145
+ return { claimId: claim.id, jobName: claim.jobName, status: "failed", receipt };
1146
+ }
1147
+ const acknowledge = this.transport.ack ?? this.transport.acknowledge;
1148
+ if (!acknowledge)
1149
+ throw new WorkerRegistrationError("WORKER_TRANSPORT_INVALID", "Worker acknowledgement is unavailable");
1150
+ try {
1151
+ const receipt = await acknowledge(rawClaim, output, context);
1152
+ return { claimId: claim.id, jobName: claim.jobName, status: "acknowledged", receipt };
1153
+ } catch {
1154
+ throw new WorkerReceiptUnconfirmedError("ack", claim.id);
1155
+ }
1156
+ }
1157
+ async failClaim(rawClaim, claim, error, context) {
1158
+ try {
1159
+ return await this.transport.fail(rawClaim, error, context);
1160
+ } catch {
1161
+ throw new WorkerReceiptUnconfirmedError("fail", claim.id);
1162
+ }
1163
+ }
1164
+ async initialize() {
1165
+ this.services = {};
1166
+ this.imported = {};
1167
+ this.moduleServices.clear();
1168
+ for (const module of this.modules) {
1169
+ const services = module.createServices(this.deps, this.imported);
1170
+ this.moduleServices.set(module.name, services);
1171
+ this.services = { ...this.services, ...services };
1172
+ this.imported[module.name] = services;
1173
+ }
1174
+ }
1175
+ async releaseServices() {
1176
+ const services = this.services;
1177
+ const imported = this.imported;
1178
+ this.services = {};
1179
+ this.imported = {};
1180
+ this.moduleServices.clear();
1181
+ if (Object.keys(services).length === 0)
1182
+ return;
1183
+ try {
1184
+ if (this.destroyServices)
1185
+ await this.destroyServices(services, imported);
1186
+ else
1187
+ await destroyServiceValues(services);
1188
+ } catch (error) {
1189
+ await this.reportError(error);
1190
+ }
1191
+ }
1192
+ async pollLoop(signal) {
1193
+ while (!signal.aborted && this.stateValue === "running") {
1194
+ if (this.inFlight.size >= this.concurrency) {
1195
+ await Promise.race(this.inFlight).catch((error) => this.reportError(error));
1196
+ continue;
1197
+ }
1198
+ let claim;
1199
+ try {
1200
+ claim = await this.transport.claim(signal);
1201
+ } catch (error) {
1202
+ if (signal.aborted)
1203
+ break;
1204
+ await this.reportError(error);
1205
+ await delay(this.pollIntervalMs, signal).catch(() => {
1206
+ return;
1207
+ });
1208
+ continue;
1209
+ }
1210
+ if (claim === null) {
1211
+ await delay(this.pollIntervalMs, signal).catch(() => {
1212
+ return;
1213
+ });
1214
+ continue;
1215
+ }
1216
+ const execution = this.processClaim(claim).catch((error) => this.reportError(error));
1217
+ this.inFlight.add(execution);
1218
+ execution.finally(() => this.inFlight.delete(execution));
1219
+ }
1220
+ }
1221
+ async reportError(error) {
1222
+ try {
1223
+ await this.onError?.(error);
1224
+ } catch {}
1225
+ }
1226
+ capturePositiveInteger(value, fallback, max, name) {
1227
+ const result = value ?? fallback;
1228
+ if (!Number.isSafeInteger(result) || result < 1 || result > max)
1229
+ throw new TypeError(`Worker ${name} is invalid`);
1230
+ return result;
1231
+ }
1232
+ captureNonNegativeInteger(value, fallback, max, name) {
1233
+ const result = value ?? fallback;
1234
+ if (!Number.isSafeInteger(result) || result < 0 || result > max)
1235
+ throw new TypeError(`Worker ${name} is invalid`);
1236
+ return result;
1237
+ }
1238
+ }
1239
+ function createWorker(options) {
1240
+ return new SupaCloudWorker(options);
1241
+ }
452
1242
 
453
1243
  // src/index.ts
454
1244
  var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
@@ -463,9 +1253,10 @@ function composeAspects(...aspects) {
463
1253
  return Promise.reject(new Error("next() called multiple times"));
464
1254
  }
465
1255
  index = current;
466
- if (current === active.length)
1256
+ const aspect = active[current];
1257
+ if (!aspect)
467
1258
  return Promise.resolve(next());
468
- return Promise.resolve(active[current](context, () => dispatch(current + 1)));
1259
+ return Promise.resolve(aspect(context, () => dispatch(current + 1)));
469
1260
  };
470
1261
  return dispatch(0);
471
1262
  };
@@ -475,7 +1266,7 @@ function observedAspects(aspects, boundary, observer) {
475
1266
  kind: context.kind,
476
1267
  operation: context.name,
477
1268
  stage: `${boundary}.aspect[${index}]:${aspect.name || "anonymous"}`,
478
- requestId: executionRequestId(context.requestContext)
1269
+ ...executionTrace(context.requestContext)
479
1270
  }, () => aspect(context, next))));
480
1271
  }
481
1272
  function composeCommandExecutors(...executors) {
@@ -537,6 +1328,19 @@ function validatedJsonResponse(validate, value, init) {
537
1328
  headers.set("content-type", "application/json");
538
1329
  return new Response(body, { ...init, headers });
539
1330
  }
1331
+ function decodeJobContract(schema, value, boundary) {
1332
+ if (schema === undefined)
1333
+ return value;
1334
+ try {
1335
+ return createSchemaDecoder(schema)(value);
1336
+ } catch {
1337
+ const input = boundary === "input";
1338
+ throw new ApplicationError(input ? "Job input contract validation failed" : "Job output contract validation failed", {
1339
+ status: input ? 422 : 500,
1340
+ code: input ? "JOB_INPUT_VALIDATION_ERROR" : "JOB_OUTPUT_VALIDATION_ERROR"
1341
+ });
1342
+ }
1343
+ }
540
1344
  function safeHeaderValue(value, maxLength) {
541
1345
  if (!value || value.length > maxLength || /[\u0000-\u001f\u007f]/.test(value)) {
542
1346
  return;
@@ -555,7 +1359,7 @@ function isAuthenticatedTrustedIdentity(value) {
555
1359
  function bearerToken(request) {
556
1360
  const authorization = request.headers.get("authorization");
557
1361
  const match = authorization?.match(/^Bearer\s+(.+)$/i);
558
- return match ? safeHeaderValue(match[1], 16384) : undefined;
1362
+ return match?.[1] ? safeHeaderValue(match[1], 16384) : undefined;
559
1363
  }
560
1364
  var createSupaCloudRequestContext = (request) => {
561
1365
  const subject = safeHeaderValue(request.headers.get(VERIFIED_JWT_SUBJECT_HEADER), 1024);
@@ -612,7 +1416,7 @@ function createCommandExecutor(governance, observer) {
612
1416
  const event = {
613
1417
  kind: "command",
614
1418
  operation: command.name,
615
- requestId: executionRequestId(invocation.requestContext)
1419
+ ...executionTrace(invocation.requestContext)
616
1420
  };
617
1421
  await observeExecution(observer, { ...event, stage: "authorize" }, () => governance.authorize(invocation));
618
1422
  return observeExecution(observer, { ...event, stage: `rpc:${command.rpc}` }, () => adapter.execute(invocation, once(next)));
@@ -630,12 +1434,12 @@ function createCommandExecutor(governance, observer) {
630
1434
  throw missingGovernanceAdapter(command, "idempotency");
631
1435
  }
632
1436
  try {
633
- const observe = (stage, next2) => observeExecution(observer, {
1437
+ const observe = (stage, next) => observeExecution(observer, {
634
1438
  kind: "command",
635
1439
  operation: command.name,
636
1440
  stage,
637
- requestId: executionRequestId(invocation.requestContext)
638
- }, next2);
1441
+ ...executionTrace(invocation.requestContext)
1442
+ }, next);
639
1443
  await observe("authorize", () => governance.authorize(invocation));
640
1444
  let execute = async () => {
641
1445
  const result = await observe("handler", invokeOnce);
@@ -669,13 +1473,15 @@ async function executeCompiledCommand(options) {
669
1473
  if (matches.length !== 1)
670
1474
  throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
671
1475
  const command = matches[0];
1476
+ if (!command)
1477
+ throw new Error(`Command not found: ${options.command}`);
672
1478
  const invocation = {
673
1479
  command,
674
1480
  input: { body: options.input, params: {}, query: {} },
675
1481
  request: options.request,
676
1482
  requestContext: options.requestContext,
677
1483
  services: options.services ?? {},
678
- scope: options.scope
1484
+ ...options.scope ? { scope: options.scope } : {}
679
1485
  };
680
1486
  const aspects = composeAspects(observedAspects(options.module.aspects ?? [], `module:${options.module.name}`, options.observer), observedAspects(command.aspects ?? [], "command", options.observer));
681
1487
  const value = await createCommandExecutor(options.governance, options.observer)(invocation, once(() => aspects({
@@ -685,11 +1491,44 @@ async function executeCompiledCommand(options) {
685
1491
  request: options.request,
686
1492
  requestContext: options.requestContext,
687
1493
  services: invocation.services,
688
- scope: invocation.scope,
1494
+ ...invocation.scope ? { scope: invocation.scope } : {},
689
1495
  metadata: command
690
1496
  }, once(() => options.handler(options.input)))));
691
1497
  return options.decode(value);
692
1498
  }
1499
+ async function previewCompiledCommand(options) {
1500
+ const matches = options.module.commands?.filter((item) => item.className === options.command) ?? [];
1501
+ if (matches.length !== 1)
1502
+ throw new ApplicationError("Command descriptor missing or ambiguous", { code: "COMMAND_NOT_REGISTERED" });
1503
+ const command = matches[0];
1504
+ if (!command)
1505
+ throw new Error(`Command not found: ${options.command}`);
1506
+ const invocation = {
1507
+ command,
1508
+ input: { body: options.input, params: {}, query: {} },
1509
+ request: options.request,
1510
+ requestContext: options.requestContext,
1511
+ services: options.services ?? {},
1512
+ ...options.scope ? { scope: options.scope } : {}
1513
+ };
1514
+ try {
1515
+ await options.governance.authorize(invocation);
1516
+ } catch (error) {
1517
+ if (error instanceof ApplicationError && (error.status === 401 || error.status === 403)) {
1518
+ return decodeCommandPreview({
1519
+ command: command.name,
1520
+ allowed: false,
1521
+ blockers: [{ code: error.code, message: error.message }]
1522
+ });
1523
+ }
1524
+ throw error;
1525
+ }
1526
+ const preview = decodeCommandPreview(await options.preview(options.input));
1527
+ if (preview.command !== command.name && preview.command !== command.className) {
1528
+ throw new TypeError("Mismatched command identity");
1529
+ }
1530
+ return preview;
1531
+ }
693
1532
  function once(next) {
694
1533
  let called = false;
695
1534
  return async () => {
@@ -731,6 +1570,26 @@ function joinPaths(prefix, path) {
731
1570
  function controllerInstance(value) {
732
1571
  return isRecord(value) ? value : undefined;
733
1572
  }
1573
+ function cookieValues(value) {
1574
+ if (!isRecord(value))
1575
+ return;
1576
+ const result = {};
1577
+ for (const [name, entry] of Object.entries(value)) {
1578
+ result[name] = isRecord(entry) && "value" in entry ? entry.value : entry;
1579
+ }
1580
+ return result;
1581
+ }
1582
+ function assertDeclaredResponseStatus(route, value, configuredStatus) {
1583
+ if (route.responses === undefined)
1584
+ return;
1585
+ const status = responseStatusOf(value, configuredStatus);
1586
+ if (responseStatusDeclared(route.responses, status))
1587
+ return;
1588
+ const transport = route.contract?.response;
1589
+ if ((transport === "binary" || transport === "stream") && status >= 200 && status < 300)
1590
+ return;
1591
+ throw new ApplicationError("Response validation failed", { status: 500, code: "RESPONSE_CONTRACT_UNDECLARED" });
1592
+ }
734
1593
  function createModulePlugin(compiled, services, ctxFactory = defaultRequestContext, options = {}, imported = {}) {
735
1594
  const hasCommandRoutes = compiled.controllers.some((controller) => controller.routes.some((route) => route.command !== undefined));
736
1595
  if (hasCommandRoutes && !options.commandGovernance && !options.commandExecutor) {
@@ -749,7 +1608,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
749
1608
  const requestContexts = new WeakMap;
750
1609
  const governanceExecutor = options.commandGovernance ? createCommandExecutor(options.commandGovernance, options.onExecution) : undefined;
751
1610
  const commandExecutor = options.commandExecutor && governanceExecutor ? composeCommandExecutors(options.commandExecutor, governanceExecutor) : options.commandExecutor ?? governanceExecutor;
752
- const plugin = new Elysia({ name: `supacloud:${compiled.name}` }).decorate("services", services);
1611
+ const plugin = new Elysia2({ name: `supacloud:${compiled.name}`, normalize: options.normalize ?? true }).decorate("services", services);
753
1612
  const createRequestScope = compiled.createRequestScope;
754
1613
  const requestScopes = new WeakMap;
755
1614
  if (compiled.destroyRequestScope) {
@@ -783,99 +1642,98 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
783
1642
  for (const controller of compiled.controllers) {
784
1643
  for (const route of controller.routes) {
785
1644
  const path = joinPaths(controller.path, route.path);
786
- const schema = {};
787
- if (route.body !== undefined)
788
- schema.body = route.body;
789
- if (route.params !== undefined)
790
- schema.params = route.params;
791
- if (route.query !== undefined)
792
- schema.query = route.query;
793
- if (route.response !== undefined)
794
- schema.response = route.response;
1645
+ const schema = toElysiaRouteSchema(route);
795
1646
  const handler = async (ctx) => {
796
1647
  const requestContext = ctx.requestContext ?? await ctxFactory(ctx.request);
797
- const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
798
- if (requestScope && compiled.destroyRequestScope)
799
- requestScopes.set(ctx.request, requestScope);
800
- const source = controller.scope === "request" ? requestScope : services;
801
- const instance = controllerInstance(source?.[controller.serviceKey]);
802
- const method = instance?.[route.handler];
803
- if (typeof method !== "function") {
804
- throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
805
- }
806
- const input = {
807
- body: ctx.body,
808
- params: ctx.params,
809
- query: ctx.query,
810
- headers: Object.fromEntries(ctx.request.headers.entries()),
811
- context: requestContext,
812
- request: ctx.request,
813
- scope: requestScope,
814
- requestContext
815
- };
816
- const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
817
- const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
818
- kind: route.command ? "command" : "route",
819
- operation: route.command ?? `${route.method} ${path}`,
820
- stage: "handler",
821
- requestId: executionRequestId(requestContext)
822
- }, handlerCall));
823
- const routeContext = {
824
- kind: "route",
825
- name: `${route.method} ${path}`,
826
- input,
827
- request: ctx.request,
828
- requestContext,
829
- scope: requestScope,
830
- services,
831
- metadata: route
832
- };
833
- const command = route.command ? commandsByClassName.get(route.command) : undefined;
834
- const commandContext = {
835
- kind: "command",
836
- name: command?.name ?? route.command ?? `${route.method} ${path}`,
837
- input,
838
- request: ctx.request,
839
- requestContext,
840
- scope: requestScope,
841
- services,
842
- metadata: command ?? route
843
- };
844
- const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
845
- const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
846
- const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
847
- const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
848
- const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
849
- const invokeCommand = () => {
850
- if (!route.command)
851
- return invoke();
852
- if (!command) {
853
- throw new ApplicationError(`Command "${route.command}" is not registered`, {
854
- code: "COMMAND_NOT_REGISTERED"
855
- });
1648
+ const execute = async () => {
1649
+ const requestScope = createRequestScope ? await createRequestScope(services, requestContext, imported) : undefined;
1650
+ if (requestScope && compiled.destroyRequestScope)
1651
+ requestScopes.set(ctx.request, requestScope);
1652
+ const source = controller.scope === "request" ? requestScope : services;
1653
+ const instance = controllerInstance(source?.[controller.serviceKey]);
1654
+ const method = instance?.[route.handler];
1655
+ if (typeof method !== "function") {
1656
+ throw new Error(`supacloud: controller "${controller.serviceKey}" has no handler "${route.handler}" in scope "${controller.scope}"`);
856
1657
  }
857
- if (!commandExecutor) {
858
- throw new ApplicationError(`Command "${command.name}" has no executor`, {
859
- status: 501,
860
- code: "COMMAND_EXECUTOR_UNAVAILABLE"
861
- });
862
- }
863
- const invocation = {
864
- command,
1658
+ const normalizedCookie = ctx.cookie === undefined ? undefined : cookieValues(ctx.cookie);
1659
+ const input = {
1660
+ body: ctx.body,
1661
+ params: ctx.params,
1662
+ query: ctx.query,
1663
+ headers: ctx.headers ?? Object.fromEntries(ctx.request.headers.entries()),
1664
+ ...normalizedCookie === undefined ? {} : { cookie: normalizedCookie },
1665
+ context: requestContext,
1666
+ request: ctx.request,
1667
+ ...requestScope ? { scope: requestScope } : {},
1668
+ requestContext
1669
+ };
1670
+ const handlerCall = () => route.invoker ? route.invoker(instance, input) : Reflect.apply(method, instance, [input]);
1671
+ const invoke = once(() => route.command && options.commandGovernance ? handlerCall() : observeExecution(options.onExecution, {
1672
+ kind: route.command ? "command" : "route",
1673
+ operation: route.command ?? `${route.method} ${path}`,
1674
+ stage: "handler",
1675
+ ...executionTrace(requestContext)
1676
+ }, handlerCall));
1677
+ const routeContext = {
1678
+ kind: "route",
1679
+ name: `${route.method} ${path}`,
865
1680
  input,
866
1681
  request: ctx.request,
867
1682
  requestContext,
868
- scope: requestScope,
869
- services
1683
+ ...requestScope ? { scope: requestScope } : {},
1684
+ services,
1685
+ metadata: route
870
1686
  };
871
- return observeExecution(options.onExecution, {
1687
+ const command = route.command ? commandsByClassName.get(route.command) : undefined;
1688
+ const commandContext = {
872
1689
  kind: "command",
873
- operation: command.name,
874
- stage: "commandExecutor",
875
- requestId: executionRequestId(requestContext)
876
- }, () => commandExecutor(invocation, invoke));
1690
+ name: command?.name ?? route.command ?? `${route.method} ${path}`,
1691
+ input,
1692
+ request: ctx.request,
1693
+ requestContext,
1694
+ ...requestScope ? { scope: requestScope } : {},
1695
+ services,
1696
+ metadata: command ?? route
1697
+ };
1698
+ const commandAspects = route.command ? commandsByClassName.get(route.command)?.aspects ?? [] : [];
1699
+ const routePipeline = observedAspects(route.aspects ?? [], "route", options.onExecution);
1700
+ const commandPipeline = observedAspects(commandAspects, "command", options.onExecution);
1701
+ const modulePipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, options.onExecution);
1702
+ const invokeRoute = () => routePipeline(routeContext, () => route.command ? commandPipeline(commandContext, () => invokeCommand()) : invoke());
1703
+ const invokeCommand = () => {
1704
+ if (!route.command)
1705
+ return invoke();
1706
+ if (!command) {
1707
+ throw new ApplicationError(`Command "${route.command}" is not registered`, {
1708
+ code: "COMMAND_NOT_REGISTERED"
1709
+ });
1710
+ }
1711
+ if (!commandExecutor) {
1712
+ throw new ApplicationError(`Command "${command.name}" has no executor`, {
1713
+ status: 501,
1714
+ code: "COMMAND_EXECUTOR_UNAVAILABLE"
1715
+ });
1716
+ }
1717
+ const invocation = {
1718
+ command,
1719
+ input,
1720
+ request: ctx.request,
1721
+ requestContext,
1722
+ ...requestScope ? { scope: requestScope } : {},
1723
+ services
1724
+ };
1725
+ return observeExecution(options.onExecution, {
1726
+ kind: "command",
1727
+ operation: command.name,
1728
+ stage: "commandExecutor",
1729
+ ...executionTrace(requestContext)
1730
+ }, () => commandExecutor(invocation, invoke));
1731
+ };
1732
+ return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
877
1733
  };
878
- return modulePipeline(route.command ? commandContext : routeContext, invokeRoute);
1734
+ const result = options.injector ? await runInRequestContext(options.injector, [provideToken(REQUEST_CONTEXT, requestContext)], execute) : await execute();
1735
+ assertDeclaredResponseStatus(route, result, ctx.set.status);
1736
+ return result;
879
1737
  };
880
1738
  switch (route.method) {
881
1739
  case "GET":
@@ -905,6 +1763,7 @@ function createModulePlugin(compiled, services, ctxFactory = defaultRequestConte
905
1763
  return plugin;
906
1764
  }
907
1765
  async function executeJob(compiled, services, job, input, requestContext, imported = {}, executor, observer) {
1766
+ const decodedInput = decodeJobContract(job.input, input, "input");
908
1767
  const jobScope = job.scope === "job" && compiled.createJobScope ? await compiled.createJobScope(services, requestContext, imported) : undefined;
909
1768
  try {
910
1769
  const source = job.scope === "job" ? jobScope : services;
@@ -915,17 +1774,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
915
1774
  }
916
1775
  const invocation = {
917
1776
  job,
918
- input,
1777
+ input: decodedInput,
919
1778
  requestContext,
920
- scope: jobScope,
1779
+ ...jobScope ? { scope: jobScope } : {},
921
1780
  services
922
1781
  };
923
1782
  const context = {
924
1783
  kind: "job",
925
1784
  name: job.name,
926
- input,
1785
+ input: decodedInput,
927
1786
  requestContext,
928
- scope: jobScope,
1787
+ ...jobScope ? { scope: jobScope } : {},
929
1788
  services,
930
1789
  metadata: job
931
1790
  };
@@ -933,16 +1792,17 @@ async function executeJob(compiled, services, job, input, requestContext, import
933
1792
  kind: "job",
934
1793
  operation: job.name,
935
1794
  stage: "handler",
936
- requestId: executionRequestId(requestContext)
937
- }, () => Reflect.apply(method, instance, [input])));
1795
+ ...executionTrace(requestContext)
1796
+ }, () => Reflect.apply(method, instance, [decodedInput])));
938
1797
  const pipeline = observedAspects(compiled.aspects ?? [], `module:${compiled.name}`, observer);
939
1798
  const jobPipeline = observedAspects(job.aspects ?? [], "job", observer);
940
- return await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
1799
+ const result = await pipeline(context, () => jobPipeline(context, () => observeExecution(observer, {
941
1800
  kind: "job",
942
1801
  operation: job.name,
943
1802
  stage: "jobExecutor",
944
- requestId: executionRequestId(requestContext)
1803
+ ...executionTrace(requestContext)
945
1804
  }, executor ? () => executor(invocation, invoke) : invoke)));
1805
+ return decodeJobContract(job.output, result, "output");
946
1806
  } finally {
947
1807
  if (jobScope && compiled.destroyJobScope) {
948
1808
  await compiled.destroyJobScope(jobScope);
@@ -950,6 +1810,11 @@ async function executeJob(compiled, services, job, input, requestContext, import
950
1810
  }
951
1811
  }
952
1812
  function defaultErrorResponse(error, frameworkCode) {
1813
+ if (error instanceof CommandError2) {
1814
+ return Response.json({ ok: false, code: error.code, message: error.code }, {
1815
+ status: commandErrorStatus(error.code)
1816
+ });
1817
+ }
953
1818
  if (isPublicApplicationError(error)) {
954
1819
  return Response.json({
955
1820
  ok: false,
@@ -984,7 +1849,9 @@ function isPublicApplicationError(error) {
984
1849
  return error.expose === true && typeof error.status === "number" && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 && typeof error.code === "string" && error.code.length > 0;
985
1850
  }
986
1851
  function createApplication(options) {
987
- const app = new Elysia({ name: options.name ?? "supacloud:app" });
1852
+ const app = new Elysia2({ name: options.name ?? "supacloud:app", normalize: options.normalize ?? true });
1853
+ if (options.documentation)
1854
+ app.use(createDocumentationPlugin(options.documentation));
988
1855
  const configuredContextFactory = options.requestContext ?? defaultRequestContext;
989
1856
  const contextCache = new WeakMap;
990
1857
  const ctxFactory = (request) => {
@@ -999,12 +1866,7 @@ function createApplication(options) {
999
1866
  for (const module of options.modules ?? []) {
1000
1867
  const services = module.createServices(options.deps ?? {}, imported);
1001
1868
  imported[module.name] = services;
1002
- app.use(createModulePlugin(module, services, ctxFactory, {
1003
- commandGovernance: options.commandGovernance,
1004
- commandExecutor: options.commandExecutor,
1005
- errorMapper: options.errorMapper,
1006
- onExecution: options.onExecution
1007
- }, imported));
1869
+ app.use(createModulePlugin(module, services, ctxFactory, options, imported));
1008
1870
  }
1009
1871
  return app;
1010
1872
  }
@@ -1015,22 +1877,41 @@ export {
1015
1877
  ApplicationError,
1016
1878
  EXECUTION_ID_HEADER,
1017
1879
  IDEMPOTENCY_KEY_HEADER,
1880
+ SchemaContractError,
1881
+ SupaCloudWorker,
1018
1882
  VERIFIED_JWT_SUBJECT_HEADER,
1883
+ WorkerReceiptUnconfirmedError,
1884
+ WorkerRegistrationError,
1019
1885
  assertFeatureTransition,
1886
+ assertResponseStatusDeclared,
1020
1887
  composeAspects,
1021
1888
  composeCommandExecutors,
1022
1889
  createApplication,
1023
1890
  createCommandExecutor,
1891
+ createDocumentationPlugin,
1024
1892
  createMemoryPolicy,
1025
1893
  createMemorySandbox,
1026
1894
  createModulePlugin,
1895
+ createPersistentCommandAdapter,
1896
+ createQueueWorkerTransport,
1897
+ createSchemaDecoder,
1027
1898
  createSupAuthRequestContext,
1028
1899
  createSupaCloudRequestContext,
1029
1900
  createTestApp,
1901
+ createWorker,
1030
1902
  defaultErrorResponse,
1903
+ defineElysiaRoute,
1904
+ defineJsonContract,
1905
+ defineRouteContract,
1031
1906
  executeCompiledCommand,
1032
1907
  executeJob,
1908
+ previewCompiledCommand,
1909
+ registerElysiaRoute,
1033
1910
  requireIdempotencyKey,
1034
1911
  requireTrustedIdentity,
1912
+ responseSchemaForStatus,
1913
+ responseStatusDeclared,
1914
+ responseStatusOf,
1915
+ toElysiaRouteSchema,
1035
1916
  validatedJsonResponse
1036
1917
  };