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