bunsane 0.3.1 → 0.3.2

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 (41) hide show
  1. package/.claude/scheduled_tasks.lock +1 -0
  2. package/CHANGELOG.md +52 -0
  3. package/config/cache.config.ts +35 -1
  4. package/core/App.ts +24 -1064
  5. package/core/ArcheType.ts +78 -2110
  6. package/core/Entity.ts +10 -33
  7. package/core/RequestContext.ts +85 -36
  8. package/core/RequestLoaders.ts +89 -31
  9. package/core/app/bootstrap.ts +133 -0
  10. package/core/app/cors.ts +94 -0
  11. package/core/app/graphqlSetup.ts +56 -0
  12. package/core/app/healthEndpoints.ts +31 -0
  13. package/core/app/metricsCollector.ts +27 -0
  14. package/core/app/preparedStatementWarmup.ts +55 -0
  15. package/core/app/processHandlers.ts +43 -0
  16. package/core/app/requestRouter.ts +309 -0
  17. package/core/app/restRegistry.ts +72 -0
  18. package/core/app/shutdown.ts +97 -0
  19. package/core/app/studioRouter.ts +83 -0
  20. package/core/archetype/customTypes.ts +100 -0
  21. package/core/archetype/decorators.ts +171 -0
  22. package/core/archetype/fieldResolvers.ts +621 -0
  23. package/core/archetype/helpers.ts +29 -0
  24. package/core/archetype/relationLoader.ts +118 -0
  25. package/core/archetype/schemaBuilder.ts +141 -0
  26. package/core/archetype/weaver.ts +218 -0
  27. package/core/archetype/zodSchemaBuilder.ts +527 -0
  28. package/core/cache/CacheManager.ts +126 -9
  29. package/core/middleware/AccessLog.ts +8 -1
  30. package/database/PreparedStatementCache.ts +12 -3
  31. package/database/cancellable.ts +22 -0
  32. package/database/instrumentedDb.ts +141 -0
  33. package/docs/RFC_APP_REFACTOR.md +248 -0
  34. package/docs/RFC_REFACTOR_TARGETS.md +251 -0
  35. package/package.json +1 -1
  36. package/query/Query.ts +53 -20
  37. package/tests/integration/loaders/RequestLoaders.abort.test.ts +82 -0
  38. package/tests/integration/query/Query.abort.test.ts +66 -0
  39. package/tests/unit/cache/CacheManager.test.ts +132 -1
  40. package/tests/unit/database/cancellable.test.ts +81 -0
  41. package/tests/unit/database/instrumentedDb.test.ts +160 -0
@@ -0,0 +1 @@
1
+ {"sessionId":"302022ef-825d-48c8-8ef6-656f1cd141e0","pid":60520,"procStart":"639139204827053470","acquiredAt":1778489386099}
package/CHANGELOG.md CHANGED
@@ -4,6 +4,58 @@ All notable changes to bunsane are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Added (v0.3.2 — AbortSignal propagation + DB observability)
8
+
9
+ - **AbortSignal threading into `Query.exec` + DataLoaders.** Resolvers
10
+ invoked from a GraphQL request now receive the request's `AbortSignal`
11
+ via the request-context plugin. When the framework's 30s wall-clock
12
+ fires (`core/app/requestRouter.ts`), in-flight `db.unsafe()` queries
13
+ are cancelled through Bun's `SQL.Query.cancel()`. Without this an
14
+ aborted request leaked its backend connection into
15
+ `idle in transaction` under pgbouncer transaction-mode pooling,
16
+ cascading into pool starvation under sustained timeout pressure.
17
+ Public surface: `Query.exec({ signal })`, `Query.count({ signal })`,
18
+ `Query.estimatedCount(component, { signal })`,
19
+ `Query.findOneById(id, { signal })`,
20
+ `Query.explainAnalyze(buffers, { signal })`,
21
+ `createRequestLoaders(db, cache?, signal?, perRequest?)`.
22
+ Reuses helper `runWithSignal` extracted to `database/cancellable.ts`
23
+ and shared with the existing `Entity.doSave` / `Entity.doDelete`
24
+ abort paths.
25
+
26
+ - **DB roundtrip observability (`database/instrumentedDb.ts`).** Every
27
+ `db.unsafe()` callsite in `Query.ts`, `RequestLoaders.ts` and the
28
+ shared `PreparedStatementCache.execute` now routes through
29
+ `timedUnsafe`. Tracks `totalCount`, `totalMs`, `maxMs`, `avgMs`,
30
+ `slowCount`, `abortedCount`, `inFlightMax`, plus per-DataLoader-kind
31
+ counters. Exposed at `/metrics` under the new `db` key. Calls over
32
+ `BUNSANE_DB_SLOW_MS` (default 500ms, set 0 to disable warn) log a
33
+ structured `Slow DB call` warning with a SQL snippet.
34
+
35
+ - **Per-request stats on access + timeout logs.** GraphQL request
36
+ context now captures `operationName`, `dataLoaderCalls`
37
+ (entity / component / relation), and `dbQueryCount`. These attach to
38
+ the underlying `Request` via `__bunsaneStats` so the HTTP router's
39
+ catch block and `AccessLog` middleware can include them in every
40
+ log line. The previous `Request failed after 30004ms: POST /graphql`
41
+ log now carries enough fields to identify the offending operation
42
+ without re-running production with a debug build. Timeout warn log
43
+ also includes operation name when reachable.
44
+
45
+ ### Env vars added
46
+
47
+ - `BUNSANE_DB_SLOW_MS` (default `500`) — per-call DB threshold for
48
+ slow log + `slowCount` metric. Set `0` to suppress the warn (stats
49
+ still accumulate).
50
+
51
+ ### Backward compatibility
52
+
53
+ All additions are opt-in. Existing apps see no behavior change:
54
+ `Query.exec()`, `Query.count()`, `createRequestLoaders(db, cache)`,
55
+ and `preparedStatementCache.execute(s, p, db)` retain their pre-0.3.2
56
+ signatures. `/metrics` gains a `db` key (pure addition). Log lines
57
+ gain fields but preserve existing ones.
58
+
7
59
  ### Added (HR-Screening ticket batch — BUNSANE-002..006)
8
60
 
9
61
  - **`@ScheduledTask` allows entity-less time-based tasks.** Previously
@@ -31,6 +31,29 @@ export interface CacheConfig {
31
31
  component?: {
32
32
  enabled: boolean;
33
33
  ttl: number;
34
+ /**
35
+ * Cache "absent" component lookups (negative cache) so repeated reads
36
+ * of optional components on the same entity do not re-hit the DB.
37
+ * Default false (opt-in) to preserve prior behavior.
38
+ */
39
+ negativeCacheEnabled?: boolean;
40
+ /**
41
+ * TTL in ms for tombstone entries written for absent components.
42
+ * Defaults to min(component.ttl, 60_000). Keep ≤ ttl so a created
43
+ * row supersedes the tombstone within bounded staleness.
44
+ */
45
+ negativeCacheTtl?: number;
46
+ };
47
+
48
+ /**
49
+ * Negative-only cache for relation lookups (e.g. @HasMany returning []).
50
+ * Empty results are cached with a short TTL; positive results are not
51
+ * cached here (cross-entity invalidation is non-trivial — see RFC
52
+ * H-CACHE-NEG). Bounded staleness window equal to negativeCacheTtl.
53
+ */
54
+ relation?: {
55
+ negativeCacheEnabled?: boolean;
56
+ negativeCacheTtl?: number;
34
57
  };
35
58
 
36
59
  query?: {
@@ -76,7 +99,18 @@ export const defaultCacheConfig: CacheConfig = {
76
99
 
77
100
  component: {
78
101
  enabled: process.env.CACHE_COMPONENT_ENABLED !== 'false', // Default true
79
- ttl: parseInt(process.env.CACHE_COMPONENT_TTL || '1800000') // 30 minutes
102
+ ttl: parseInt(process.env.CACHE_COMPONENT_TTL || '1800000'), // 30 minutes
103
+ negativeCacheEnabled: process.env.CACHE_COMPONENT_NEGATIVE_ENABLED === 'true',
104
+ negativeCacheTtl: process.env.CACHE_COMPONENT_NEGATIVE_TTL
105
+ ? parseInt(process.env.CACHE_COMPONENT_NEGATIVE_TTL)
106
+ : undefined
107
+ },
108
+
109
+ relation: {
110
+ negativeCacheEnabled: process.env.CACHE_RELATION_NEGATIVE_ENABLED === 'true',
111
+ negativeCacheTtl: process.env.CACHE_RELATION_NEGATIVE_TTL
112
+ ? parseInt(process.env.CACHE_RELATION_NEGATIVE_TTL)
113
+ : 60_000
80
114
  },
81
115
 
82
116
  query: {