bunsane 0.2.10 → 0.3.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 +266 -0
- package/config/cache.config.ts +12 -2
- package/core/App.ts +296 -69
- package/core/ApplicationLifecycle.ts +68 -4
- package/core/Entity.ts +407 -256
- package/core/EntityHookManager.ts +88 -21
- package/core/EntityManager.ts +12 -3
- package/core/Logger.ts +4 -0
- package/core/RequestContext.ts +4 -1
- package/core/SchedulerManager.ts +92 -9
- package/core/cache/CacheFactory.ts +3 -1
- package/core/cache/CacheManager.ts +54 -17
- package/core/cache/RedisCache.ts +38 -3
- package/core/decorators/EntityHooks.ts +24 -12
- package/core/middleware/RateLimit.ts +105 -0
- package/core/middleware/index.ts +1 -0
- package/core/remote/OutboxWorker.ts +42 -35
- package/core/scheduler/DistributedLock.ts +22 -7
- package/gql/builders/ResolverBuilder.ts +4 -4
- package/gql/complexityLimit.ts +95 -0
- package/gql/index.ts +15 -3
- package/gql/visitors/ResolverGeneratorVisitor.ts +16 -2
- package/package.json +1 -1
- package/query/ComponentInclusionNode.ts +13 -6
- package/query/OrNode.ts +2 -4
- package/query/Query.ts +30 -3
- package/query/SqlIdentifier.ts +105 -0
- package/query/builders/FullTextSearchBuilder.ts +19 -6
- package/service/ServiceRegistry.ts +21 -8
- package/storage/LocalStorageProvider.ts +12 -3
- package/storage/S3StorageProvider.ts +6 -6
- package/tests/e2e/http.test.ts +6 -2
- package/tests/integration/entity/Entity.saveTimeout.test.ts +110 -0
- package/tests/unit/storage/S3StorageProvider.test.ts +6 -10
- package/upload/FileValidator.ts +9 -6
|
@@ -9,6 +9,16 @@ export enum ApplicationPhase {
|
|
|
9
9
|
APPLICATION_READY = "application_ready"
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const PHASE_ORDER: Record<ApplicationPhase, number> = {
|
|
13
|
+
[ApplicationPhase.DATABASE_INITIALIZING]: 0,
|
|
14
|
+
[ApplicationPhase.DATABASE_READY]: 1,
|
|
15
|
+
[ApplicationPhase.COMPONENTS_REGISTERING]: 2,
|
|
16
|
+
[ApplicationPhase.COMPONENTS_READY]: 3,
|
|
17
|
+
[ApplicationPhase.SYSTEM_REGISTERING]: 4,
|
|
18
|
+
[ApplicationPhase.SYSTEM_READY]: 5,
|
|
19
|
+
[ApplicationPhase.APPLICATION_READY]: 6,
|
|
20
|
+
};
|
|
21
|
+
|
|
12
22
|
export interface PhaseChangeEvent extends CustomEvent {
|
|
13
23
|
detail: ApplicationPhase;
|
|
14
24
|
}
|
|
@@ -30,10 +40,29 @@ class ApplicationLifecycle {
|
|
|
30
40
|
private currentPhase = ApplicationPhase.DATABASE_INITIALIZING;
|
|
31
41
|
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Wait for the lifecycle to reach the given phase. Resolves immediately if
|
|
45
|
+
* already at that phase; otherwise attaches a one-shot listener that
|
|
46
|
+
* resolves when the phase is reached. Bounded by `timeoutMs` so callers
|
|
47
|
+
* cannot hang forever when a phase transition fails silently.
|
|
48
|
+
*/
|
|
49
|
+
async waitForPhase(phase: ApplicationPhase, timeoutMs = 30_000): Promise<void> {
|
|
50
|
+
if (this.currentPhase === phase) return;
|
|
51
|
+
return new Promise<void>((resolve, reject) => {
|
|
52
|
+
const timer = setTimeout(() => {
|
|
53
|
+
this.eventEmitter.removeEventListener("phaseChanged", onPhase as EventListener);
|
|
54
|
+
reject(new Error(`waitForPhase(${phase}) timed out after ${timeoutMs}ms; current=${this.currentPhase}`));
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
const onPhase = (event: PhaseChangeEvent) => {
|
|
58
|
+
if (event.detail === phase) {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
this.eventEmitter.removeEventListener("phaseChanged", onPhase as EventListener);
|
|
61
|
+
resolve();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
this.eventEmitter.addEventListener("phaseChanged", onPhase as EventListener);
|
|
65
|
+
});
|
|
37
66
|
}
|
|
38
67
|
|
|
39
68
|
addPhaseListener(listener: (event: PhaseChangeEvent) => void) {
|
|
@@ -45,7 +74,42 @@ class ApplicationLifecycle {
|
|
|
45
74
|
this.eventEmitter.removeEventListener("phaseChanged", listener as EventListener);
|
|
46
75
|
}
|
|
47
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Test / shutdown helper: remove all phase listeners. Call before
|
|
79
|
+
* recreating singletons in tests to prevent listener stacking.
|
|
80
|
+
*/
|
|
81
|
+
removeAllListeners() {
|
|
82
|
+
this.eventEmitter = new EventTarget();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Test helper: reset the current phase to the initial value. Paired with
|
|
87
|
+
* `removeAllListeners()` when re-initializing the lifecycle in tests.
|
|
88
|
+
* Monotonic phase enforcement would otherwise reject re-entering early
|
|
89
|
+
* phases on a second App.init().
|
|
90
|
+
*/
|
|
91
|
+
resetPhase() {
|
|
92
|
+
this.currentPhase = ApplicationPhase.DATABASE_INITIALIZING;
|
|
93
|
+
}
|
|
94
|
+
|
|
48
95
|
setPhase(phase: ApplicationPhase) {
|
|
96
|
+
// Phases are linear: refuse non-monotonic transitions to prevent
|
|
97
|
+
// concurrent callers from clobbering each other's progress (H-LIFE-2).
|
|
98
|
+
// Idempotent re-emits at the same phase are silently ignored.
|
|
99
|
+
const currentRank = PHASE_ORDER[this.currentPhase];
|
|
100
|
+
const nextRank = PHASE_ORDER[phase];
|
|
101
|
+
if (nextRank === undefined) {
|
|
102
|
+
throw new Error(`Unknown application phase: ${phase}`);
|
|
103
|
+
}
|
|
104
|
+
if (nextRank < currentRank) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Non-monotonic phase transition rejected: ${this.currentPhase} → ${phase}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (nextRank === currentRank) {
|
|
110
|
+
// Same phase — no-op, no re-dispatch
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
49
113
|
this.currentPhase = phase;
|
|
50
114
|
this.eventEmitter.dispatchEvent(new CustomEvent("phaseChanged", { detail: phase }));
|
|
51
115
|
}
|