bunsane 0.3.2 → 0.5.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 (220) hide show
  1. package/CHANGELOG.md +471 -370
  2. package/core/BatchLoader.ts +56 -32
  3. package/core/Entity.ts +93 -1020
  4. package/core/EntityHookManager.ts +52 -754
  5. package/core/Logger.ts +10 -0
  6. package/core/RequestContext.ts +94 -85
  7. package/core/RequestLoaders.ts +98 -5
  8. package/core/SchedulerManager.ts +28 -600
  9. package/core/app/cors.ts +2 -11
  10. package/core/app/preparedStatementWarmup.ts +9 -49
  11. package/core/app/requestRouter.ts +9 -8
  12. package/core/app/restRegistry.ts +8 -0
  13. package/core/archetype/fieldResolvers.ts +85 -40
  14. package/core/archetype/relationLoader.ts +135 -92
  15. package/core/cache/CacheManager.ts +91 -302
  16. package/core/cache/CompressionUtils.ts +34 -3
  17. package/core/cache/MemoryCache.ts +40 -37
  18. package/core/cache/RedisCache.ts +8 -7
  19. package/core/cache/health.ts +30 -0
  20. package/core/cache/invalidation.ts +96 -0
  21. package/core/cache/strategies/writeInvalidate.ts +111 -0
  22. package/core/cache/strategies/writeThrough.ts +233 -0
  23. package/core/components/BaseComponent.ts +25 -10
  24. package/core/components/ComponentRegistry.ts +28 -0
  25. package/core/decorators/IndexedField.ts +1 -1
  26. package/core/entity/cacheStrategies.ts +97 -0
  27. package/core/entity/componentAccess.ts +383 -0
  28. package/core/entity/finders.ts +202 -0
  29. package/core/entity/getCacheManager.ts +10 -0
  30. package/core/entity/pendingOps.ts +72 -0
  31. package/core/entity/saveEntity.ts +375 -0
  32. package/core/health.ts +93 -4
  33. package/core/hooks/dispatcher.ts +439 -0
  34. package/core/hooks/guards.ts +155 -0
  35. package/core/hooks/registry.ts +247 -0
  36. package/core/metadata/definitions/Component.ts +1 -1
  37. package/core/metadata/index.ts +15 -4
  38. package/core/middleware/RateLimit.ts +102 -105
  39. package/core/middleware/RequestId.ts +2 -9
  40. package/core/middleware/SecurityHeaders.ts +2 -11
  41. package/core/middleware/headers.ts +28 -0
  42. package/core/remote/OutboxWorker.ts +213 -183
  43. package/core/remote/RemoteManager.ts +401 -400
  44. package/core/remote/StreamConsumer.ts +535 -535
  45. package/core/remote/types.ts +153 -151
  46. package/core/requestScope.ts +34 -0
  47. package/core/scheduler/cronEvaluator.ts +174 -0
  48. package/core/scheduler/lifecycleHooks.ts +21 -0
  49. package/core/scheduler/lockCoordinator.ts +27 -0
  50. package/core/scheduler/metrics.ts +14 -0
  51. package/core/scheduler/taskRunner.ts +420 -0
  52. package/core/validateEnv.ts +10 -0
  53. package/database/DatabaseHelper.ts +128 -101
  54. package/database/IndexingStrategy.ts +72 -2
  55. package/database/PreparedStatementCache.ts +8 -2
  56. package/database/cancellable.ts +35 -22
  57. package/database/index.ts +29 -3
  58. package/database/instrumentedDb.ts +141 -141
  59. package/database/sqlHelpers.ts +3 -1
  60. package/endpoints/archetypes.ts +2 -8
  61. package/endpoints/tables.ts +6 -1
  62. package/gql/index.ts +1 -1
  63. package/gql/schema/index.ts +15 -4
  64. package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
  65. package/package.json +22 -1
  66. package/query/CTENode.ts +5 -3
  67. package/query/ComponentInclusionNode.ts +245 -14
  68. package/query/OrNode.ts +8 -19
  69. package/query/Query.ts +208 -79
  70. package/query/QueryContext.ts +6 -0
  71. package/query/QueryDAG.ts +7 -2
  72. package/query/membershipSource.ts +66 -0
  73. package/storage/LocalStorageProvider.ts +8 -3
  74. package/studio/dist/assets/index-BMZ67Npg.js +254 -0
  75. package/studio/dist/assets/index-BpbuYz9g.css +1 -0
  76. package/studio/{index.html → dist/index.html} +3 -2
  77. package/swagger/generator.ts +11 -1
  78. package/upload/UploadManager.ts +8 -6
  79. package/utils/uuid.ts +40 -10
  80. package/.claude/scheduled_tasks.lock +0 -1
  81. package/.claude/settings.local.json +0 -47
  82. package/.prettierrc +0 -4
  83. package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
  84. package/.serena/memories/architecture.md +0 -154
  85. package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
  86. package/.serena/memories/code_style_and_conventions.md +0 -76
  87. package/.serena/memories/project_overview.md +0 -43
  88. package/.serena/memories/schema-dsl-plan.md +0 -107
  89. package/.serena/memories/suggested_commands.md +0 -80
  90. package/.serena/memories/typescript-compilation-status.md +0 -54
  91. package/.serena/project.yml +0 -114
  92. package/BunSane.jpg +0 -0
  93. package/CLAUDE.md +0 -198
  94. package/TODO.md +0 -2
  95. package/bun.lock +0 -302
  96. package/bunfig.toml +0 -10
  97. package/docs/RFC_APP_REFACTOR.md +0 -248
  98. package/docs/RFC_REFACTOR_TARGETS.md +0 -251
  99. package/docs/SCALABILITY_PLAN.md +0 -175
  100. package/studio/bun.lock +0 -482
  101. package/studio/package.json +0 -39
  102. package/studio/postcss.config.js +0 -6
  103. package/studio/src/components/DataTable.tsx +0 -211
  104. package/studio/src/components/Layout.tsx +0 -13
  105. package/studio/src/components/PageContainer.tsx +0 -9
  106. package/studio/src/components/PageHeader.tsx +0 -13
  107. package/studio/src/components/SearchBar.tsx +0 -57
  108. package/studio/src/components/Sidebar.tsx +0 -294
  109. package/studio/src/components/ui/button.tsx +0 -56
  110. package/studio/src/components/ui/checkbox.tsx +0 -26
  111. package/studio/src/components/ui/input.tsx +0 -25
  112. package/studio/src/hooks/useDataTable.ts +0 -131
  113. package/studio/src/index.css +0 -36
  114. package/studio/src/lib/api.ts +0 -186
  115. package/studio/src/lib/utils.ts +0 -13
  116. package/studio/src/main.tsx +0 -17
  117. package/studio/src/pages/ArcheType.tsx +0 -239
  118. package/studio/src/pages/Components.tsx +0 -124
  119. package/studio/src/pages/EntityInspector.tsx +0 -302
  120. package/studio/src/pages/QueryRunner.tsx +0 -246
  121. package/studio/src/pages/Table.tsx +0 -94
  122. package/studio/src/pages/Welcome.tsx +0 -241
  123. package/studio/src/routes.tsx +0 -45
  124. package/studio/src/store/archeTypeSettings.ts +0 -30
  125. package/studio/src/store/studio.ts +0 -65
  126. package/studio/src/utils/columnHelpers.tsx +0 -114
  127. package/studio/studio-instructions.md +0 -81
  128. package/studio/tailwind.config.js +0 -77
  129. package/studio/utils.ts +0 -54
  130. package/studio/vite.config.js +0 -19
  131. package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
  132. package/tests/benchmark/bunfig.toml +0 -9
  133. package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
  134. package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
  135. package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
  136. package/tests/benchmark/fixtures/index.ts +0 -6
  137. package/tests/benchmark/index.ts +0 -22
  138. package/tests/benchmark/noop-preload.ts +0 -3
  139. package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
  140. package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
  141. package/tests/benchmark/runners/index.ts +0 -4
  142. package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
  143. package/tests/benchmark/scripts/generate-db.ts +0 -344
  144. package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
  145. package/tests/e2e/http.test.ts +0 -130
  146. package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
  147. package/tests/fixtures/components/TestOrder.ts +0 -23
  148. package/tests/fixtures/components/TestProduct.ts +0 -23
  149. package/tests/fixtures/components/TestUser.ts +0 -20
  150. package/tests/fixtures/components/index.ts +0 -6
  151. package/tests/graphql/SchemaGeneration.test.ts +0 -90
  152. package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
  153. package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
  154. package/tests/helpers/MockRedisClient.ts +0 -113
  155. package/tests/helpers/MockRedisStreamServer.ts +0 -448
  156. package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
  157. package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
  158. package/tests/integration/entity/Entity.persistence.test.ts +0 -333
  159. package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
  160. package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
  161. package/tests/integration/query/Query.abort.test.ts +0 -66
  162. package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
  163. package/tests/integration/query/Query.edgeCases.test.ts +0 -595
  164. package/tests/integration/query/Query.exec.test.ts +0 -576
  165. package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
  166. package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
  167. package/tests/integration/remote/dlq.test.ts +0 -175
  168. package/tests/integration/remote/event-dispatch.test.ts +0 -114
  169. package/tests/integration/remote/outbox.test.ts +0 -130
  170. package/tests/integration/remote/rpc.test.ts +0 -177
  171. package/tests/pglite-setup.ts +0 -62
  172. package/tests/setup.ts +0 -164
  173. package/tests/stress/BenchmarkRunner.ts +0 -203
  174. package/tests/stress/DataSeeder.ts +0 -190
  175. package/tests/stress/StressTestReporter.ts +0 -229
  176. package/tests/stress/cursor-perf-test.ts +0 -171
  177. package/tests/stress/fixtures/RealisticComponents.ts +0 -235
  178. package/tests/stress/fixtures/StressTestComponents.ts +0 -58
  179. package/tests/stress/index.ts +0 -7
  180. package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
  181. package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
  182. package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
  183. package/tests/unit/BatchLoader.test.ts +0 -196
  184. package/tests/unit/archetype/ArcheType.test.ts +0 -107
  185. package/tests/unit/cache/CacheManager.test.ts +0 -498
  186. package/tests/unit/cache/MemoryCache.test.ts +0 -260
  187. package/tests/unit/cache/RedisCache.test.ts +0 -411
  188. package/tests/unit/database/cancellable.test.ts +0 -81
  189. package/tests/unit/database/instrumentedDb.test.ts +0 -160
  190. package/tests/unit/entity/Entity.components.test.ts +0 -317
  191. package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
  192. package/tests/unit/entity/Entity.reload.test.ts +0 -63
  193. package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
  194. package/tests/unit/entity/Entity.test.ts +0 -345
  195. package/tests/unit/gql/depthLimit.test.ts +0 -203
  196. package/tests/unit/gql/operationMiddleware.test.ts +0 -293
  197. package/tests/unit/health/Health.test.ts +0 -129
  198. package/tests/unit/middleware/AccessLog.test.ts +0 -37
  199. package/tests/unit/middleware/Middleware.test.ts +0 -98
  200. package/tests/unit/middleware/RequestId.test.ts +0 -54
  201. package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
  202. package/tests/unit/query/FilterBuilder.test.ts +0 -111
  203. package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
  204. package/tests/unit/query/Query.emptyString.test.ts +0 -69
  205. package/tests/unit/query/Query.test.ts +0 -310
  206. package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
  207. package/tests/unit/remote/RemoteError.test.ts +0 -55
  208. package/tests/unit/remote/decorators.test.ts +0 -195
  209. package/tests/unit/remote/metrics.test.ts +0 -115
  210. package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
  211. package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
  212. package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
  213. package/tests/unit/schema/schema-integration.test.ts +0 -426
  214. package/tests/unit/schema/schema.test.ts +0 -580
  215. package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
  216. package/tests/unit/upload/RestUpload.test.ts +0 -267
  217. package/tests/unit/validateEnv.test.ts +0 -82
  218. package/tests/utils/entity-tracker.ts +0 -57
  219. package/tests/utils/index.ts +0 -13
  220. 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
+ }