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.
- package/CHANGELOG.md +445 -370
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1020
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +94 -85
- package/core/RequestLoaders.ts +98 -5
- package/core/SchedulerManager.ts +28 -600
- package/core/app/cors.ts +2 -11
- package/core/app/preparedStatementWarmup.ts +9 -49
- package/core/app/requestRouter.ts +9 -8
- package/core/app/restRegistry.ts +8 -0
- package/core/archetype/fieldResolvers.ts +85 -40
- package/core/archetype/relationLoader.ts +135 -92
- package/core/cache/CacheManager.ts +91 -302
- package/core/cache/CompressionUtils.ts +34 -3
- package/core/cache/MemoryCache.ts +40 -37
- package/core/cache/RedisCache.ts +4 -4
- package/core/cache/health.ts +30 -0
- package/core/cache/invalidation.ts +96 -0
- package/core/cache/strategies/writeInvalidate.ts +111 -0
- package/core/cache/strategies/writeThrough.ts +233 -0
- package/core/components/BaseComponent.ts +16 -8
- package/core/components/ComponentRegistry.ts +28 -0
- package/core/decorators/IndexedField.ts +1 -1
- package/core/entity/cacheStrategies.ts +97 -0
- package/core/entity/componentAccess.ts +364 -0
- package/core/entity/finders.ts +202 -0
- package/core/entity/pendingOps.ts +72 -0
- package/core/entity/saveEntity.ts +377 -0
- package/core/hooks/dispatcher.ts +439 -0
- package/core/hooks/guards.ts +155 -0
- package/core/hooks/registry.ts +247 -0
- package/core/metadata/definitions/Component.ts +1 -1
- package/core/metadata/index.ts +15 -4
- package/core/middleware/RateLimit.ts +102 -105
- package/core/middleware/RequestId.ts +2 -9
- package/core/middleware/SecurityHeaders.ts +2 -11
- package/core/middleware/headers.ts +28 -0
- package/core/remote/OutboxWorker.ts +213 -183
- package/core/remote/RemoteManager.ts +401 -400
- package/core/remote/types.ts +153 -151
- package/core/requestScope.ts +34 -0
- package/core/scheduler/cronEvaluator.ts +174 -0
- package/core/scheduler/lifecycleHooks.ts +21 -0
- package/core/scheduler/lockCoordinator.ts +27 -0
- package/core/scheduler/metrics.ts +14 -0
- package/core/scheduler/taskRunner.ts +420 -0
- package/database/DatabaseHelper.ts +128 -101
- package/database/IndexingStrategy.ts +72 -2
- package/database/PreparedStatementCache.ts +8 -2
- package/database/cancellable.ts +35 -22
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -141
- package/endpoints/archetypes.ts +2 -8
- package/endpoints/tables.ts +6 -1
- package/gql/index.ts +1 -1
- package/gql/visitors/ResolverGeneratorVisitor.ts +25 -4
- package/package.json +22 -1
- package/query/CTENode.ts +5 -3
- package/query/ComponentInclusionNode.ts +240 -13
- package/query/OrNode.ts +6 -5
- package/query/Query.ts +157 -46
- package/query/QueryContext.ts +6 -0
- package/query/QueryDAG.ts +7 -2
- package/query/membershipSource.ts +66 -0
- package/storage/LocalStorageProvider.ts +8 -3
- package/studio/dist/assets/index-BMZ67Npg.js +254 -0
- package/studio/dist/assets/index-BpbuYz9g.css +1 -0
- package/studio/{index.html → dist/index.html} +3 -2
- package/swagger/generator.ts +11 -1
- package/upload/UploadManager.ts +8 -6
- package/utils/uuid.ts +40 -10
- package/.claude/scheduled_tasks.lock +0 -1
- package/.claude/settings.local.json +0 -47
- package/.prettierrc +0 -4
- package/.serena/memories/architectural-decision-no-dependency-injection.md +0 -76
- package/.serena/memories/architecture.md +0 -154
- package/.serena/memories/cache-interface-refactoring-2026-01-24.md +0 -165
- package/.serena/memories/code_style_and_conventions.md +0 -76
- package/.serena/memories/project_overview.md +0 -43
- package/.serena/memories/schema-dsl-plan.md +0 -107
- package/.serena/memories/suggested_commands.md +0 -80
- package/.serena/memories/typescript-compilation-status.md +0 -54
- package/.serena/project.yml +0 -114
- package/BunSane.jpg +0 -0
- package/CLAUDE.md +0 -198
- package/TODO.md +0 -2
- package/bun.lock +0 -302
- package/bunfig.toml +0 -10
- package/docs/RFC_APP_REFACTOR.md +0 -248
- package/docs/RFC_REFACTOR_TARGETS.md +0 -251
- package/docs/SCALABILITY_PLAN.md +0 -175
- package/studio/bun.lock +0 -482
- package/studio/package.json +0 -39
- package/studio/postcss.config.js +0 -6
- package/studio/src/components/DataTable.tsx +0 -211
- package/studio/src/components/Layout.tsx +0 -13
- package/studio/src/components/PageContainer.tsx +0 -9
- package/studio/src/components/PageHeader.tsx +0 -13
- package/studio/src/components/SearchBar.tsx +0 -57
- package/studio/src/components/Sidebar.tsx +0 -294
- package/studio/src/components/ui/button.tsx +0 -56
- package/studio/src/components/ui/checkbox.tsx +0 -26
- package/studio/src/components/ui/input.tsx +0 -25
- package/studio/src/hooks/useDataTable.ts +0 -131
- package/studio/src/index.css +0 -36
- package/studio/src/lib/api.ts +0 -186
- package/studio/src/lib/utils.ts +0 -13
- package/studio/src/main.tsx +0 -17
- package/studio/src/pages/ArcheType.tsx +0 -239
- package/studio/src/pages/Components.tsx +0 -124
- package/studio/src/pages/EntityInspector.tsx +0 -302
- package/studio/src/pages/QueryRunner.tsx +0 -246
- package/studio/src/pages/Table.tsx +0 -94
- package/studio/src/pages/Welcome.tsx +0 -241
- package/studio/src/routes.tsx +0 -45
- package/studio/src/store/archeTypeSettings.ts +0 -30
- package/studio/src/store/studio.ts +0 -65
- package/studio/src/utils/columnHelpers.tsx +0 -114
- package/studio/studio-instructions.md +0 -81
- package/studio/tailwind.config.js +0 -77
- package/studio/utils.ts +0 -54
- package/studio/vite.config.js +0 -19
- package/tests/benchmark/BENCHMARK_DATABASES_PLAN.md +0 -338
- package/tests/benchmark/bunfig.toml +0 -9
- package/tests/benchmark/fixtures/EcommerceComponents.ts +0 -283
- package/tests/benchmark/fixtures/EcommerceDataGenerators.ts +0 -301
- package/tests/benchmark/fixtures/RelationTracker.ts +0 -159
- package/tests/benchmark/fixtures/index.ts +0 -6
- package/tests/benchmark/index.ts +0 -22
- package/tests/benchmark/noop-preload.ts +0 -3
- package/tests/benchmark/query-lateral-benchmark.test.ts +0 -372
- package/tests/benchmark/runners/BenchmarkLoader.ts +0 -132
- package/tests/benchmark/runners/index.ts +0 -4
- package/tests/benchmark/scenarios/query-benchmarks.test.ts +0 -465
- package/tests/benchmark/scripts/generate-db.ts +0 -344
- package/tests/benchmark/scripts/run-benchmarks.ts +0 -97
- package/tests/e2e/http.test.ts +0 -130
- package/tests/fixtures/archetypes/TestUserArchetype.ts +0 -21
- package/tests/fixtures/components/TestOrder.ts +0 -23
- package/tests/fixtures/components/TestProduct.ts +0 -23
- package/tests/fixtures/components/TestUser.ts +0 -20
- package/tests/fixtures/components/index.ts +0 -6
- package/tests/graphql/SchemaGeneration.test.ts +0 -90
- package/tests/graphql/builders/ResolverBuilder.test.ts +0 -223
- package/tests/graphql/builders/TypeDefBuilder.test.ts +0 -153
- package/tests/helpers/MockRedisClient.ts +0 -113
- package/tests/helpers/MockRedisStreamServer.ts +0 -448
- package/tests/integration/archetype/ArcheType.persistence.test.ts +0 -241
- package/tests/integration/cache/CacheInvalidation.test.ts +0 -259
- package/tests/integration/entity/Entity.persistence.test.ts +0 -333
- package/tests/integration/entity/Entity.saveTimeout.test.ts +0 -110
- package/tests/integration/loaders/RequestLoaders.abort.test.ts +0 -82
- package/tests/integration/query/Query.abort.test.ts +0 -66
- package/tests/integration/query/Query.complexAnalysis.test.ts +0 -557
- package/tests/integration/query/Query.edgeCases.test.ts +0 -595
- package/tests/integration/query/Query.exec.test.ts +0 -576
- package/tests/integration/query/Query.explainAnalyze.test.ts +0 -233
- package/tests/integration/query/Query.jsonbArray.test.ts +0 -214
- package/tests/integration/remote/dlq.test.ts +0 -175
- package/tests/integration/remote/event-dispatch.test.ts +0 -114
- package/tests/integration/remote/outbox.test.ts +0 -130
- package/tests/integration/remote/rpc.test.ts +0 -177
- package/tests/pglite-setup.ts +0 -62
- package/tests/setup.ts +0 -164
- package/tests/stress/BenchmarkRunner.ts +0 -203
- package/tests/stress/DataSeeder.ts +0 -190
- package/tests/stress/StressTestReporter.ts +0 -229
- package/tests/stress/cursor-perf-test.ts +0 -171
- package/tests/stress/fixtures/RealisticComponents.ts +0 -235
- package/tests/stress/fixtures/StressTestComponents.ts +0 -58
- package/tests/stress/index.ts +0 -7
- package/tests/stress/scenarios/query-benchmarks.test.ts +0 -285
- package/tests/stress/scenarios/realistic-scenarios.test.ts +0 -1081
- package/tests/stress/scenarios/timeout-investigation.test.ts +0 -522
- package/tests/unit/BatchLoader.test.ts +0 -196
- package/tests/unit/archetype/ArcheType.test.ts +0 -107
- package/tests/unit/cache/CacheManager.test.ts +0 -498
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- package/tests/unit/database/cancellable.test.ts +0 -81
- package/tests/unit/database/instrumentedDb.test.ts +0 -160
- package/tests/unit/entity/Entity.components.test.ts +0 -317
- package/tests/unit/entity/Entity.drainSideEffects.test.ts +0 -51
- package/tests/unit/entity/Entity.reload.test.ts +0 -63
- package/tests/unit/entity/Entity.requireComponents.test.ts +0 -72
- package/tests/unit/entity/Entity.test.ts +0 -345
- package/tests/unit/gql/depthLimit.test.ts +0 -203
- package/tests/unit/gql/operationMiddleware.test.ts +0 -293
- package/tests/unit/health/Health.test.ts +0 -129
- package/tests/unit/middleware/AccessLog.test.ts +0 -37
- package/tests/unit/middleware/Middleware.test.ts +0 -98
- package/tests/unit/middleware/RequestId.test.ts +0 -54
- package/tests/unit/middleware/SecurityHeaders.test.ts +0 -66
- package/tests/unit/query/FilterBuilder.test.ts +0 -111
- package/tests/unit/query/JsonbArrayBuilder.test.ts +0 -178
- package/tests/unit/query/Query.emptyString.test.ts +0 -69
- package/tests/unit/query/Query.test.ts +0 -310
- package/tests/unit/remote/CircuitBreaker.test.ts +0 -159
- package/tests/unit/remote/RemoteError.test.ts +0 -55
- package/tests/unit/remote/decorators.test.ts +0 -195
- package/tests/unit/remote/metrics.test.ts +0 -115
- package/tests/unit/remote/mockRedisStreamServer.test.ts +0 -104
- package/tests/unit/scheduler/DistributedLock.test.ts +0 -274
- package/tests/unit/scheduler/SchedulerManager.timeBased.test.ts +0 -95
- package/tests/unit/schema/schema-integration.test.ts +0 -426
- package/tests/unit/schema/schema.test.ts +0 -580
- package/tests/unit/storage/S3StorageProvider.test.ts +0 -567
- package/tests/unit/upload/RestUpload.test.ts +0 -267
- package/tests/unit/validateEnv.test.ts +0 -82
- package/tests/utils/entity-tracker.ts +0 -57
- package/tests/utils/index.ts +0 -13
- package/tests/utils/test-context.ts +0 -149
package/core/remote/types.ts
CHANGED
|
@@ -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
|
-
/**
|
|
139
|
-
|
|
140
|
-
/** Circuit breaker
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
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
|
+
}
|