bunsane 0.3.1 → 0.4.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.
Files changed (224) hide show
  1. package/CHANGELOG.md +445 -318
  2. package/config/cache.config.ts +35 -1
  3. package/core/App.ts +24 -1064
  4. package/core/ArcheType.ts +78 -2110
  5. package/core/BatchLoader.ts +56 -32
  6. package/core/Entity.ts +85 -1043
  7. package/core/EntityHookManager.ts +52 -754
  8. package/core/Logger.ts +10 -0
  9. package/core/RequestContext.ts +64 -6
  10. package/core/RequestLoaders.ts +187 -36
  11. package/core/SchedulerManager.ts +28 -600
  12. package/core/app/bootstrap.ts +133 -0
  13. package/core/app/cors.ts +85 -0
  14. package/core/app/graphqlSetup.ts +56 -0
  15. package/core/app/healthEndpoints.ts +31 -0
  16. package/core/app/metricsCollector.ts +27 -0
  17. package/core/app/preparedStatementWarmup.ts +15 -0
  18. package/core/app/processHandlers.ts +43 -0
  19. package/core/app/requestRouter.ts +310 -0
  20. package/core/app/restRegistry.ts +80 -0
  21. package/core/app/shutdown.ts +97 -0
  22. package/core/app/studioRouter.ts +83 -0
  23. package/core/archetype/customTypes.ts +100 -0
  24. package/core/archetype/decorators.ts +171 -0
  25. package/core/archetype/fieldResolvers.ts +666 -0
  26. package/core/archetype/helpers.ts +29 -0
  27. package/core/archetype/relationLoader.ts +161 -0
  28. package/core/archetype/schemaBuilder.ts +141 -0
  29. package/core/archetype/weaver.ts +218 -0
  30. package/core/archetype/zodSchemaBuilder.ts +527 -0
  31. package/core/cache/CacheManager.ts +173 -267
  32. package/core/cache/CompressionUtils.ts +34 -3
  33. package/core/cache/MemoryCache.ts +40 -37
  34. package/core/cache/RedisCache.ts +4 -4
  35. package/core/cache/health.ts +30 -0
  36. package/core/cache/invalidation.ts +96 -0
  37. package/core/cache/strategies/writeInvalidate.ts +111 -0
  38. package/core/cache/strategies/writeThrough.ts +233 -0
  39. package/core/components/BaseComponent.ts +16 -8
  40. package/core/components/ComponentRegistry.ts +28 -0
  41. package/core/decorators/IndexedField.ts +1 -1
  42. package/core/entity/cacheStrategies.ts +97 -0
  43. package/core/entity/componentAccess.ts +364 -0
  44. package/core/entity/finders.ts +202 -0
  45. package/core/entity/pendingOps.ts +72 -0
  46. package/core/entity/saveEntity.ts +377 -0
  47. package/core/hooks/dispatcher.ts +439 -0
  48. package/core/hooks/guards.ts +155 -0
  49. package/core/hooks/registry.ts +247 -0
  50. package/core/metadata/definitions/Component.ts +1 -1
  51. package/core/metadata/index.ts +15 -4
  52. package/core/middleware/AccessLog.ts +8 -1
  53. package/core/middleware/RateLimit.ts +102 -105
  54. package/core/middleware/RequestId.ts +2 -9
  55. package/core/middleware/SecurityHeaders.ts +2 -11
  56. package/core/middleware/headers.ts +28 -0
  57. package/core/remote/OutboxWorker.ts +213 -183
  58. package/core/remote/RemoteManager.ts +401 -400
  59. package/core/remote/types.ts +153 -151
  60. package/core/requestScope.ts +34 -0
  61. package/core/scheduler/cronEvaluator.ts +174 -0
  62. package/core/scheduler/lifecycleHooks.ts +21 -0
  63. package/core/scheduler/lockCoordinator.ts +27 -0
  64. package/core/scheduler/metrics.ts +14 -0
  65. package/core/scheduler/taskRunner.ts +420 -0
  66. package/database/DatabaseHelper.ts +128 -101
  67. package/database/IndexingStrategy.ts +72 -2
  68. package/database/PreparedStatementCache.ts +20 -5
  69. package/database/cancellable.ts +35 -0
  70. package/database/index.ts +15 -3
  71. package/database/instrumentedDb.ts +141 -0
  72. package/endpoints/archetypes.ts +2 -8
  73. package/endpoints/tables.ts +6 -1
  74. package/gql/index.ts +1 -1
  75. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  76. package/package.json +22 -1
  77. package/query/CTENode.ts +5 -3
  78. package/query/ComponentInclusionNode.ts +240 -13
  79. package/query/OrNode.ts +6 -5
  80. package/query/Query.ts +203 -59
  81. package/query/QueryContext.ts +6 -0
  82. package/query/QueryDAG.ts +7 -2
  83. package/query/membershipSource.ts +66 -0
  84. package/storage/LocalStorageProvider.ts +8 -3
  85. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  86. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  87. package/studio/{index.html → dist/index.html} +3 -2
  88. package/swagger/generator.ts +11 -1
  89. package/upload/UploadManager.ts +8 -6
  90. package/utils/uuid.ts +40 -10
  91. package/.claude/settings.local.json +0 -47
  92. package/.prettierrc +0 -4
  93. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  94. package/.serena/memories/architecture.md +0 -154
  95. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  96. package/.serena/memories/code_style_and_conventions.md +0 -76
  97. package/.serena/memories/project_overview.md +0 -43
  98. package/.serena/memories/schema-dsl-plan.md +0 -107
  99. package/.serena/memories/suggested_commands.md +0 -80
  100. package/.serena/memories/typescript-compilation-status.md +0 -54
  101. package/.serena/project.yml +0 -114
  102. package/BunSane.jpg +0 -0
  103. package/CLAUDE.md +0 -198
  104. package/TODO.md +0 -2
  105. package/bun.lock +0 -302
  106. package/bunfig.toml +0 -10
  107. package/docs/SCALABILITY_PLAN.md +0 -175
  108. package/studio/bun.lock +0 -482
  109. package/studio/package.json +0 -39
  110. package/studio/postcss.config.js +0 -6
  111. package/studio/src/components/DataTable.tsx +0 -211
  112. package/studio/src/components/Layout.tsx +0 -13
  113. package/studio/src/components/PageContainer.tsx +0 -9
  114. package/studio/src/components/PageHeader.tsx +0 -13
  115. package/studio/src/components/SearchBar.tsx +0 -57
  116. package/studio/src/components/Sidebar.tsx +0 -294
  117. package/studio/src/components/ui/button.tsx +0 -56
  118. package/studio/src/components/ui/checkbox.tsx +0 -26
  119. package/studio/src/components/ui/input.tsx +0 -25
  120. package/studio/src/hooks/useDataTable.ts +0 -131
  121. package/studio/src/index.css +0 -36
  122. package/studio/src/lib/api.ts +0 -186
  123. package/studio/src/lib/utils.ts +0 -13
  124. package/studio/src/main.tsx +0 -17
  125. package/studio/src/pages/ArcheType.tsx +0 -239
  126. package/studio/src/pages/Components.tsx +0 -124
  127. package/studio/src/pages/EntityInspector.tsx +0 -302
  128. package/studio/src/pages/QueryRunner.tsx +0 -246
  129. package/studio/src/pages/Table.tsx +0 -94
  130. package/studio/src/pages/Welcome.tsx +0 -241
  131. package/studio/src/routes.tsx +0 -45
  132. package/studio/src/store/archeTypeSettings.ts +0 -30
  133. package/studio/src/store/studio.ts +0 -65
  134. package/studio/src/utils/columnHelpers.tsx +0 -114
  135. package/studio/studio-instructions.md +0 -81
  136. package/studio/tailwind.config.js +0 -77
  137. package/studio/utils.ts +0 -54
  138. package/studio/vite.config.js +0 -19
  139. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  140. package/tests/benchmark/bunfig.toml +0 -9
  141. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  142. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  143. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  144. package/tests/benchmark/fixtures/index.ts +0 -6
  145. package/tests/benchmark/index.ts +0 -22
  146. package/tests/benchmark/noop-preload.ts +0 -3
  147. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  148. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  149. package/tests/benchmark/runners/index.ts +0 -4
  150. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  151. package/tests/benchmark/scripts/generate-db.ts +0 -344
  152. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  153. package/tests/e2e/http.test.ts +0 -130
  154. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  155. package/tests/fixtures/components/TestOrder.ts +0 -23
  156. package/tests/fixtures/components/TestProduct.ts +0 -23
  157. package/tests/fixtures/components/TestUser.ts +0 -20
  158. package/tests/fixtures/components/index.ts +0 -6
  159. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  160. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  161. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  162. package/tests/helpers/MockRedisClient.ts +0 -113
  163. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  164. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  165. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  166. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  167. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  168. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  169. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  170. package/tests/integration/query/Query.exec.test.ts +0 -576
  171. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  172. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  173. package/tests/integration/remote/dlq.test.ts +0 -175
  174. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  175. package/tests/integration/remote/outbox.test.ts +0 -130
  176. package/tests/integration/remote/rpc.test.ts +0 -177
  177. package/tests/pglite-setup.ts +0 -62
  178. package/tests/setup.ts +0 -164
  179. package/tests/stress/BenchmarkRunner.ts +0 -203
  180. package/tests/stress/DataSeeder.ts +0 -190
  181. package/tests/stress/StressTestReporter.ts +0 -229
  182. package/tests/stress/cursor-perf-test.ts +0 -171
  183. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  184. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  185. package/tests/stress/index.ts +0 -7
  186. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  187. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  188. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  189. package/tests/unit/BatchLoader.test.ts +0 -196
  190. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  191. package/tests/unit/cache/CacheManager.test.ts +0 -367
  192. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  193. package/tests/unit/cache/RedisCache.test.ts +0 -411
  194. package/tests/unit/entity/Entity.components.test.ts +0 -317
  195. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  196. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  197. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  198. package/tests/unit/entity/Entity.test.ts +0 -345
  199. package/tests/unit/gql/depthLimit.test.ts +0 -203
  200. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  201. package/tests/unit/health/Health.test.ts +0 -129
  202. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  203. package/tests/unit/middleware/Middleware.test.ts +0 -98
  204. package/tests/unit/middleware/RequestId.test.ts +0 -54
  205. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  206. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  207. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  208. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  209. package/tests/unit/query/Query.test.ts +0 -310
  210. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  211. package/tests/unit/remote/RemoteError.test.ts +0 -55
  212. package/tests/unit/remote/decorators.test.ts +0 -195
  213. package/tests/unit/remote/metrics.test.ts +0 -115
  214. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  215. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  216. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  217. package/tests/unit/schema/schema-integration.test.ts +0 -426
  218. package/tests/unit/schema/schema.test.ts +0 -580
  219. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  220. package/tests/unit/upload/RestUpload.test.ts +0 -267
  221. package/tests/unit/validateEnv.test.ts +0 -82
  222. package/tests/utils/entity-tracker.ts +0 -57
  223. package/tests/utils/index.ts +0 -13
  224. package/tests/utils/test-context.ts +0 -149
@@ -0,0 +1,310 @@
1
+ import * as path from "path";
2
+ import { logger as MainLogger } from "../Logger";
3
+ import { getMetadataScript } from "../metadata";
4
+ import { addCorsHeaders, getCorsHeaders } from "./cors";
5
+ import {
6
+ handleHealth,
7
+ handleReady,
8
+ handleRemoteHealth,
9
+ } from "./healthEndpoints";
10
+ import { routeStudio } from "./studioRouter";
11
+ import { getDbStats } from "../../database/instrumentedDb";
12
+ import type { RequestStats } from "../RequestContext";
13
+
14
+ const logger = MainLogger.child({ scope: "App" });
15
+
16
+ function combineSignals(signals: AbortSignal[]): AbortSignal {
17
+ const anyFn = (AbortSignal as any).any;
18
+ if (typeof anyFn === 'function') {
19
+ return anyFn.call(AbortSignal, signals);
20
+ }
21
+ const controller = new AbortController();
22
+ for (const s of signals) {
23
+ if (s.aborted) {
24
+ controller.abort((s as any).reason);
25
+ return controller.signal;
26
+ }
27
+ // { once: true } auto-removes the listener after first fire, so no
28
+ // explicit removeEventListener is needed; GC cleans up the rest.
29
+ s.addEventListener('abort', () => controller.abort((s as any).reason), { once: true });
30
+ }
31
+ return controller.signal;
32
+ }
33
+
34
+ export async function handleRequest(app: any, req: Request): Promise<Response> {
35
+ const url = new URL(req.url);
36
+ const method = req.method;
37
+ const startTime = Date.now();
38
+
39
+ if (method === 'OPTIONS') {
40
+ return new Response(null, {
41
+ status: 204,
42
+ headers: getCorsHeaders(app.config.cors, req),
43
+ });
44
+ }
45
+
46
+ // Request timeout — combine framework wall-clock with client abort signal
47
+ // and rebind onto the request so downstream handlers (Yoga, REST) see
48
+ // cancellation propagation (C05).
49
+ const controller = new AbortController();
50
+ const timeoutId = setTimeout(() => {
51
+ controller.abort(new Error(`Request timeout after 30000ms: ${method} ${url.pathname}`));
52
+ const stats = (req as any).__bunsaneStats as RequestStats | undefined;
53
+ logger.warn({
54
+ scope: 'App',
55
+ method,
56
+ path: url.pathname,
57
+ operationName: stats?.operationName,
58
+ dataLoaderCalls: stats?.dataLoaderCalls,
59
+ dbQueryCount: stats?.dbQueryCount,
60
+ msg: 'Request timeout',
61
+ }, `Request timeout: ${method} ${url.pathname}`);
62
+ }, 30000);
63
+ // Prevent the timer from keeping the Bun event loop alive at high concurrency.
64
+ (timeoutId as any).unref?.();
65
+ const combinedSignal = combineSignals([req.signal, controller.signal]);
66
+ req = new Request(req, { signal: combinedSignal });
67
+
68
+ const cors = app.config.cors;
69
+ const wrap = (response: Response) => addCorsHeaders(response, cors, req);
70
+
71
+ try {
72
+ if (url.pathname === "/health") {
73
+ const response = await handleHealth(app);
74
+ clearTimeout(timeoutId);
75
+ return wrap(response);
76
+ }
77
+
78
+ if (url.pathname === "/metrics") {
79
+ const metrics = await app.collectMetrics();
80
+ clearTimeout(timeoutId);
81
+ return wrap(new Response(JSON.stringify(metrics), {
82
+ status: 200,
83
+ headers: { "Content-Type": "application/json" },
84
+ }));
85
+ }
86
+
87
+ if (url.pathname === "/health/remote") {
88
+ const response = await handleRemoteHealth(app);
89
+ clearTimeout(timeoutId);
90
+ return wrap(response);
91
+ }
92
+
93
+ if (url.pathname === "/health/ready") {
94
+ const response = await handleReady(app);
95
+ clearTimeout(timeoutId);
96
+ return wrap(response);
97
+ }
98
+
99
+ if (url.pathname === "/openapi.json") {
100
+ clearTimeout(timeoutId);
101
+ return wrap(new Response(app.openAPISpecGenerator!.toJSON(), {
102
+ headers: { "Content-Type": "application/json" },
103
+ }));
104
+ }
105
+
106
+ if (url.pathname === "/docs") {
107
+ clearTimeout(timeoutId);
108
+ const swaggerUIHTML = `
109
+ <!DOCTYPE html>
110
+ <html>
111
+ <head>
112
+ <title>${app.name} Documentation</title>
113
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui.css" />
114
+ <style>
115
+ html { box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll; }
116
+ *, *:before, *:after { box-sizing: inherit; }
117
+ body { margin: 0; background: #fafafa; }
118
+ </style>
119
+ </head>
120
+ <body>
121
+ <div id="swagger-ui"></div>
122
+ <script src="https://unpkg.com/swagger-ui-dist@5.10.3/swagger-ui-bundle.js"></script>
123
+ <script>
124
+ window.onload = function() {
125
+ const ui = SwaggerUIBundle({
126
+ url: '/openapi.json',
127
+ dom_id: '#swagger-ui',
128
+ deepLinking: true,
129
+ presets: [
130
+ SwaggerUIBundle.presets.apis,
131
+ SwaggerUIBundle.presets.standalone
132
+ ],
133
+ plugins: [
134
+ SwaggerUIBundle.plugins.DownloadUrl
135
+ ],
136
+ layout: "BaseLayout"
137
+ });
138
+ };
139
+ </script>
140
+ </body>
141
+ </html>`;
142
+ return wrap(new Response(swaggerUIHTML, {
143
+ headers: { "Content-Type": "text/html" },
144
+ }));
145
+ }
146
+
147
+ const studioApiResponse = await routeStudio(app, url, req, method);
148
+ if (studioApiResponse) {
149
+ clearTimeout(timeoutId);
150
+ return wrap(studioApiResponse);
151
+ }
152
+
153
+ if (
154
+ app.studioEnabled &&
155
+ (url.pathname === "/studio" || url.pathname.startsWith("/studio/"))
156
+ ) {
157
+ clearTimeout(timeoutId);
158
+
159
+ if (url.pathname.startsWith("/studio/api/")) {
160
+ return wrap(new Response(
161
+ JSON.stringify({ error: "Studio API endpoint not found" }),
162
+ { status: 404, headers: { "Content-Type": "application/json" } },
163
+ ));
164
+ }
165
+
166
+ if (!url.pathname.startsWith("/studio/assets/")) {
167
+ const studioIndexPath = path.join(
168
+ import.meta.dirname,
169
+ "..",
170
+ "..",
171
+ "studio",
172
+ "dist",
173
+ "index.html",
174
+ );
175
+ try {
176
+ const studioFile = Bun.file(studioIndexPath);
177
+ if (await studioFile.exists()) {
178
+ let html = await studioFile.text();
179
+ html = html.replace("</head>", `${getMetadataScript()}</head>`);
180
+ return wrap(new Response(html, {
181
+ headers: { "Content-Type": "text/html" },
182
+ }));
183
+ } else {
184
+ return wrap(new Response(
185
+ "Studio not built. Run `bun run build:studio` to build the studio.",
186
+ { status: 404, headers: { "Content-Type": "text/plain" } },
187
+ ));
188
+ }
189
+ } catch (error) {
190
+ console.log("Error loading studio index.html:", error);
191
+ return wrap(new Response("Studio not available", {
192
+ status: 404,
193
+ headers: { "Content-Type": "text/plain" },
194
+ }));
195
+ }
196
+ }
197
+ }
198
+
199
+ for (const [route, folder] of app.staticAssets) {
200
+ if (url.pathname.startsWith(route)) {
201
+ const relativePath = url.pathname.slice(route.length);
202
+ const filePath = path.join(folder, relativePath);
203
+ try {
204
+ const file = Bun.file(filePath);
205
+ if (await file.exists()) {
206
+ clearTimeout(timeoutId);
207
+ return wrap(new Response(file));
208
+ }
209
+ } catch (error) {
210
+ logger.error(`Error serving static file ${filePath}:`, error as any);
211
+ }
212
+ }
213
+ }
214
+
215
+ const endpointKey = `${method}:${url.pathname}`;
216
+ let endpoint = app.restEndpointMap.get(endpointKey);
217
+
218
+ if (!endpoint) {
219
+ // Only iterate endpoints that have params (regex precompiled at registration).
220
+ for (const ep of app.restEndpoints) {
221
+ if (!ep.regex || ep.method !== method) continue;
222
+ if (ep.regex.test(url.pathname)) {
223
+ endpoint = ep;
224
+ break;
225
+ }
226
+ }
227
+ }
228
+
229
+ if (endpoint) {
230
+ try {
231
+ const result = await endpoint.handler(req);
232
+ const duration = Date.now() - startTime;
233
+ logger.trace(`REST ${method} ${url.pathname} completed in ${duration}ms`);
234
+
235
+ clearTimeout(timeoutId);
236
+ if (result instanceof Response) {
237
+ return wrap(result);
238
+ } else {
239
+ return wrap(new Response(JSON.stringify(result), {
240
+ headers: { "Content-Type": "application/json" },
241
+ }));
242
+ }
243
+ } catch (error) {
244
+ const duration = Date.now() - startTime;
245
+ logger.error(
246
+ `Error in REST endpoint ${method} ${endpoint.path} after ${duration}ms`,
247
+ error as any,
248
+ );
249
+ clearTimeout(timeoutId);
250
+ return wrap(new Response(
251
+ JSON.stringify({
252
+ error: "Internal server error",
253
+ code: "INTERNAL_ERROR",
254
+ ...(process.env.NODE_ENV === 'development' && {
255
+ message: (error as Error)?.message,
256
+ }),
257
+ }),
258
+ { status: 500, headers: { "Content-Type": "application/json" } },
259
+ ));
260
+ }
261
+ }
262
+
263
+ if (app.yoga) {
264
+ const response = await app.yoga(req);
265
+ const duration = Date.now() - startTime;
266
+ logger.trace(`GraphQL request completed in ${duration}ms`);
267
+ clearTimeout(timeoutId);
268
+ return response;
269
+ }
270
+
271
+ clearTimeout(timeoutId);
272
+ return wrap(new Response("Not Found", { status: 404 }));
273
+ } catch (error) {
274
+ const duration = Date.now() - startTime;
275
+ const stats = (req as any).__bunsaneStats as RequestStats | undefined;
276
+ logger.error(
277
+ {
278
+ scope: 'App',
279
+ method,
280
+ path: url.pathname,
281
+ duration,
282
+ operationName: stats?.operationName,
283
+ dataLoaderCalls: stats?.dataLoaderCalls,
284
+ dbQueryCount: stats?.dbQueryCount,
285
+ dbStats: getDbStats(),
286
+ err: error,
287
+ },
288
+ `Request failed after ${duration}ms: ${method} ${url.pathname}`,
289
+ );
290
+ clearTimeout(timeoutId);
291
+
292
+ if ((error as Error).name === "AbortError") {
293
+ return wrap(new Response(
294
+ JSON.stringify({ error: "Request timeout", code: "TIMEOUT_ERROR" }),
295
+ { status: 408, headers: { "Content-Type": "application/json" } },
296
+ ));
297
+ }
298
+
299
+ return wrap(new Response(
300
+ JSON.stringify({
301
+ error: "Internal server error",
302
+ code: "INTERNAL_ERROR",
303
+ ...(process.env.NODE_ENV === 'development' && {
304
+ message: (error as Error)?.message,
305
+ }),
306
+ }),
307
+ { status: 500, headers: { "Content-Type": "application/json" } },
308
+ ));
309
+ }
310
+ }
@@ -0,0 +1,80 @@
1
+ import { logger as MainLogger } from "../Logger";
2
+
3
+ const logger = MainLogger.child({ scope: "App" });
4
+
5
+ export function collectRestEndpoints(app: any, services: any[]): void {
6
+ for (const service of services) {
7
+ const endpoints = (service.constructor as any).httpEndpoints;
8
+ if (!endpoints) continue;
9
+
10
+ for (const endpoint of endpoints) {
11
+ // Precompile the parameterized regex once so the hot-path router
12
+ // never calls replace + new RegExp per request.
13
+ const hasParams = endpoint.path.includes(':');
14
+ const regex = hasParams
15
+ ? new RegExp(`^${endpoint.path.replace(/:[^/]+/g, '[^/]+')}$`)
16
+ : undefined;
17
+
18
+ const endpointInfo = {
19
+ method: endpoint.method,
20
+ path: endpoint.path,
21
+ regex,
22
+ handler: endpoint.handler.bind(service),
23
+ service: service,
24
+ };
25
+ logger.trace(
26
+ `Registered REST endpoint: [${endpoint.method}] ${endpoint.path} for service ${service.constructor.name}`,
27
+ );
28
+ app.restEndpoints.push(endpointInfo);
29
+ app.restEndpointMap.set(`${endpoint.method}:${endpoint.path}`, endpointInfo);
30
+
31
+ if ((endpoint.handler as any).swaggerOperation) {
32
+ const classTags = (service.constructor as any).swaggerClassTags || [];
33
+ const methodTags =
34
+ (service.constructor as any).swaggerMethodTags?.[endpoint.handler.name] || [];
35
+ const allTags = [...classTags, ...methodTags];
36
+
37
+ logger.trace(
38
+ `Generating OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path} with tags: ${allTags.join(", ")}`,
39
+ );
40
+
41
+ const operation = { ...(endpoint.handler as any).swaggerOperation };
42
+ if (allTags.length > 0) {
43
+ operation.tags = [...(operation.tags || []), ...allTags];
44
+ }
45
+
46
+ app.openAPISpecGenerator!.addEndpoint({
47
+ method: endpoint.method,
48
+ path: endpoint.path,
49
+ operation,
50
+ });
51
+ logger.trace(
52
+ `Registered OpenAPI spec for endpoint: [${endpoint.method}] ${endpoint.path}`,
53
+ );
54
+ } else if (app.enforceDocs) {
55
+ logger.warn(
56
+ `No swagger operation found for endpoint: [${endpoint.method}] ${endpoint.path} in service ${service.constructor.name}`,
57
+ );
58
+ app.openAPISpecGenerator!.addEndpoint({
59
+ method: endpoint.method,
60
+ path: endpoint.path,
61
+ operation: {
62
+ summary: `No description for ${endpoint.path}. Don't use this endpoint until it's properly documented!`,
63
+ requestBody: {
64
+ content: {
65
+ "application/json": {
66
+ schema: {},
67
+ },
68
+ },
69
+ },
70
+ responses: {
71
+ "200": {
72
+ description: "Success",
73
+ },
74
+ },
75
+ },
76
+ });
77
+ }
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,97 @@
1
+ import ApplicationLifecycle from "../ApplicationLifecycle";
2
+ import { logger as MainLogger } from "../Logger";
3
+ import { SchedulerManager } from "../SchedulerManager";
4
+ import db from "../../database";
5
+ import { setRemoteManager } from "../remote";
6
+
7
+ const logger = MainLogger.child({ scope: "App" });
8
+
9
+ export async function runShutdown(app: any): Promise<void> {
10
+ if (app.isShuttingDown) return;
11
+ app.isShuttingDown = true;
12
+ app.isReady = false;
13
+
14
+ const shutdownStart = Date.now();
15
+ logger.info({ scope: 'app', component: 'App', msg: 'Shutting down application', gracePeriodMs: app.shutdownGracePeriod });
16
+
17
+ const budgetRemaining = () => Math.max(500, app.shutdownGracePeriod - (Date.now() - shutdownStart));
18
+
19
+ if (app.server) {
20
+ try {
21
+ logger.info({ scope: 'app', component: 'App', msg: 'Draining HTTP connections' });
22
+ app.server.stop(false);
23
+ await waitForHttpDrain(app, budgetRemaining());
24
+ try { app.server.stop(true); } catch {}
25
+ logger.info({ scope: 'app', component: 'App', msg: 'HTTP server stopped' });
26
+ } catch (error) {
27
+ logger.warn({ scope: 'app', component: 'App', msg: 'HTTP server stop error', err: error });
28
+ }
29
+ }
30
+
31
+ try {
32
+ await SchedulerManager.getInstance().stop(Math.min(budgetRemaining(), 15_000));
33
+ logger.info({ scope: 'app', component: 'App', msg: 'Scheduler stopped' });
34
+ } catch (error) {
35
+ logger.warn({ scope: 'app', component: 'App', msg: 'Scheduler stop error', err: error });
36
+ }
37
+
38
+ if (app.remote) {
39
+ try {
40
+ await app.remote.shutdown();
41
+ setRemoteManager(null);
42
+ app.remote = null;
43
+ logger.info({ scope: 'app', component: 'App', msg: 'RemoteManager shutdown' });
44
+ } catch (error) {
45
+ logger.warn({ scope: 'app', component: 'App', msg: 'RemoteManager shutdown error', err: error });
46
+ }
47
+ }
48
+
49
+ try {
50
+ const { Entity } = await import('../Entity');
51
+ await Entity.drainPendingCacheOps(Math.min(budgetRemaining(), 5_000));
52
+ await Entity.drainPendingSideEffects(Math.min(budgetRemaining(), 5_000));
53
+ } catch (error) {
54
+ logger.warn({ scope: 'cache', component: 'App', msg: 'Entity cache op drain error', err: error });
55
+ }
56
+
57
+ try {
58
+ const { CacheManager } = await import('../cache/CacheManager');
59
+ await CacheManager.getInstance().shutdown();
60
+ logger.info({ scope: 'cache', component: 'App', msg: 'Cache shutdown completed' });
61
+ } catch (error) {
62
+ logger.warn({ scope: 'cache', component: 'App', msg: 'Cache shutdown error', err: error });
63
+ }
64
+
65
+ try {
66
+ db.close();
67
+ logger.info({ scope: 'app', component: 'App', msg: 'Database pool closed' });
68
+ } catch (error) {
69
+ logger.warn({ scope: 'app', component: 'App', msg: 'Database pool close error', err: error });
70
+ }
71
+
72
+ try {
73
+ if (app.phaseListener) {
74
+ ApplicationLifecycle.removePhaseListener(app.phaseListener);
75
+ app.phaseListener = null;
76
+ }
77
+ SchedulerManager.getInstance().disposeLifecycleIntegration();
78
+ } catch { /* ignore */ }
79
+
80
+ app.unregisterProcessHandlers();
81
+
82
+ logger.info({ scope: 'app', component: 'App', msg: 'Application shutdown completed', durationMs: Date.now() - shutdownStart });
83
+ }
84
+
85
+ export async function waitForHttpDrain(app: any, timeoutMs: number): Promise<void> {
86
+ if (!app.server) return;
87
+ const deadline = Date.now() + timeoutMs;
88
+ while (Date.now() < deadline) {
89
+ const pending = (app.server as any).pendingRequests ?? 0;
90
+ if (pending === 0) return;
91
+ await new Promise((r) => setTimeout(r, 50));
92
+ }
93
+ const leftover = (app.server as any).pendingRequests ?? -1;
94
+ if (leftover > 0) {
95
+ logger.warn({ scope: 'app', component: 'App', msg: 'HTTP drain timeout, pending requests remaining', pendingRequests: leftover });
96
+ }
97
+ }
@@ -0,0 +1,83 @@
1
+ import studioEndpoint from "../../endpoints";
2
+
3
+ export async function routeStudio(
4
+ app: any,
5
+ url: URL,
6
+ req: Request,
7
+ method: string,
8
+ ): Promise<Response | null> {
9
+ if (!app.studioEnabled || !url.pathname.startsWith("/studio/api/")) return null;
10
+
11
+ if (url.pathname === "/studio/api/tables") {
12
+ return await studioEndpoint.getTables();
13
+ }
14
+
15
+ if (url.pathname === "/studio/api/stats") {
16
+ return await studioEndpoint.handleStudioStatsRequest();
17
+ }
18
+
19
+ if (url.pathname === "/studio/api/components") {
20
+ return await studioEndpoint.handleStudioComponentsRequest();
21
+ }
22
+
23
+ if (url.pathname === "/studio/api/query" && method === "POST") {
24
+ const body = await req.json();
25
+ return await studioEndpoint.handleStudioQueryRequest(body);
26
+ }
27
+
28
+ const studioApiPath = url.pathname.replace("/studio/api/", "");
29
+ const pathSegments = studioApiPath.split("/");
30
+
31
+ if (pathSegments[0] === "entity" && pathSegments[1]) {
32
+ const entityId = pathSegments[1];
33
+ return await studioEndpoint.handleEntityInspectorRequest(entityId);
34
+ }
35
+
36
+ if (pathSegments[0] === "table" && pathSegments[1]) {
37
+ const tableName = pathSegments[1];
38
+
39
+ if (method === "DELETE") {
40
+ const body = await req.json();
41
+ return await studioEndpoint.handleStudioTableDeleteRequest(tableName, body);
42
+ }
43
+
44
+ const limit = url.searchParams.get("limit");
45
+ const offset = url.searchParams.get("offset");
46
+ const search = url.searchParams.get("search");
47
+
48
+ return await studioEndpoint.handleStudioTableRequest(tableName, {
49
+ limit: limit ? parseInt(limit, 10) : undefined,
50
+ offset: offset ? parseInt(offset, 10) : undefined,
51
+ search: search ?? undefined,
52
+ });
53
+ }
54
+
55
+ if (pathSegments[0] === "arche-type" && pathSegments[1]) {
56
+ const archeTypeName = pathSegments[1];
57
+
58
+ if (method === "DELETE") {
59
+ const body = await req.json();
60
+ return await studioEndpoint.handleStudioArcheTypeDeleteRequest(archeTypeName, body);
61
+ }
62
+
63
+ const limit = url.searchParams.get("limit");
64
+ const offset = url.searchParams.get("offset");
65
+ const search = url.searchParams.get("search");
66
+ const includeDeleted = url.searchParams.get("include_deleted");
67
+
68
+ return await studioEndpoint.handleStudioArcheTypeRecordsRequest(archeTypeName, {
69
+ limit: limit ? parseInt(limit, 10) : undefined,
70
+ offset: offset ? parseInt(offset, 10) : undefined,
71
+ search: search ?? undefined,
72
+ include_deleted: includeDeleted === "true",
73
+ });
74
+ }
75
+
76
+ return new Response(
77
+ JSON.stringify({ error: "Studio API endpoint not found" }),
78
+ {
79
+ status: 404,
80
+ headers: { "Content-Type": "application/json" },
81
+ },
82
+ );
83
+ }
@@ -0,0 +1,100 @@
1
+ import { z, ZodObject } from "zod";
2
+ import { asObjectType } from "@gqloom/zod";
3
+
4
+ export const customTypeRegistry = new Map<any, any>();
5
+ export const customTypeNameRegistry = new Map<any, string>();
6
+ export const registeredCustomTypes = new Map<string, any>();
7
+ export const customTypeSilks = new Map<string, any>();
8
+ export const customTypeResolvers: any[] = [];
9
+ export const inputTypeRegistry = new Map<any, string>();
10
+
11
+ // Structural signature registry for input type deduplication
12
+ // Maps structural signature -> registered input type name
13
+ export const structuralSignatureRegistry = new Map<string, string>();
14
+
15
+ let _generateZodStructuralSignature: ((schema: any) => string) | null = null;
16
+
17
+ function getSignatureGenerator(): (schema: any) => string {
18
+ if (!_generateZodStructuralSignature) {
19
+ const { generateZodStructuralSignature } = require('../../gql/utils/TypeSignature');
20
+ _generateZodStructuralSignature = generateZodStructuralSignature;
21
+ }
22
+ return _generateZodStructuralSignature!;
23
+ }
24
+
25
+ export function registerCustomZodType(
26
+ type: any,
27
+ schema: any,
28
+ typeName?: string,
29
+ inputTypeName?: string
30
+ ) {
31
+ if (typeName && schema instanceof ZodObject) {
32
+ const shape = schema.shape;
33
+ const namedSchema = z.object({
34
+ __typename: z.literal(typeName).nullish(),
35
+ ...shape,
36
+ });
37
+ customTypeRegistry.set(type, namedSchema);
38
+ if (typeName) {
39
+ customTypeNameRegistry.set(type, typeName);
40
+ registeredCustomTypes.set(typeName, namedSchema);
41
+ }
42
+
43
+ if (inputTypeName) {
44
+ const inputSchema = z.object(shape).register(asObjectType, { name: inputTypeName });
45
+ registeredCustomTypes.set(inputTypeName, inputSchema);
46
+ inputTypeRegistry.set(type, inputTypeName);
47
+
48
+ try {
49
+ const generateSignature = getSignatureGenerator();
50
+ const signature = generateSignature(z.object(shape));
51
+ structuralSignatureRegistry.set(signature, inputTypeName);
52
+ } catch (e) {
53
+ // Signature registration is optional, don't fail if it errors
54
+ }
55
+ }
56
+ } else {
57
+ customTypeRegistry.set(type, schema);
58
+ if (typeName) {
59
+ customTypeNameRegistry.set(type, typeName);
60
+ registeredCustomTypes.set(typeName, schema);
61
+ }
62
+
63
+ if (inputTypeName && schema instanceof ZodObject) {
64
+ const inputSchema = schema.register(asObjectType, { name: inputTypeName });
65
+ registeredCustomTypes.set(inputTypeName, inputSchema);
66
+ inputTypeRegistry.set(type, inputTypeName);
67
+
68
+ try {
69
+ const generateSignature = getSignatureGenerator();
70
+ const signature = generateSignature(schema);
71
+ structuralSignatureRegistry.set(signature, inputTypeName);
72
+ } catch (e) {
73
+ // Signature registration is optional, don't fail if it errors
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ export function getRegisteredCustomTypes() {
80
+ return registeredCustomTypes;
81
+ }
82
+
83
+ /**
84
+ * Find a matching registered input type for a given Zod schema based on structural equivalence.
85
+ */
86
+ export function findMatchingInputType(schema: any): string | null {
87
+ if (!schema) return null;
88
+
89
+ try {
90
+ const generateSignature = getSignatureGenerator();
91
+ const signature = generateSignature(schema);
92
+ return structuralSignatureRegistry.get(signature) || null;
93
+ } catch (e) {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ export function getStructuralSignatureRegistry(): Map<string, string> {
99
+ return structuralSignatureRegistry;
100
+ }