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
@@ -1,151 +1,153 @@
1
- /**
2
- * Remote Communication: Types
3
- *
4
- * Standalone types for cross-app events over Redis Streams.
5
- * RemoteContext is NOT derived from GraphQLContext — remote handlers run
6
- * outside request scope.
7
- */
8
-
9
- export interface RemoteContext {
10
- sourceApp: string;
11
- messageId: string;
12
- timestamp: Date;
13
- attempt: number;
14
- correlationId?: string;
15
- deadline?: Date;
16
- }
17
-
18
- export type RemoteHandler<T = unknown> = (
19
- data: T,
20
- ctx: RemoteContext
21
- ) => Promise<void> | void;
22
-
23
- export type RemoteKind = "event" | "rpc_request";
24
-
25
- export interface RemoteHandlerInfo {
26
- event: string;
27
- methodName: string;
28
- handlerId: string;
29
- /** "event" for @RemoteEvent, "rpc_request" for @RemoteRpc */
30
- kind: RemoteKind;
31
- }
32
-
33
- export interface RemoteEnvelope {
34
- /** Discriminator — absent/`"event"` = fire-and-forget, `"rpc_request"` = RPC */
35
- kind?: RemoteKind;
36
- sourceApp: string;
37
- event: string;
38
- data: unknown;
39
- emittedAt: number;
40
-
41
- /** RPC-only */
42
- correlationId?: string;
43
- replyTo?: string;
44
- deadline?: number;
45
- }
46
-
47
- export type RpcHandler<TIn = unknown, TOut = unknown> = (
48
- data: TIn,
49
- ctx: RemoteContext
50
- ) => Promise<TOut> | TOut;
51
-
52
- export interface RpcSuccessResponse {
53
- correlationId: string;
54
- sourceApp: string;
55
- success: true;
56
- result: unknown;
57
- respondedAt: number;
58
- }
59
-
60
- export interface RpcErrorResponse {
61
- correlationId: string;
62
- sourceApp: string;
63
- success: false;
64
- error: {
65
- code: string;
66
- message: string;
67
- extensions?: Record<string, unknown>;
68
- };
69
- respondedAt: number;
70
- }
71
-
72
- export type RpcResponse = RpcSuccessResponse | RpcErrorResponse;
73
-
74
- export interface CallOptions {
75
- /** Timeout in ms (default: 5000) */
76
- timeout?: number;
77
- }
78
-
79
- /**
80
- * emit() options. Passing `trx` routes the event through the transactional
81
- * outbox — the row is inserted within the caller's transaction and
82
- * published by the OutboxWorker after commit.
83
- */
84
- export interface EmitOptions {
85
- /** Transaction handle from `db.begin()` / `db.transaction()`. */
86
- trx?: import("bun").SQL;
87
- }
88
-
89
- export interface RemoteErrorOptions {
90
- code: string;
91
- sourceApp?: string;
92
- extensions?: Record<string, unknown>;
93
- }
94
-
95
- export class RemoteError extends Error {
96
- public readonly code: string;
97
- public readonly sourceApp?: string;
98
- public readonly extensions?: Record<string, unknown>;
99
-
100
- constructor(message: string, options: RemoteErrorOptions) {
101
- super(message);
102
- this.name = "RemoteError";
103
- this.code = options.code;
104
- this.sourceApp = options.sourceApp;
105
- this.extensions = options.extensions;
106
- }
107
- }
108
-
109
- export interface RemoteManagerConfig {
110
- /** This app's identity — used as stream name and sourceApp field */
111
- appName: string;
112
- /** Consumer group (defaults to appName) */
113
- consumerGroup?: string;
114
- /** Unique consumer id within the group (defaults to pid + timestamp) */
115
- consumerId?: string;
116
- /** Stream key prefix (default: "remote:") */
117
- streamPrefix?: string;
118
- /** Enable verbose logging */
119
- enableLogging?: boolean;
120
- /** Max messages per XREADGROUP batch (default: 10) */
121
- batchSize?: number;
122
- /** XREADGROUP BLOCK timeout in ms (default: 2000) */
123
- blockMs?: number;
124
- /** XAUTOCLAIM idle threshold in ms on startup (default: 60000). 0 disables */
125
- autoClaimIdleMs?: number;
126
- /** Max response stream length cap per XADD MAXLEN ~ (default: 1000) */
127
- responseStreamMaxLen?: number;
128
- /** Default RPC call timeout in ms (default: 5000) */
129
- defaultCallTimeout?: number;
130
- /** Grace window for pending RPC calls during shutdown (default: 2000) */
131
- shutdownDrainMs?: number;
132
- /** Enable transactional outbox (default: false) */
133
- enableOutbox?: boolean;
134
- /** Outbox polling interval in ms (default: 1000) */
135
- outboxPollIntervalMs?: number;
136
- /** Max rows processed per outbox tick (default: 100) */
137
- outboxBatchSize?: number;
138
- /** Circuit breaker failure threshold before opening (default: 5) */
139
- circuitBreakerThreshold?: number;
140
- /** Circuit breaker reset timeout in ms (default: 30000) */
141
- circuitBreakerResetMs?: number;
142
- /** Max deliveries before routing a message to DLQ (default: 3, 0 disables) */
143
- dlqMaxDeliveries?: number;
144
- /**
145
- * Test-only: override how Redis clients are constructed. Return a
146
- * connected client compatible with the ioredis `Redis` interface.
147
- * `blocking` is `true` for connections that will issue BLOCK commands
148
- * (consumer + RPC listener), `false` for the publisher.
149
- */
150
- redisFactory?: (blocking: boolean) => any;
151
- }
1
+ /**
2
+ * Remote Communication: Types
3
+ *
4
+ * Standalone types for cross-app events over Redis Streams.
5
+ * RemoteContext is NOT derived from GraphQLContext — remote handlers run
6
+ * outside request scope.
7
+ */
8
+
9
+ export interface RemoteContext {
10
+ sourceApp: string;
11
+ messageId: string;
12
+ timestamp: Date;
13
+ attempt: number;
14
+ correlationId?: string;
15
+ deadline?: Date;
16
+ }
17
+
18
+ export type RemoteHandler<T = unknown> = (
19
+ data: T,
20
+ ctx: RemoteContext
21
+ ) => Promise<void> | void;
22
+
23
+ export type RemoteKind = "event" | "rpc_request";
24
+
25
+ export interface RemoteHandlerInfo {
26
+ event: string;
27
+ methodName: string;
28
+ handlerId: string;
29
+ /** "event" for @RemoteEvent, "rpc_request" for @RemoteRpc */
30
+ kind: RemoteKind;
31
+ }
32
+
33
+ export interface RemoteEnvelope {
34
+ /** Discriminator — absent/`"event"` = fire-and-forget, `"rpc_request"` = RPC */
35
+ kind?: RemoteKind;
36
+ sourceApp: string;
37
+ event: string;
38
+ data: unknown;
39
+ emittedAt: number;
40
+
41
+ /** RPC-only */
42
+ correlationId?: string;
43
+ replyTo?: string;
44
+ deadline?: number;
45
+ }
46
+
47
+ export type RpcHandler<TIn = unknown, TOut = unknown> = (
48
+ data: TIn,
49
+ ctx: RemoteContext
50
+ ) => Promise<TOut> | TOut;
51
+
52
+ export interface RpcSuccessResponse {
53
+ correlationId: string;
54
+ sourceApp: string;
55
+ success: true;
56
+ result: unknown;
57
+ respondedAt: number;
58
+ }
59
+
60
+ export interface RpcErrorResponse {
61
+ correlationId: string;
62
+ sourceApp: string;
63
+ success: false;
64
+ error: {
65
+ code: string;
66
+ message: string;
67
+ extensions?: Record<string, unknown>;
68
+ };
69
+ respondedAt: number;
70
+ }
71
+
72
+ export type RpcResponse = RpcSuccessResponse | RpcErrorResponse;
73
+
74
+ export interface CallOptions {
75
+ /** Timeout in ms (default: 5000) */
76
+ timeout?: number;
77
+ }
78
+
79
+ /**
80
+ * emit() options. Passing `trx` routes the event through the transactional
81
+ * outbox — the row is inserted within the caller's transaction and
82
+ * published by the OutboxWorker after commit.
83
+ */
84
+ export interface EmitOptions {
85
+ /** Transaction handle from `db.begin()` / `db.transaction()`. */
86
+ trx?: import("bun").SQL;
87
+ }
88
+
89
+ export interface RemoteErrorOptions {
90
+ code: string;
91
+ sourceApp?: string;
92
+ extensions?: Record<string, unknown>;
93
+ }
94
+
95
+ export class RemoteError extends Error {
96
+ public readonly code: string;
97
+ public readonly sourceApp?: string;
98
+ public readonly extensions?: Record<string, unknown>;
99
+
100
+ constructor(message: string, options: RemoteErrorOptions) {
101
+ super(message);
102
+ this.name = "RemoteError";
103
+ this.code = options.code;
104
+ this.sourceApp = options.sourceApp;
105
+ this.extensions = options.extensions;
106
+ }
107
+ }
108
+
109
+ export interface RemoteManagerConfig {
110
+ /** This app's identity — used as stream name and sourceApp field */
111
+ appName: string;
112
+ /** Consumer group (defaults to appName) */
113
+ consumerGroup?: string;
114
+ /** Unique consumer id within the group (defaults to pid + timestamp) */
115
+ consumerId?: string;
116
+ /** Stream key prefix (default: "remote:") */
117
+ streamPrefix?: string;
118
+ /** Enable verbose logging */
119
+ enableLogging?: boolean;
120
+ /** Max messages per XREADGROUP batch (default: 10) */
121
+ batchSize?: number;
122
+ /** XREADGROUP BLOCK timeout in ms (default: 2000) */
123
+ blockMs?: number;
124
+ /** XAUTOCLAIM idle threshold in ms on startup (default: 60000). 0 disables */
125
+ autoClaimIdleMs?: number;
126
+ /** Max response stream length cap per XADD MAXLEN ~ (default: 1000) */
127
+ responseStreamMaxLen?: number;
128
+ /** Default RPC call timeout in ms (default: 5000) */
129
+ defaultCallTimeout?: number;
130
+ /** Grace window for pending RPC calls during shutdown (default: 2000) */
131
+ shutdownDrainMs?: number;
132
+ /** Enable transactional outbox (default: false) */
133
+ enableOutbox?: boolean;
134
+ /** Outbox polling interval in ms (default: 1000) */
135
+ outboxPollIntervalMs?: number;
136
+ /** Max rows processed per outbox tick (default: 100) */
137
+ outboxBatchSize?: number;
138
+ /** How long to keep published outbox rows before deletion in ms (default: 86400000 / 24h, 0 disables) */
139
+ outboxRetentionMs?: number;
140
+ /** Circuit breaker failure threshold before opening (default: 5) */
141
+ circuitBreakerThreshold?: number;
142
+ /** Circuit breaker reset timeout in ms (default: 30000) */
143
+ circuitBreakerResetMs?: number;
144
+ /** Max deliveries before routing a message to DLQ (default: 3, 0 disables) */
145
+ dlqMaxDeliveries?: number;
146
+ /**
147
+ * Test-only: override how Redis clients are constructed. Return a
148
+ * connected client compatible with the ioredis `Redis` interface.
149
+ * `blocking` is `true` for connections that will issue BLOCK commands
150
+ * (consumer + RPC listener), `false` for the publisher.
151
+ */
152
+ redisFactory?: (blocking: boolean) => any;
153
+ }
@@ -0,0 +1,34 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import type { RequestLoaders } from "./RequestLoaders";
3
+ import type { PerRequestCounters } from "../database/instrumentedDb";
4
+
5
+ /**
6
+ * Ambient per-request context carrying the request's DataLoaders,
7
+ * AbortSignal and per-request counters via AsyncLocalStorage.
8
+ *
9
+ * Why: explicit `context` threading only reaches call sites that accept a
10
+ * context parameter. `@ArcheTypeFunction` bodies, `Unwrap()`, and service
11
+ * helpers call `entity.get(Component)` bare — without this scope every such
12
+ * call is an individual SELECT (N+1 per parent row). The GraphQL request
13
+ * plugin (`createRequestContextPlugin`) wraps execution in
14
+ * `runWithRequestScope`, so `Entity._loadComponent` and the relation
15
+ * population helpers can fall back to the request's batching DataLoaders
16
+ * when no explicit context is provided.
17
+ *
18
+ * Imports are type-only — no runtime dependency cycle with Entity/loaders.
19
+ */
20
+ export interface RequestScope {
21
+ loaders: RequestLoaders;
22
+ signal?: AbortSignal;
23
+ perRequest?: PerRequestCounters;
24
+ }
25
+
26
+ const storage = new AsyncLocalStorage<RequestScope>();
27
+
28
+ export function runWithRequestScope<T>(scope: RequestScope, fn: () => T): T {
29
+ return storage.run(scope, fn);
30
+ }
31
+
32
+ export function getRequestScope(): RequestScope | undefined {
33
+ return storage.getStore();
34
+ }
@@ -0,0 +1,174 @@
1
+ import { logger } from "../Logger";
2
+ import { CronParser } from "../../utils/cronParser";
3
+ import { ScheduleInterval } from "../../types/scheduler.types";
4
+ import type { ScheduledTaskInfo } from "../../types/scheduler.types";
5
+ import type { SchedulerManager } from "../SchedulerManager";
6
+
7
+ const loggerInstance = logger.child({ scope: "SchedulerManager" });
8
+
9
+ export function getIntervalMilliseconds(interval: ScheduleInterval): number {
10
+ switch (interval) {
11
+ case ScheduleInterval.MINUTE:
12
+ return 60 * 1000; // 1 minute
13
+ case ScheduleInterval.HOUR:
14
+ return 60 * 60 * 1000; // 1 hour
15
+ case ScheduleInterval.DAILY:
16
+ return 24 * 60 * 60 * 1000; // 24 hours
17
+ case ScheduleInterval.WEEKLY:
18
+ return 7 * 24 * 60 * 60 * 1000; // 7 days
19
+ case ScheduleInterval.MONTHLY:
20
+ return 30 * 24 * 60 * 60 * 1000; // 30 days (approximate)
21
+ default:
22
+ throw new Error(`Unsupported interval: ${interval}`);
23
+ }
24
+ }
25
+
26
+ export function scheduleLongIntervalTask(manager: SchedulerManager, taskInfo: ScheduledTaskInfo, intervalMs: number): void {
27
+ // For very long intervals, use a shorter check interval to avoid timeout overflow
28
+ const checkInterval = Math.min(intervalMs, 24 * 60 * 60 * 1000); // Max 24 hours check interval
29
+ const nextExecution = new Date(Date.now() + intervalMs);
30
+ taskInfo.nextExecution = nextExecution;
31
+
32
+ const intervalId = setInterval(async () => {
33
+ const now = Date.now();
34
+ if (now >= nextExecution.getTime()) {
35
+ await (manager as any).executeTask(taskInfo.id);
36
+ // Reschedule for next execution
37
+ taskInfo.nextExecution = new Date(now + intervalMs);
38
+ }
39
+ }, checkInterval);
40
+
41
+ manager.intervals.set(taskInfo.id, intervalId);
42
+ }
43
+
44
+ export function scheduleIntervalTask(manager: SchedulerManager, taskInfo: ScheduledTaskInfo): void {
45
+ const intervalMs = getIntervalMilliseconds(taskInfo.interval);
46
+
47
+ // Clear any existing interval for this task before creating a new one
48
+ const existingInterval = manager.intervals.get(taskInfo.id);
49
+ if (existingInterval) {
50
+ clearInterval(existingInterval);
51
+ manager.intervals.delete(taskInfo.id);
52
+ }
53
+
54
+ // For very long intervals (monthly), use a different approach
55
+ if (intervalMs > 24 * 60 * 60 * 1000) { // More than 24 hours
56
+ scheduleLongIntervalTask(manager, taskInfo, intervalMs);
57
+ } else {
58
+ const intervalId = setInterval(async () => {
59
+ await (manager as any).executeTask(taskInfo.id);
60
+ }, intervalMs);
61
+
62
+ manager.intervals.set(taskInfo.id, intervalId);
63
+ taskInfo.nextExecution = new Date(Date.now() + intervalMs);
64
+ }
65
+
66
+ if (manager.config.enableLogging) {
67
+ loggerInstance.info(`Scheduled task ${taskInfo.name} to run every ${intervalMs}ms`);
68
+ }
69
+ }
70
+
71
+ export function scheduleCronTask(manager: SchedulerManager, taskInfo: ScheduledTaskInfo): void {
72
+ if (!taskInfo.cronExpression) {
73
+ throw new Error(`Cron expression is required for CRON interval tasks`);
74
+ }
75
+
76
+ // Validate cron expression
77
+ const validation = CronParser.validate(taskInfo.cronExpression);
78
+ if (!validation.isValid) {
79
+ throw new Error(`Invalid cron expression: ${validation.error}`);
80
+ }
81
+
82
+ // Calculate next execution time
83
+ const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
84
+ if (!nextExecution) {
85
+ throw new Error(`Unable to calculate next execution time for cron expression: ${taskInfo.cronExpression}`);
86
+ }
87
+
88
+ taskInfo.nextExecution = nextExecution;
89
+
90
+ // Clear any existing timeout for this task before creating a new one
91
+ const existingTimeout = manager.intervals.get(taskInfo.id);
92
+ if (existingTimeout) {
93
+ clearTimeout(existingTimeout as any);
94
+ }
95
+
96
+ // Schedule the task to run at the calculated time
97
+ const timeoutId = setTimeout(async () => {
98
+ await (manager as any).executeTask(taskInfo.id);
99
+ // Reschedule for next execution
100
+ scheduleCronTask(manager, taskInfo);
101
+ }, nextExecution.getTime() - Date.now());
102
+
103
+ manager.intervals.set(taskInfo.id, timeoutId as any);
104
+
105
+ if (manager.config.enableLogging) {
106
+ loggerInstance.info(`Scheduled cron task ${taskInfo.name} to run at ${nextExecution.toISOString()}`);
107
+ }
108
+ }
109
+
110
+ export function scheduleTask(manager: SchedulerManager, taskInfo: ScheduledTaskInfo): void {
111
+ try {
112
+ if (taskInfo.interval === ScheduleInterval.CRON) {
113
+ scheduleCronTask(manager, taskInfo);
114
+ } else {
115
+ scheduleIntervalTask(manager, taskInfo);
116
+ }
117
+ } catch (error) {
118
+ loggerInstance.error(`Failed to schedule task ${taskInfo.name}: ${error instanceof Error ? error.message : String(error)}`);
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ export function scheduleJob(
124
+ manager: SchedulerManager,
125
+ name: string,
126
+ cronExpression: string,
127
+ callback: () => Promise<void> | void
128
+ ): { cancel: () => void } {
129
+ const jobId = `job_${name}_${Date.now()}`;
130
+
131
+ // Validate cron expression
132
+ const validation = CronParser.validate(cronExpression);
133
+ if (!validation.isValid) {
134
+ throw new Error(`Invalid cron expression for job "${name}": ${validation.error}`);
135
+ }
136
+
137
+ let timeoutId: ReturnType<typeof setTimeout> | null = null;
138
+ let cancelled = false;
139
+
140
+ const scheduleNextExecution = () => {
141
+ if (cancelled) return;
142
+
143
+ const nextExecution = CronParser.getNextExecution(validation.fields!, new Date());
144
+ if (!nextExecution) {
145
+ loggerInstance.warn(`Unable to calculate next execution for job "${name}"`);
146
+ return;
147
+ }
148
+
149
+ const delay = nextExecution.getTime() - Date.now();
150
+ timeoutId = setTimeout(async () => {
151
+ if (cancelled) return;
152
+ try {
153
+ await callback();
154
+ } catch (error) {
155
+ loggerInstance.error(`Job "${name}" failed: ${error instanceof Error ? error.message : String(error)}`);
156
+ }
157
+ scheduleNextExecution();
158
+ }, delay);
159
+
160
+ manager.intervals.set(jobId, timeoutId as any);
161
+ };
162
+
163
+ scheduleNextExecution();
164
+
165
+ return {
166
+ cancel: () => {
167
+ cancelled = true;
168
+ if (timeoutId) {
169
+ clearTimeout(timeoutId);
170
+ manager.intervals.delete(jobId);
171
+ }
172
+ }
173
+ };
174
+ }
@@ -0,0 +1,21 @@
1
+ import ApplicationLifecycle, { ApplicationPhase } from "../ApplicationLifecycle";
2
+ import type { SchedulerManager } from "../SchedulerManager";
3
+
4
+ export function initializeLifecycleIntegration(manager: SchedulerManager): void {
5
+ manager.phaseListener = (event) => {
6
+ const phase = event.detail;
7
+ if (phase === ApplicationPhase.APPLICATION_READY) {
8
+ if (manager.config.runOnStart) {
9
+ manager.start();
10
+ }
11
+ }
12
+ };
13
+ ApplicationLifecycle.addPhaseListener(manager.phaseListener);
14
+ }
15
+
16
+ export function disposeLifecycleIntegration(manager: SchedulerManager): void {
17
+ if (manager.phaseListener) {
18
+ ApplicationLifecycle.removePhaseListener(manager.phaseListener);
19
+ manager.phaseListener = null;
20
+ }
21
+ }
@@ -0,0 +1,27 @@
1
+ import type { DistributedLockConfig } from "./DistributedLock";
2
+ import type { SchedulerManager } from "../SchedulerManager";
3
+
4
+ export function getDistributedLockInfo(manager: SchedulerManager): {
5
+ enabled: boolean;
6
+ heldLocks: number;
7
+ config: DistributedLockConfig;
8
+ } {
9
+ return {
10
+ enabled: manager.config.distributedLocking !== false,
11
+ heldLocks: manager.distributedLock.getHeldLockCount(),
12
+ config: manager.distributedLock.getConfig(),
13
+ };
14
+ }
15
+
16
+ export function isDistributedLockingEnabled(manager: SchedulerManager): boolean {
17
+ return manager.config.distributedLocking !== false;
18
+ }
19
+
20
+ export function syncLockConfig(manager: SchedulerManager): void {
21
+ manager.distributedLock.updateConfig({
22
+ enabled: manager.config.distributedLocking ?? true,
23
+ enableLogging: manager.config.enableLogging,
24
+ lockTimeout: manager.config.lockTimeout ?? 0,
25
+ retryInterval: manager.config.lockRetryInterval ?? 100,
26
+ });
27
+ }
@@ -0,0 +1,14 @@
1
+ import type { SchedulerMetrics, TaskMetrics } from "../../types/scheduler.types";
2
+ import type { SchedulerManager } from "../SchedulerManager";
3
+
4
+ export function getMetrics(manager: SchedulerManager): SchedulerMetrics {
5
+ return { ...manager.metrics };
6
+ }
7
+
8
+ export function getTaskMetrics(manager: SchedulerManager, taskId: string): TaskMetrics | null {
9
+ return manager.metrics.taskMetrics[taskId] || null;
10
+ }
11
+
12
+ export function getAllTaskMetrics(manager: SchedulerManager): Record<string, TaskMetrics> {
13
+ return { ...manager.metrics.taskMetrics };
14
+ }