@supacloud/elysia 0.20.2 → 0.22.0

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/NOTICE CHANGED
@@ -1,22 +1,17 @@
1
1
  SupaCloud
2
2
  Copyright 2025 SupaCloud Contributors
3
3
 
4
- SupaCloud project-owned code in this revision is licensed under the GNU
5
- Affero General Public License, version 3 only (AGPL-3.0-only). The complete
6
- license is provided in LICENSE. This is not an "or later" grant.
4
+ SupaCloud project-owned code in this revision is licensed under the Apache
5
+ License, Version 2.0 (Apache-2.0). The complete license is provided in
6
+ LICENSE.
7
7
 
8
8
  This program is distributed without any warranty; without even the implied
9
9
  warranty of merchantability or fitness for a particular purpose.
10
10
 
11
11
  Third-party components, incorporated upstream code, and their copyright,
12
- license and attribution notices retain their original terms. This notice
13
- does not replace those terms or claim ownership of third-party work.
12
+ license and attribution notices retain their original terms. This notice does
13
+ not replace those terms or claim ownership of third-party work.
14
14
 
15
- Earlier SupaCloud code was distributed under Apache-2.0 or MIT as indicated
16
- in its original files and package metadata. Previously granted permissions
17
- are not revoked by this change. The historical Apache-2.0 text is preserved
18
- in LICENSE-APACHE-2.0.txt. Retain any applicable upstream MIT notices as well.
19
-
20
- Before publishing this licensing change, maintainers must confirm authority
21
- to license the affected contributions and compatibility of incorporated
22
- third-party code. This notice is not evidence of contributor consent.
15
+ Previously released SupaCloud versions retain the permissions granted with
16
+ those versions. See the historical license files and release notices where
17
+ applicable.
package/README.md CHANGED
@@ -562,9 +562,32 @@ with those statuses. The default content type is
562
562
  is for bounded JSON payloads, not files or streams. Existing native `Response`
563
563
  passthrough is unchanged.
564
564
 
565
- ### `createApplication(options: ApplicationOptions): Elysia`
565
+ ### `createApplication(options: ApplicationOptions): Elysia & ApplicationLifecycle`
566
566
 
567
- Creates the root Elysia application from compiled modules.
567
+ Creates the root Elysia application from compiled modules. The returned app
568
+ also exposes explicit `initialize()` and `destroy()` controls for
569
+ application-scoped resources. Initialization runs compiled modules in their
570
+ topological order; destruction runs in reverse order and is idempotent. A
571
+ startup failure triggers best-effort cleanup of all modules that were created,
572
+ and shutdown continues across modules before reporting an `AggregateError`.
573
+
574
+ The controls are intentionally explicit so hosts can await startup before
575
+ accepting traffic and await shutdown from their process signal handler:
576
+
577
+ ```ts
578
+ const app = createApplication({ modules: createCompiledModules() });
579
+ await app.initialize();
580
+ const server = app.listen(3000);
581
+ process.once("SIGTERM", async () => {
582
+ server.stop();
583
+ await app.destroy();
584
+ });
585
+ ```
586
+
587
+ The compiler emits `initializeServices` and `destroyServices` for
588
+ application-scoped providers and controllers that declare `onInit()` or
589
+ `onDestroy()`/`ngOnDestroy()`. Borrowed `useExisting` providers are never
590
+ destroyed by the owning module.
568
591
 
569
592
  ### `createModulePlugin(compiled, services, ctxFactory?, options?, imported?): Elysia`
570
593
 
@@ -0,0 +1,77 @@
1
+ import { type CompiledModule } from "../index";
2
+ export interface CatalogIdentity {
3
+ tenantId: string;
4
+ subject: string;
5
+ }
6
+ export interface CatalogItem {
7
+ id: string;
8
+ name: string;
9
+ }
10
+ export interface CatalogPorts {
11
+ /** Resolve credentials using a trusted verifier, never a tenant header alone. */
12
+ authenticate(request: Request): Promise<CatalogIdentity>;
13
+ /** The repository must apply this verified identity to its authorization. */
14
+ read(identity: CatalogIdentity, id: string): Promise<CatalogItem>;
15
+ }
16
+ /**
17
+ * A handwritten adapter fixture, not compiler output or a new module format.
18
+ * Real applications can supply the existing compiler's createCompiledModules().
19
+ */
20
+ export declare function createCatalogModule(ports: CatalogPorts): CompiledModule;
21
+ export declare function createCatalogApplication(ports: CatalogPorts): import("elysia/types").AddRoute<"", "local", {
22
+ decorator: {
23
+ clock: () => number;
24
+ };
25
+ store: {};
26
+ derive: {};
27
+ }, {
28
+ typebox: {};
29
+ error: [];
30
+ }, import("elysia").DefaultMetadata & {
31
+ schema: {};
32
+ schemas: {};
33
+ macro: {};
34
+ macroFn: {};
35
+ parser: {};
36
+ response: {};
37
+ }, {}, import("elysia").DefaultEphemeral, {
38
+ derive: {};
39
+ schema: {};
40
+ schemas: {};
41
+ response: {};
42
+ error: [];
43
+ }, "get", "/health", import("elysia/types").IntersectIfObjectSchema<import("elysia").UnwrapRoute<{}, {}, "/health">, import("elysia/types").MergeScopedSchemas<{}, {}, {}>>, {}, ({ clock }: {
44
+ body: unknown;
45
+ query: Record<string, string | undefined>;
46
+ params: {};
47
+ headers: Record<string, string | undefined>;
48
+ cookie: Record<string, import("elysia").Cookie<unknown>>;
49
+ server: import("elysia").Server | null;
50
+ redirect: import("elysia").redirect;
51
+ set: {
52
+ headers: import("elysia").HTTPHeaders;
53
+ status?: number | keyof import("elysia").StatusMap;
54
+ cookie?: Record<string, import("elysia").BaseCookie>;
55
+ redirect?: never;
56
+ };
57
+ status: <const Code extends number | keyof import("elysia").StatusMap, const T = import("elysia").StatusResponse<Code>>(code: Code, response?: T) => import("elysia").ElysiaStatus<Code, T, import("elysia").NumericStatus<Code>>;
58
+ problem: typeof import("elysia").problem;
59
+ defer: (fn: import("elysia/types").AfterResponseHandler<NoInfer<import("elysia/types").IntersectIfObjectSchema<import("elysia").UnwrapRoute<{}, {}, "/health">, import("elysia/types").MergeScopedSchemas<{}, {}, {}>>>, NoInfer<{
60
+ decorator: {
61
+ clock: () => number;
62
+ };
63
+ store: {};
64
+ derive: {};
65
+ } & {
66
+ derive: {};
67
+ }>>) => import("elysia/types").MaybePromise<void>;
68
+ readonly path: string;
69
+ route?: string | undefined;
70
+ rid?: string | undefined;
71
+ request: Request;
72
+ store: {};
73
+ clock: () => number;
74
+ }) => {
75
+ ok: boolean;
76
+ time: number;
77
+ }>;
@@ -1,13 +1,2 @@
1
- export interface ExecutionEvent {
2
- kind: "route" | "command" | "job";
3
- operation: string;
4
- stage: string;
5
- phase: "started" | "succeeded" | "failed";
6
- requestId?: string;
7
- durationMs?: number;
8
- }
9
- /** Metadata only: never receives request bodies, credentials, results or errors. */
10
- export type ExecutionObserver = (event: Readonly<ExecutionEvent>) => void | Promise<void>;
11
- export declare function observeExecution<T>(observer: ExecutionObserver | undefined, event: Pick<ExecutionEvent, "kind" | "operation" | "stage" | "requestId">, next: () => T | Promise<T>): Promise<T>;
12
- export declare function executionRequestId(context: unknown): string | undefined;
13
- export declare function executionTrace(context: unknown): Pick<ExecutionEvent, "requestId">;
1
+ export { executionRequestId, executionTrace, observeExecution, } from "@supacloud/app/execution";
2
+ export type { ExecutionEvent, ExecutionObserver } from "@supacloud/app/execution";
@@ -0,0 +1,39 @@
1
+ import type { AnyElysia, Elysia } from "elysia";
2
+ import { type ApplicationOptions, type CompiledModule } from "./index";
3
+ /**
4
+ * Experimental profile for evaluating SupaCloud application composition.
5
+ *
6
+ * This intentionally remains a thin profile over createApplication. It owns
7
+ * SupaCloud defaults without wrapping or reimplementing Elysia's API.
8
+ */
9
+ export interface SupaCloudFrameworkOptions<Http extends AnyElysia = Elysia, RequestContext = unknown> extends Omit<ApplicationOptions<Http, RequestContext>, "name" | "normalize" | "modules"> {
10
+ /** Application modules in compiler-emitted topological order. */
11
+ modules?: CompiledModule[];
12
+ /** Public application name used in diagnostics and Elysia plugin identity. */
13
+ name: string;
14
+ /**
15
+ * Reject extra properties rather than silently removing them.
16
+ */
17
+ normalize?: false;
18
+ }
19
+ export declare function createSupaCloudFramework<const Http extends AnyElysia = Elysia, RequestContext = unknown>(options: SupaCloudFrameworkOptions<Http, RequestContext>): Elysia<"", "local", {
20
+ decorator: {} & Http["~Singleton"]["decorator"];
21
+ store: ({} & Http["~Singleton"]["store"] extends infer T_1 ? { [K_1 in keyof T_1]: T_1[K_1]; } : never) extends infer T ? { [K in keyof T]: T[K]; } : never;
22
+ derive: {} & Http["~Singleton"]["derive"] & Http["~Ephemeral"]["derive"];
23
+ }, {
24
+ typebox: {} & Http["~Definitions"]["typebox"];
25
+ error: [...Http["~Definitions"]["error"], ...Http["~Ephemeral"]["error"]];
26
+ }, import("elysia").DefaultMetadata & {
27
+ schema: import("elysia/types").MergeSchema<import("elysia/types").MergeSchema<{} & Http["~Ephemeral"]["schema"], {}, "", false>, (import("elysia").DefaultMetadata & Http["~Metadata"])["schema"], "", false>;
28
+ schemas: (import("elysia").DefaultMetadata & Http["~Metadata"])["schemas"] & {} & Http["~Ephemeral"]["schemas"];
29
+ macro: (import("elysia").DefaultMetadata & Http["~Metadata"])["macro"];
30
+ macroFn: (import("elysia").DefaultMetadata & Http["~Metadata"])["macroFn"];
31
+ parser: (import("elysia").DefaultMetadata & Http["~Metadata"])["parser"];
32
+ response: import("elysia/types").UnionResponseStatus<(import("elysia").DefaultMetadata & Http["~Metadata"])["response"], import("elysia/types").UnionResponseStatus<{} & Http["~Ephemeral"]["response"], {}>>;
33
+ }, import("elysia/types").ResolveRouteErrors<{}, [...Http["~Definitions"]["error"], ...Http["~Ephemeral"]["error"]]> & Http["~Routes"], import("elysia").DefaultEphemeral, {
34
+ derive: {};
35
+ schema: {};
36
+ schemas: {};
37
+ response: {};
38
+ error: [];
39
+ }> & import("./index").ApplicationLifecycle;
package/dist/index.d.ts CHANGED
@@ -22,6 +22,8 @@ export type { ElysiaRouteContext, ElysiaRouteDefinition, ElysiaRouteHandler, Ely
22
22
  export { createPersistentCommandAdapter, type PersistentCommandHandler } from "./persistent-command";
23
23
  export { createDocumentationPlugin } from "./documentation";
24
24
  export type { ApplicationDocumentationOptions, DocumentationSource, GraphqlDocumentationOptions, OpenApiDocumentationOptions, } from "./documentation";
25
+ export { createSupaCloudFramework } from "./framework";
26
+ export type { SupaCloudFrameworkOptions } from "./framework";
25
27
  export interface CompiledRoute {
26
28
  parse?: "none";
27
29
  allowDeleteBody?: true;
@@ -106,6 +108,10 @@ export interface CompiledController {
106
108
  export interface CompiledModule {
107
109
  name: string;
108
110
  createServices(deps: Record<string, unknown>, imported: Record<string, Record<string, unknown>>): Record<string, unknown>;
111
+ /** Initializes application-scoped instances owned by this module. */
112
+ initializeServices?(services: Record<string, unknown>): Promise<void>;
113
+ /** Destroys application-scoped instances owned by this module. */
114
+ destroyServices?(services: Record<string, unknown>): Promise<void>;
109
115
  createRequestScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Record<string, unknown> | Promise<Record<string, unknown>>;
110
116
  destroyRequestScope?(scope: Record<string, unknown>): Promise<void>;
111
117
  createJobScope?(services: Record<string, unknown>, ctx: unknown, imported?: Record<string, Record<string, unknown>>): Record<string, unknown> | Promise<Record<string, unknown>>;
@@ -267,6 +273,13 @@ export interface ApplicationOptions<Http extends AnyElysia = Elysia, RequestCont
267
273
  /** Optional read-only OpenAPI and GraphQL documentation endpoints. */
268
274
  documentation?: ApplicationDocumentationOptions;
269
275
  }
276
+ /** Lifecycle controls exposed by createApplication for explicit host startup/shutdown. */
277
+ export interface ApplicationLifecycle {
278
+ readonly initialized: boolean;
279
+ readonly destroyed: boolean;
280
+ initialize(): Promise<void>;
281
+ destroy(): Promise<void>;
282
+ }
270
283
  export interface JobInvocation {
271
284
  job: CompiledJob;
272
285
  input: unknown;
@@ -392,7 +405,7 @@ export declare function createApplication<const Http extends AnyElysia = Elysia>
392
405
  schemas: {};
393
406
  response: {};
394
407
  error: [];
395
- }>;
408
+ }> & ApplicationLifecycle;
396
409
  /** Semantic alias of createApplication for readable tests. */
397
410
  export declare function createTestApp<const Http extends AnyElysia = Elysia>(options: ApplicationOptions<Http>): Elysia<"", "local", {
398
411
  decorator: {} & Http["~Singleton"]["decorator"];
@@ -414,7 +427,7 @@ export declare function createTestApp<const Http extends AnyElysia = Elysia>(opt
414
427
  schemas: {};
415
428
  response: {};
416
429
  error: [];
417
- }>;
430
+ }> & ApplicationLifecycle;
418
431
  export { createMemorySandbox, } from "./memory";
419
432
  export type { HandleLike as MemoryHandleLike, MemoryDatabase, MemorySandbox, MemorySandboxOptions, MemoryStorage, MemoryStorageObject, } from "./memory";
420
433
  export { createMemoryPolicy } from "./memory_policy";
package/dist/index.js CHANGED
@@ -662,39 +662,11 @@ function createPostgresHttpPolicyStores(database) {
662
662
  };
663
663
  }
664
664
  // src/execution.ts
665
- async function observeExecution(observer, event, next) {
666
- const emit = (phase, durationMs) => {
667
- if (!observer)
668
- return;
669
- try {
670
- Promise.resolve(observer(Object.freeze({
671
- ...event,
672
- phase,
673
- ...durationMs === undefined ? {} : { durationMs }
674
- }))).catch(() => {});
675
- } catch {}
676
- };
677
- const started = performance.now();
678
- emit("started");
679
- try {
680
- const result = await next();
681
- emit("succeeded", performance.now() - started);
682
- return result;
683
- } catch (error) {
684
- emit("failed", performance.now() - started);
685
- throw error;
686
- }
687
- }
688
- function executionRequestId(context) {
689
- if (!context || typeof context !== "object" || !("requestId" in context))
690
- return;
691
- const value = context.requestId;
692
- return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/.test(value) ? value : undefined;
693
- }
694
- function executionTrace(context) {
695
- const requestId = executionRequestId(context);
696
- return requestId === undefined ? {} : { requestId };
697
- }
665
+ import {
666
+ executionRequestId,
667
+ executionTrace,
668
+ observeExecution
669
+ } from "@supacloud/app/execution";
698
670
 
699
671
  // src/schema_contract.ts
700
672
  import {
@@ -1099,6 +1071,14 @@ function createPersistentCommandAdapter(command, options) {
1099
1071
  }
1100
1072
  };
1101
1073
  }
1074
+ // src/framework.ts
1075
+ function createSupaCloudFramework(options) {
1076
+ return createApplication({
1077
+ ...options,
1078
+ normalize: options.normalize ?? false,
1079
+ modules: options.modules ?? []
1080
+ });
1081
+ }
1102
1082
  // src/feature.ts
1103
1083
  function assertFeatureTransition(spec, state, event) {
1104
1084
  const transition = Object.hasOwn(spec.transitions, event) ? spec.transitions[event] : undefined;
@@ -2716,11 +2696,72 @@ function createApplication(options) {
2716
2696
  return pending;
2717
2697
  };
2718
2698
  const imported = {};
2699
+ const moduleServices = [];
2719
2700
  for (const module of options.modules ?? []) {
2720
2701
  const services = module.createServices(options.deps ?? {}, imported);
2721
2702
  imported[module.name] = services;
2703
+ moduleServices.push({ module, services });
2722
2704
  app.use(createModulePlugin(module, services, ctxFactory, { ...options, http: undefined }, imported));
2723
2705
  }
2706
+ let initialized = false;
2707
+ let destroyed = false;
2708
+ let initializationPromise;
2709
+ let destructionPromise;
2710
+ const lifecycle = {
2711
+ get initialized() {
2712
+ return initialized;
2713
+ },
2714
+ get destroyed() {
2715
+ return destroyed;
2716
+ },
2717
+ initialize() {
2718
+ if (destroyed)
2719
+ return Promise.reject(new Error("Application has already been destroyed"));
2720
+ if (initializationPromise)
2721
+ return initializationPromise;
2722
+ initializationPromise = (async () => {
2723
+ try {
2724
+ for (const { module, services } of moduleServices) {
2725
+ await module.initializeServices?.(services);
2726
+ }
2727
+ initialized = true;
2728
+ } catch (error) {
2729
+ try {
2730
+ await lifecycle.destroy();
2731
+ } catch (cleanupError) {
2732
+ throw new AggregateError([error, cleanupError], "Application initialization failed and cleanup failed");
2733
+ }
2734
+ throw error;
2735
+ }
2736
+ })();
2737
+ return initializationPromise;
2738
+ },
2739
+ destroy() {
2740
+ if (destructionPromise)
2741
+ return destructionPromise;
2742
+ destroyed = true;
2743
+ destructionPromise = (async () => {
2744
+ const errors = [];
2745
+ for (const { module, services } of [...moduleServices].reverse()) {
2746
+ try {
2747
+ await module.destroyServices?.(services);
2748
+ } catch (error) {
2749
+ errors.push(error);
2750
+ }
2751
+ }
2752
+ if (errors.length > 0) {
2753
+ throw new AggregateError(errors, "Application destruction failed");
2754
+ }
2755
+ })();
2756
+ return destructionPromise;
2757
+ }
2758
+ };
2759
+ Object.defineProperties(app, {
2760
+ initialized: { enumerable: true, configurable: false, get: () => lifecycle.initialized },
2761
+ destroyed: { enumerable: true, configurable: false, get: () => lifecycle.destroyed },
2762
+ initialize: { enumerable: true, configurable: false, value: lifecycle.initialize },
2763
+ destroy: { enumerable: true, configurable: false, value: lifecycle.destroy }
2764
+ });
2724
2765
  return app;
2725
2766
  }
2726
2767
  function createTestApp(options) {
@@ -2758,6 +2799,7 @@ export {
2758
2799
  createQueueWorkerTransport,
2759
2800
  createSchemaDecoder,
2760
2801
  createSupAuthRequestContext,
2802
+ createSupaCloudFramework,
2761
2803
  createSupaCloudRequestContext,
2762
2804
  createTestApp,
2763
2805
  createWorker,
@@ -38,6 +38,6 @@ export declare function createWebhookMigrationExample(options: {
38
38
  schemas: {};
39
39
  response: {};
40
40
  error: [];
41
- }>;
41
+ }> & import("./index").ApplicationLifecycle;
42
42
  modules: import("./fixtures/webhook-generated/application").CompiledApplicationModule[];
43
43
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supacloud/elysia",
3
- "version": "0.20.2",
3
+ "version": "0.22.0",
4
4
  "description": "Elysia runtime adapter for SupaCloud compiled modules: application/request scopes, route registration and validation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "clean": "rm -rf dist",
29
29
  "prepublishOnly": "bun run build",
30
30
  "test": "bun test",
31
- "test:conformance": "bun test src/conformance.test.ts",
31
+ "test:conformance": "bun test src/conformance.test.ts src/application-execution.test.ts ../app/src/execution.test.ts ../app/src/execution_bundle.test.ts",
32
32
  "test:runtime-safety": "SUPACLOUD_REQUIRE_RUNTIME_SAFETY=1 bun test src/runtime-safety.test.ts",
33
33
  "test:contract-upgrade": "bun test src/contract-upgrade.test.ts",
34
34
  "bench:http-policy": "bun bench/http-policy.ts",
@@ -42,32 +42,32 @@
42
42
  "dependency-injection",
43
43
  "module"
44
44
  ],
45
- "license": "AGPL-3.0-only",
45
+ "license": "Apache-2.0",
46
46
  "repository": {
47
47
  "type": "git",
48
48
  "url": "https://github.com/vibeunion/supacloud.git",
49
49
  "directory": "packages/elysia"
50
50
  },
51
51
  "dependencies": {
52
- "@supacloud/app": "0.18.1",
53
- "@supacloud/contracts": "0.5.1",
52
+ "@supacloud/app": "0.20.0",
53
+ "@supacloud/contracts": "0.7.0",
54
54
  "exact-mirror": "1.2.6",
55
55
  "jose": "^6.2.11",
56
56
  "typebox": "1.3.34"
57
57
  },
58
58
  "overrides": {
59
- "@supacloud/delivery": "0.2.1",
60
- "@supacloud/contracts": "0.5.1",
61
- "@supacloud/commands": "0.7.1"
59
+ "@supacloud/delivery": "0.4.0",
60
+ "@supacloud/contracts": "0.7.0",
61
+ "@supacloud/commands": "0.9.0"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "elysia": "2.0.0-beta.19"
65
65
  },
66
66
  "devDependencies": {
67
- "@supacloud/compiler": "0.27.4",
68
- "@supacloud/delivery": "0.2.1",
69
- "@supacloud/commands": "0.7.1",
70
- "@supacloud/db": "0.9.1",
67
+ "@supacloud/compiler": "0.29.0",
68
+ "@supacloud/delivery": "0.4.0",
69
+ "@supacloud/commands": "0.9.0",
70
+ "@supacloud/db": "0.11.0",
71
71
  "@types/bun": "^1.4.2",
72
72
  "elysia": "2.0.0-beta.19",
73
73
  "typescript": "^7.0.2"