@zmdb/app 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 (108) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +27 -0
  3. package/dist/application.d.ts +45 -0
  4. package/dist/application.d.ts.map +1 -0
  5. package/dist/application.js +186 -0
  6. package/dist/application.js.map +1 -0
  7. package/dist/commands/index.d.ts +31 -0
  8. package/dist/commands/index.d.ts.map +1 -0
  9. package/dist/commands/index.js +338 -0
  10. package/dist/commands/index.js.map +1 -0
  11. package/dist/cqrs/index.d.ts +47 -0
  12. package/dist/cqrs/index.d.ts.map +1 -0
  13. package/dist/cqrs/index.js +76 -0
  14. package/dist/cqrs/index.js.map +1 -0
  15. package/dist/data/index.d.ts +12 -0
  16. package/dist/data/index.d.ts.map +1 -0
  17. package/dist/data/index.js +25 -0
  18. package/dist/data/index.js.map +1 -0
  19. package/dist/di/index.d.ts +44 -0
  20. package/dist/di/index.d.ts.map +1 -0
  21. package/dist/di/index.js +136 -0
  22. package/dist/di/index.js.map +1 -0
  23. package/dist/events/index.d.ts +46 -0
  24. package/dist/events/index.d.ts.map +1 -0
  25. package/dist/events/index.js +181 -0
  26. package/dist/events/index.js.map +1 -0
  27. package/dist/health/index.d.ts +35 -0
  28. package/dist/health/index.d.ts.map +1 -0
  29. package/dist/health/index.js +14 -0
  30. package/dist/health/index.js.map +1 -0
  31. package/dist/index.d.ts +11 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +22 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/lifecycle.d.ts +21 -0
  36. package/dist/lifecycle.d.ts.map +1 -0
  37. package/dist/lifecycle.js +52 -0
  38. package/dist/lifecycle.js.map +1 -0
  39. package/dist/messaging/index.d.ts +176 -0
  40. package/dist/messaging/index.d.ts.map +1 -0
  41. package/dist/messaging/index.js +442 -0
  42. package/dist/messaging/index.js.map +1 -0
  43. package/dist/messaging/transport-kit.d.ts +22 -0
  44. package/dist/messaging/transport-kit.d.ts.map +1 -0
  45. package/dist/messaging/transport-kit.js +174 -0
  46. package/dist/messaging/transport-kit.js.map +1 -0
  47. package/dist/modules/index.d.ts +52 -0
  48. package/dist/modules/index.d.ts.map +1 -0
  49. package/dist/modules/index.js +351 -0
  50. package/dist/modules/index.js.map +1 -0
  51. package/dist/modules/lifecycle-instances.d.ts +4 -0
  52. package/dist/modules/lifecycle-instances.d.ts.map +1 -0
  53. package/dist/modules/lifecycle-instances.js +19 -0
  54. package/dist/modules/lifecycle-instances.js.map +1 -0
  55. package/dist/modules/runtime.d.ts +18 -0
  56. package/dist/modules/runtime.d.ts.map +1 -0
  57. package/dist/modules/runtime.js +8 -0
  58. package/dist/modules/runtime.js.map +1 -0
  59. package/dist/observability/index.d.ts +27 -0
  60. package/dist/observability/index.d.ts.map +1 -0
  61. package/dist/observability/index.js +145 -0
  62. package/dist/observability/index.js.map +1 -0
  63. package/dist/observability/propagation.d.ts +10 -0
  64. package/dist/observability/propagation.d.ts.map +1 -0
  65. package/dist/observability/propagation.js +96 -0
  66. package/dist/observability/propagation.js.map +1 -0
  67. package/dist/observability/types.d.ts +66 -0
  68. package/dist/observability/types.d.ts.map +1 -0
  69. package/dist/observability/types.js +10 -0
  70. package/dist/observability/types.js.map +1 -0
  71. package/dist/polyfill.d.ts +2 -0
  72. package/dist/polyfill.d.ts.map +1 -0
  73. package/dist/polyfill.js +21 -0
  74. package/dist/polyfill.js.map +1 -0
  75. package/dist/state/fixtures.d.ts +10 -0
  76. package/dist/state/fixtures.d.ts.map +1 -0
  77. package/dist/state/fixtures.js +14 -0
  78. package/dist/state/fixtures.js.map +1 -0
  79. package/dist/state/index.d.ts +26 -0
  80. package/dist/state/index.d.ts.map +1 -0
  81. package/dist/state/index.js +37 -0
  82. package/dist/state/index.js.map +1 -0
  83. package/dist/state-transitions.d.ts +57 -0
  84. package/dist/state-transitions.d.ts.map +1 -0
  85. package/dist/state-transitions.js +31 -0
  86. package/dist/state-transitions.js.map +1 -0
  87. package/package.json +99 -0
  88. package/src/application.ts +243 -0
  89. package/src/commands/index.ts +447 -0
  90. package/src/cqrs/index.ts +124 -0
  91. package/src/data/index.ts +33 -0
  92. package/src/di/index.ts +179 -0
  93. package/src/events/index.ts +248 -0
  94. package/src/health/index.ts +54 -0
  95. package/src/index.ts +68 -0
  96. package/src/lifecycle.ts +62 -0
  97. package/src/messaging/index.ts +668 -0
  98. package/src/messaging/transport-kit.ts +234 -0
  99. package/src/modules/index.ts +476 -0
  100. package/src/modules/lifecycle-instances.ts +23 -0
  101. package/src/modules/runtime.ts +26 -0
  102. package/src/observability/index.ts +204 -0
  103. package/src/observability/propagation.ts +107 -0
  104. package/src/observability/types.ts +68 -0
  105. package/src/polyfill.ts +24 -0
  106. package/src/state/fixtures.ts +20 -0
  107. package/src/state/index.ts +57 -0
  108. package/src/state-transitions.ts +150 -0
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@zmdb/app",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Protocol-neutral application kernel for zmdb: Stage-3 metadata, dependency injection, modules, lifecycle, messaging, commands, events, CQRS, state, health, and observability.",
5
+ "keywords": [
6
+ "application-framework",
7
+ "cqrs",
8
+ "dependency-injection",
9
+ "events",
10
+ "messaging",
11
+ "stage-3",
12
+ "typescript",
13
+ "zmdb"
14
+ ],
15
+ "homepage": "https://github.com/ambasta/zmdb#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/ambasta/zmdb/issues"
18
+ },
19
+ "license": "GPL-3.0-or-later",
20
+ "author": "zmdb contributors",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/ambasta/zmdb.git",
24
+ "directory": "packages/app"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": [
28
+ "./dist/polyfill.js"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./commands": {
36
+ "types": "./dist/commands/index.d.ts",
37
+ "import": "./dist/commands/index.js"
38
+ },
39
+ "./cqrs": {
40
+ "types": "./dist/cqrs/index.d.ts",
41
+ "import": "./dist/cqrs/index.js"
42
+ },
43
+ "./data": {
44
+ "types": "./dist/data/index.d.ts",
45
+ "import": "./dist/data/index.js"
46
+ },
47
+ "./di": {
48
+ "types": "./dist/di/index.d.ts",
49
+ "import": "./dist/di/index.js"
50
+ },
51
+ "./events": {
52
+ "types": "./dist/events/index.d.ts",
53
+ "import": "./dist/events/index.js"
54
+ },
55
+ "./health": {
56
+ "types": "./dist/health/index.d.ts",
57
+ "import": "./dist/health/index.js"
58
+ },
59
+ "./lifecycle": {
60
+ "types": "./dist/lifecycle.d.ts",
61
+ "import": "./dist/lifecycle.js"
62
+ },
63
+ "./messaging": {
64
+ "types": "./dist/messaging/index.d.ts",
65
+ "import": "./dist/messaging/index.js"
66
+ },
67
+ "./modules": {
68
+ "types": "./dist/modules/index.d.ts",
69
+ "import": "./dist/modules/index.js"
70
+ },
71
+ "./observability": {
72
+ "types": "./dist/observability/index.d.ts",
73
+ "import": "./dist/observability/index.js"
74
+ },
75
+ "./state": {
76
+ "types": "./dist/state/index.d.ts",
77
+ "import": "./dist/state/index.js"
78
+ }
79
+ },
80
+ "publishConfig": {
81
+ "access": "public",
82
+ "tag": "beta"
83
+ },
84
+ "scripts": {
85
+ "build": "node ../../scripts/build-package.mjs",
86
+ "test": "vitest run"
87
+ },
88
+ "dependencies": {
89
+ "@zmdb/orm": "1.0.0-beta.1",
90
+ "@zmdb/schema": "1.0.0-beta.1",
91
+ "@zmdb/sql": "1.0.0-beta.1",
92
+ "@zmdb/validator": "1.0.0-beta.1"
93
+ },
94
+ "engines": {
95
+ "node": ">=26"
96
+ },
97
+ "main": "./dist/index.js",
98
+ "types": "./dist/index.d.ts"
99
+ }
@@ -0,0 +1,243 @@
1
+ import type { Container } from './di/index.js';
2
+ import { runInit, runShutdown } from './lifecycle.js';
3
+ import { compileModule, type CompiledModule, type LazyModuleHandle, type ModuleClass } from './modules/index.js';
4
+ import { lifecycleInstances } from './modules/lifecycle-instances.js';
5
+ import { runtimeOf, type CompiledController } from './modules/runtime.js';
6
+ import type { Observability } from './observability/types.js';
7
+
8
+ const APPLICATION_BRIDGE = Symbol.for('@zmdb/app.application-bridge');
9
+ const APPLICATION_EXTENSION_BRIDGE = Symbol('@zmdb/app.application-extension-bridge');
10
+ const COMPILED_APPLICATION = Symbol.for('@zmdb/app.compiled-application');
11
+ const DEFAULT_GRACE_MS = 5_000;
12
+ const EMPTY_OBSERVABILITY: Observability = Object.freeze({});
13
+
14
+ interface CompiledApplicationOptions {
15
+ readonly [COMPILED_APPLICATION]?: CompiledModule;
16
+ }
17
+
18
+ /** Private cross-package bridge used by HTTP/testing adapters without widening Application. */
19
+ export interface ApplicationBridge {
20
+ readonly compiled: CompiledModule;
21
+ readonly controllers: readonly CompiledController[];
22
+ }
23
+
24
+ interface BridgedApplication extends Application {
25
+ readonly [APPLICATION_BRIDGE]?: ApplicationBridge;
26
+ }
27
+
28
+ interface ApplicationExtensionBridge {
29
+ readonly controllers: readonly CompiledController[];
30
+ }
31
+
32
+ interface BridgedApplicationExtensionContext extends ApplicationExtensionContext {
33
+ readonly [APPLICATION_EXTENSION_BRIDGE]?: ApplicationExtensionBridge;
34
+ }
35
+
36
+ export interface ApplicationExtensionContext {
37
+ readonly container: Container;
38
+ readonly controllers: readonly object[];
39
+ readonly commands: readonly object[];
40
+ readonly observability: Observability;
41
+ }
42
+
43
+ export interface ApplicationExtension {
44
+ readonly name: string;
45
+ start(context: ApplicationExtensionContext): void | Promise<void>;
46
+ stop(options: { readonly graceMs: number }): void | Promise<void>;
47
+ }
48
+
49
+ export interface ApplicationOptions {
50
+ readonly extensions?: readonly ApplicationExtension[];
51
+ readonly observability?: Observability;
52
+ readonly graceMs?: number;
53
+ }
54
+
55
+ export interface Application extends AsyncDisposable {
56
+ readonly container: Container;
57
+ readonly lazy: readonly LazyModuleHandle[];
58
+ init(): Promise<void>;
59
+ }
60
+
61
+ /**
62
+ * Compile one protocol-neutral application and own its complete lifecycle.
63
+ *
64
+ * The two symbol-keyed bridge properties are non-enumerable implementation
65
+ * details. They let the HTTP and testing adapters consume the same compiled
66
+ * identities without adding public Application fields or compiling twice.
67
+ */
68
+ export function createApplication(rootModule: ModuleClass, options: ApplicationOptions = {}): Application {
69
+ const graceMs = applicationGrace(options.graceMs ?? DEFAULT_GRACE_MS);
70
+ const extensions = validatedExtensions(options.extensions ?? []);
71
+ const compiled = compiledApplication(options) ?? compileModule(rootModule);
72
+ const runtime = runtimeOf(compiled);
73
+ const instances = lifecycleInstances(compiled.container);
74
+ const observability = options.observability ?? EMPTY_OBSERVABILITY;
75
+ const controllerBindings = Object.freeze([
76
+ ...(runtime?.controllers ?? compiled.controllers.map(controller => ({ kind: 'eager' as const, controller }))),
77
+ ]);
78
+ let context: ApplicationExtensionContext | undefined;
79
+ if (extensions.length > 0) {
80
+ const extensionContext: ApplicationExtensionContext = {
81
+ container: compiled.container,
82
+ controllers: Object.freeze([...compiled.controllers]),
83
+ commands: Object.freeze([...compiled.commands]),
84
+ observability,
85
+ };
86
+ Object.defineProperty(extensionContext, APPLICATION_EXTENSION_BRIDGE, {
87
+ value: Object.freeze({ controllers: controllerBindings }),
88
+ });
89
+ context = Object.freeze(extensionContext);
90
+ }
91
+ const entered: ApplicationExtension[] = [];
92
+ let state: 'created' | 'starting' | 'running' | 'stopping' | 'stopped' | 'failed-cleaned' = 'created';
93
+ let initPromise: Promise<void> | undefined;
94
+ let disposePromise: Promise<void> | undefined;
95
+
96
+ const stopEntered = async (): Promise<unknown[]> => {
97
+ const errors: unknown[] = [];
98
+ const deadline = Date.now() + graceMs;
99
+ for (let index = entered.length - 1; index >= 0; index -= 1) {
100
+ const extension = entered[index];
101
+ if (extension === undefined) continue;
102
+ try {
103
+ await extension.stop({ graceMs: Math.max(0, deadline - Date.now()) });
104
+ } catch (error) {
105
+ errors.push(error);
106
+ }
107
+ }
108
+ entered.length = 0;
109
+ return errors;
110
+ };
111
+
112
+ const shutdownInstances = async (): Promise<unknown[]> => {
113
+ try {
114
+ await runShutdown(instances);
115
+ return [];
116
+ } catch (error) {
117
+ return errorsOf(error);
118
+ }
119
+ };
120
+
121
+ const start = async (): Promise<void> => {
122
+ state = 'starting';
123
+ try {
124
+ await runInit(instances);
125
+ if (context !== undefined) {
126
+ for (const extension of extensions) {
127
+ entered.push(extension);
128
+ await extension.start(context);
129
+ }
130
+ }
131
+ state = 'running';
132
+ } catch (startupError) {
133
+ runtime?.beginShutdown();
134
+ await runtime?.waitForLoads();
135
+ const cleanupErrors = [...(await stopEntered()), ...(await shutdownInstances())];
136
+ state = 'failed-cleaned';
137
+ if (cleanupErrors.length === 0) throw startupError;
138
+ throw new AggregateError([startupError, ...cleanupErrors], '@zmdb/app: application startup cleanup failed', {
139
+ cause: startupError,
140
+ });
141
+ }
142
+ };
143
+
144
+ const dispose = async (): Promise<void> => {
145
+ runtime?.beginShutdown();
146
+ if (initPromise !== undefined) {
147
+ try {
148
+ await initPromise;
149
+ } catch {
150
+ // Failed initialization performed its own complete cleanup transaction.
151
+ return;
152
+ }
153
+ }
154
+ if (state === 'failed-cleaned' || state === 'stopped') return;
155
+
156
+ state = 'stopping';
157
+ await runtime?.waitForLoads();
158
+ const errors = [...(await stopEntered()), ...(await shutdownInstances())];
159
+ state = 'stopped';
160
+ throwObserved(errors, '@zmdb/app: application shutdown failed');
161
+ };
162
+
163
+ const application: Application = {
164
+ container: compiled.container,
165
+ lazy: compiled.lazy,
166
+ init: () => {
167
+ if (disposePromise !== undefined) {
168
+ return Promise.reject(new Error('@zmdb/app: application is shutting down'));
169
+ }
170
+ initPromise ??= start();
171
+ return initPromise;
172
+ },
173
+ [Symbol.asyncDispose]: () => {
174
+ disposePromise ??= dispose();
175
+ return disposePromise;
176
+ },
177
+ };
178
+
179
+ Object.defineProperty(application, APPLICATION_BRIDGE, {
180
+ value: Object.freeze({
181
+ compiled,
182
+ controllers: controllerBindings,
183
+ }),
184
+ });
185
+ return application;
186
+ }
187
+
188
+ /** Package-private controller declarations for extensions with eager-only rules. */
189
+ export function applicationExtensionControllersOf(context: ApplicationExtensionContext): readonly CompiledController[] {
190
+ const carrier: BridgedApplicationExtensionContext = context;
191
+ return (
192
+ carrier[APPLICATION_EXTENSION_BRIDGE]?.controllers ??
193
+ Object.freeze(context.controllers.map(controller => ({ kind: 'eager' as const, controller })))
194
+ );
195
+ }
196
+
197
+ /** Package-private access for command applications. */
198
+ export function applicationBridgeOf(application: Application): ApplicationBridge {
199
+ const carrier: BridgedApplication = application;
200
+ const bridge = carrier[APPLICATION_BRIDGE];
201
+ if (bridge === undefined) {
202
+ throw new Error('@zmdb/app: application omitted its compiled graph');
203
+ }
204
+ return bridge;
205
+ }
206
+
207
+ function compiledApplication(options: ApplicationOptions): CompiledModule | undefined {
208
+ const carrier: ApplicationOptions & CompiledApplicationOptions = options;
209
+ return carrier[COMPILED_APPLICATION];
210
+ }
211
+
212
+ function applicationGrace(value: number): number {
213
+ if (!Number.isInteger(value) || value <= 0) {
214
+ throw new RangeError('@zmdb/app: graceMs must be a positive integer');
215
+ }
216
+ return value;
217
+ }
218
+
219
+ function validatedExtensions(values: readonly ApplicationExtension[]): readonly ApplicationExtension[] {
220
+ const names = new Set<string>();
221
+ const extensions = [...values];
222
+ for (const extension of extensions) {
223
+ if (extension.name.length === 0) {
224
+ throw new RangeError('@zmdb/app: an extension name cannot be empty');
225
+ }
226
+ if (names.has(extension.name)) {
227
+ throw new Error(`@zmdb/app: duplicate extension name "${extension.name}"`);
228
+ }
229
+ names.add(extension.name);
230
+ }
231
+ return Object.freeze(extensions);
232
+ }
233
+
234
+ function errorsOf(error: unknown): unknown[] {
235
+ return error instanceof AggregateError ? [...error.errors] : [error];
236
+ }
237
+
238
+ function throwObserved(errors: readonly unknown[], message: string): void {
239
+ if (errors.length === 0) return;
240
+ const first = errors[0];
241
+ if (errors.length === 1 && first !== undefined) throw first;
242
+ throw new AggregateError(errors, message);
243
+ }