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,114 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { RemoteManager } from "../../../core/remote/RemoteManager";
3
- import { MockRedisStreamServer } from "../../helpers/MockRedisStreamServer";
4
- import { createMockRedisFactory } from "../../helpers/MockRedisClient";
5
-
6
- const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
7
-
8
- describe("Event round-trip over mock Redis", () => {
9
- let server: MockRedisStreamServer;
10
- let appA: RemoteManager;
11
- let appB: RemoteManager;
12
-
13
- beforeEach(async () => {
14
- server = new MockRedisStreamServer();
15
- appA = new RemoteManager({
16
- appName: "app-a",
17
- redisFactory: createMockRedisFactory(server),
18
- blockMs: 50,
19
- autoClaimIdleMs: 0, // skip orphan reclaim
20
- dlqMaxDeliveries: 0, // no DLQ for basic tests
21
- });
22
- appB = new RemoteManager({
23
- appName: "app-b",
24
- redisFactory: createMockRedisFactory(server),
25
- blockMs: 50,
26
- autoClaimIdleMs: 0,
27
- dlqMaxDeliveries: 0,
28
- });
29
- await appA.start();
30
- await appB.start();
31
- });
32
-
33
- afterEach(async () => {
34
- await appA.shutdown();
35
- await appB.shutdown();
36
- });
37
-
38
- test("emit from A is received by B's handler", async () => {
39
- const received: any[] = [];
40
- appB.on(
41
- "order.created",
42
- async (data, ctx) => {
43
- received.push({ data, sourceApp: ctx.sourceApp });
44
- },
45
- "h1"
46
- );
47
-
48
- await appA.emit("app-b", "order.created", { orderId: "abc" });
49
- await wait(150);
50
-
51
- expect(received).toHaveLength(1);
52
- expect(received[0].data).toEqual({ orderId: "abc" });
53
- expect(received[0].sourceApp).toBe("app-a");
54
- });
55
-
56
- test("handler receives ctx.attempt=1 on first delivery", async () => {
57
- const attempts: number[] = [];
58
- appB.on(
59
- "x",
60
- async (_data, ctx) => {
61
- attempts.push(ctx.attempt);
62
- },
63
- "h1"
64
- );
65
- await appA.emit("app-b", "x", {});
66
- await wait(150);
67
- expect(attempts).toEqual([1]);
68
- });
69
-
70
- test("no-handler event is ACKed silently", async () => {
71
- await appA.emit("app-b", "unhandled.event", {});
72
- await wait(150);
73
- // PEL should be empty after ACK
74
- expect(server.getPelSize("remote:app-b", "app-b")).toBe(0);
75
- const snap = appB.getMetrics();
76
- expect(snap.events.noHandler).toBeGreaterThan(0);
77
- });
78
-
79
- test("multiple handlers all fire for one event", async () => {
80
- const log: string[] = [];
81
- appB.on("e", async () => { log.push("h1"); }, "h1");
82
- appB.on("e", async () => { log.push("h2"); }, "h2");
83
- await appA.emit("app-b", "e", {});
84
- await wait(150);
85
- expect(log.sort()).toEqual(["h1", "h2"]);
86
- });
87
-
88
- test("handler failure leaves message in PEL (no ACK)", async () => {
89
- appB.on(
90
- "fail",
91
- async () => {
92
- throw new Error("handler boom");
93
- },
94
- "h1"
95
- );
96
- await appA.emit("app-b", "fail", {});
97
- await wait(150);
98
- expect(server.getPelSize("remote:app-b", "app-b")).toBe(1);
99
- const snap = appB.getMetrics();
100
- expect(snap.events.handlerFailed).toBe(1);
101
- });
102
-
103
- test("metrics reflect emit + receive counters", async () => {
104
- appB.on("m", async () => {}, "h1");
105
- await appA.emit("app-b", "m", {});
106
- await appA.emit("app-b", "m", {});
107
- await wait(200);
108
-
109
- const aSnap = appA.getMetrics();
110
- const bSnap = appB.getMetrics();
111
- expect(aSnap.emit.direct).toBe(2);
112
- expect(bSnap.events.handled).toBe(2);
113
- });
114
- });
@@ -1,130 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { RemoteManager } from "../../../core/remote/RemoteManager";
3
- import { MockRedisStreamServer } from "../../helpers/MockRedisStreamServer";
4
- import { createMockRedisFactory } from "../../helpers/MockRedisClient";
5
- import db from "../../../database";
6
-
7
- const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
8
-
9
- describe("Transactional Outbox", () => {
10
- let server: MockRedisStreamServer;
11
- let app: RemoteManager;
12
-
13
- beforeEach(async () => {
14
- // Clear any residual outbox rows from earlier tests
15
- try {
16
- await db`DELETE FROM remote_outbox`;
17
- } catch {
18
- /* table may not exist yet */
19
- }
20
- server = new MockRedisStreamServer();
21
- app = new RemoteManager({
22
- appName: "app",
23
- redisFactory: createMockRedisFactory(server),
24
- blockMs: 30,
25
- autoClaimIdleMs: 0,
26
- dlqMaxDeliveries: 0,
27
- shutdownDrainMs: 100,
28
- enableOutbox: true,
29
- outboxPollIntervalMs: 50,
30
- outboxBatchSize: 10,
31
- });
32
- await app.start();
33
- });
34
-
35
- afterEach(async () => {
36
- await app.shutdown();
37
- });
38
-
39
- test("emit({ trx }) inserts outbox row within transaction", async () => {
40
- await (db as any).begin(async (trx: any) => {
41
- const id = await app.emit(
42
- "downstream",
43
- "order.created",
44
- { orderId: "abc" },
45
- { trx }
46
- );
47
- expect(id).toBeDefined();
48
- });
49
-
50
- const rows = await db`
51
- SELECT target, event, data, published_at
52
- FROM remote_outbox
53
- `;
54
- expect(rows.length).toBe(1);
55
- expect(rows[0]!.target).toBe("downstream");
56
- expect(rows[0]!.event).toBe("order.created");
57
- });
58
-
59
- test("worker publishes committed rows to Redis + marks published_at", async () => {
60
- await (db as any).begin(async (trx: any) => {
61
- await app.emit(
62
- "downstream",
63
- "published.event",
64
- { n: 1 },
65
- { trx }
66
- );
67
- });
68
- await wait(200); // allow at least one poll tick
69
-
70
- // Row should now be marked published
71
- const rows = await db`
72
- SELECT published_at FROM remote_outbox
73
- WHERE event = 'published.event'
74
- `;
75
- expect(rows[0]!.published_at).not.toBeNull();
76
-
77
- // Stream should have the message
78
- expect(server.getStreamLength("remote:downstream")).toBeGreaterThanOrEqual(
79
- 1
80
- );
81
- });
82
-
83
- test("rolled-back transaction suppresses publish", async () => {
84
- try {
85
- await (db as any).begin(async (trx: any) => {
86
- await app.emit(
87
- "downstream",
88
- "rolled.back",
89
- { n: 2 },
90
- { trx }
91
- );
92
- throw new Error("force rollback");
93
- });
94
- } catch {
95
- /* expected */
96
- }
97
- await wait(200);
98
-
99
- const rows = await db`
100
- SELECT COUNT(*)::int AS c FROM remote_outbox
101
- WHERE event = 'rolled.back'
102
- `;
103
- expect(rows[0]!.c).toBe(0);
104
- expect(server.getStreamLength("remote:downstream")).toBe(0);
105
- });
106
-
107
- test("direct emit + outbox emit hit same target stream", async () => {
108
- await app.emit("other", "direct", { n: "a" });
109
- await (db as any).begin(async (trx: any) => {
110
- await app.emit("other", "outbox", { n: "b" }, { trx });
111
- });
112
- await wait(200);
113
-
114
- const entries = server.xrange("remote:other", "-", "+");
115
- expect(entries.length).toBe(2);
116
- });
117
-
118
- test("metrics count emit.direct vs emit.outbox separately", async () => {
119
- await app.emit("dst", "direct", {});
120
- await (db as any).begin(async (trx: any) => {
121
- await app.emit("dst", "outbox", {}, { trx });
122
- });
123
- await wait(150);
124
-
125
- const snap = app.getMetrics();
126
- expect(snap.emit.direct).toBe(1);
127
- expect(snap.emit.outbox).toBe(1);
128
- expect(snap.outbox.published).toBeGreaterThanOrEqual(1);
129
- });
130
- });
@@ -1,177 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { RemoteManager } from "../../../core/remote/RemoteManager";
3
- import { RemoteError } from "../../../core/remote/types";
4
- import { MockRedisStreamServer } from "../../helpers/MockRedisStreamServer";
5
- import { createMockRedisFactory } from "../../helpers/MockRedisClient";
6
-
7
- describe("RPC round-trip", () => {
8
- let server: MockRedisStreamServer;
9
- let client: RemoteManager;
10
- let server_app: RemoteManager;
11
-
12
- beforeEach(async () => {
13
- server = new MockRedisStreamServer();
14
- client = new RemoteManager({
15
- appName: "client-app",
16
- redisFactory: createMockRedisFactory(server),
17
- blockMs: 50,
18
- autoClaimIdleMs: 0,
19
- dlqMaxDeliveries: 0,
20
- shutdownDrainMs: 100,
21
- defaultCallTimeout: 1000,
22
- });
23
- server_app = new RemoteManager({
24
- appName: "server-app",
25
- redisFactory: createMockRedisFactory(server),
26
- blockMs: 50,
27
- autoClaimIdleMs: 0,
28
- dlqMaxDeliveries: 0,
29
- shutdownDrainMs: 100,
30
- });
31
- await client.start();
32
- await server_app.start();
33
- });
34
-
35
- afterEach(async () => {
36
- await client.shutdown();
37
- await server_app.shutdown();
38
- });
39
-
40
- test("call() returns handler result", async () => {
41
- server_app.onRpc(
42
- "order.get",
43
- async (data: any) => ({ id: data.id, status: "ok" }),
44
- "h1"
45
- );
46
- const result = await client.call<{ id: string; status: string }>(
47
- "server-app",
48
- "order.get",
49
- { id: "abc" }
50
- );
51
- expect(result).toEqual({ id: "abc", status: "ok" });
52
- });
53
-
54
- test("call() rejects with TIMEOUT when no handler registered", async () => {
55
- // server_app has no handler — still returns NOT_FOUND, not TIMEOUT
56
- try {
57
- await client.call(
58
- "server-app",
59
- "nonexistent.method",
60
- {},
61
- { timeout: 500 }
62
- );
63
- throw new Error("expected throw");
64
- } catch (err) {
65
- expect(err).toBeInstanceOf(RemoteError);
66
- expect((err as RemoteError).code).toBe("NOT_FOUND");
67
- }
68
- });
69
-
70
- test("call() rejects with TIMEOUT when target app down", async () => {
71
- // Kill server_app — no consumer for the request stream
72
- await server_app.shutdown();
73
- try {
74
- await client.call(
75
- "server-app",
76
- "anything",
77
- {},
78
- { timeout: 200 }
79
- );
80
- throw new Error("expected throw");
81
- } catch (err) {
82
- expect(err).toBeInstanceOf(RemoteError);
83
- expect((err as RemoteError).code).toBe("TIMEOUT");
84
- }
85
- });
86
-
87
- test("call() to broadcast target * rejects INVALID_TARGET", async () => {
88
- try {
89
- await client.call("*", "anything", {});
90
- throw new Error("expected throw");
91
- } catch (err) {
92
- expect(err).toBeInstanceOf(RemoteError);
93
- expect((err as RemoteError).code).toBe("INVALID_TARGET");
94
- }
95
- });
96
-
97
- test("handler exception propagates as HANDLER_ERROR", async () => {
98
- server_app.onRpc(
99
- "fail",
100
- async () => {
101
- throw new Error("something bad");
102
- },
103
- "h1"
104
- );
105
- try {
106
- await client.call("server-app", "fail", {});
107
- throw new Error("expected throw");
108
- } catch (err) {
109
- expect(err).toBeInstanceOf(RemoteError);
110
- expect((err as RemoteError).code).toBe("HANDLER_ERROR");
111
- }
112
- });
113
-
114
- test("handler custom RemoteError code flows through", async () => {
115
- server_app.onRpc(
116
- "forbidden",
117
- async () => {
118
- throw new RemoteError("no", {
119
- code: "FORBIDDEN",
120
- extensions: { reason: "test" },
121
- });
122
- },
123
- "h1"
124
- );
125
- try {
126
- await client.call("server-app", "forbidden", {});
127
- throw new Error("expected throw");
128
- } catch (err) {
129
- expect(err).toBeInstanceOf(RemoteError);
130
- const re = err as RemoteError;
131
- expect(re.code).toBe("FORBIDDEN");
132
- expect(re.extensions).toEqual({ reason: "test" });
133
- }
134
- });
135
-
136
- test("ctx carries correlationId + deadline", async () => {
137
- let captured: any = null;
138
- server_app.onRpc(
139
- "ctx-check",
140
- async (_data, ctx) => {
141
- captured = {
142
- correlationId: ctx.correlationId,
143
- deadline: ctx.deadline,
144
- sourceApp: ctx.sourceApp,
145
- };
146
- return null;
147
- },
148
- "h1"
149
- );
150
- await client.call("server-app", "ctx-check", {}, { timeout: 2000 });
151
- expect(captured.correlationId).toMatch(/^[0-9a-f-]{36}$/);
152
- expect(captured.deadline).toBeInstanceOf(Date);
153
- expect(captured.sourceApp).toBe("client-app");
154
- });
155
-
156
- test("metrics track successful + failed RPCs", async () => {
157
- server_app.onRpc("ok", async () => "x", "h1");
158
- server_app.onRpc(
159
- "bad",
160
- async () => {
161
- throw new Error("x");
162
- },
163
- "h2"
164
- );
165
- await client.call("server-app", "ok", {});
166
- await client.call("server-app", "bad", {}).catch(() => {});
167
-
168
- const clientSnap = client.getMetrics();
169
- expect(clientSnap.rpc.called).toBe(2);
170
- expect(clientSnap.rpc.succeeded).toBe(1);
171
- expect(clientSnap.rpc.failed).toBe(1);
172
-
173
- const serverSnap = server_app.getMetrics();
174
- expect(serverSnap.rpc.handlerExecuted).toBe(1);
175
- expect(serverSnap.rpc.handlerFailed).toBe(1);
176
- });
177
- });
@@ -1,62 +0,0 @@
1
- /**
2
- * PGlite wrapper script for zero-infrastructure testing.
3
- *
4
- * Starts an in-memory PostgreSQL via PGlite Socket, then spawns
5
- * `bun test` with the correct env vars already set at the process level.
6
- * This avoids all preload ordering issues.
7
- *
8
- * Usage:
9
- * bun tests/pglite-setup.ts [test-dirs...]
10
- * bun tests/pglite-setup.ts tests/unit/
11
- * bun tests/pglite-setup.ts tests/unit tests/integration tests/graphql
12
- */
13
-
14
- import { PGlite } from '@electric-sql/pglite';
15
- import { PGLiteSocketServer } from '@electric-sql/pglite-socket';
16
- import { spawn } from 'child_process';
17
-
18
- const PORT = 54321;
19
-
20
- console.log('[pglite] Starting in-memory PostgreSQL...');
21
- const pg = new PGlite();
22
- await pg.waitReady;
23
-
24
- const server = new PGLiteSocketServer({ db: pg, port: PORT });
25
- await server.start();
26
- console.log(`[pglite] Socket server running on port ${PORT}`);
27
-
28
- // Test dirs from CLI args, default to unit + integration + graphql
29
- const testDirs = process.argv.slice(2);
30
- if (testDirs.length === 0) {
31
- testDirs.push('tests/unit', 'tests/integration', 'tests/graphql');
32
- }
33
-
34
- const proc = spawn('bun', ['test', ...testDirs], {
35
- env: {
36
- ...process.env,
37
- USE_PGLITE: 'true',
38
- DB_CONNECTION_URL: '', // Clear to use POSTGRES_* vars
39
- POSTGRES_HOST: 'localhost',
40
- POSTGRES_PORT: String(PORT),
41
- POSTGRES_USER: 'postgres',
42
- POSTGRES_PASSWORD: 'postgres',
43
- POSTGRES_DB: 'postgres',
44
- POSTGRES_MAX_CONNECTIONS: '1',
45
- },
46
- stdio: 'inherit',
47
- cwd: process.cwd(),
48
- });
49
-
50
- proc.on('exit', async (code) => {
51
- console.log('[pglite] Stopping server...');
52
- try { await server.stop(); } catch {}
53
- try { await pg.close(); } catch {}
54
- process.exit(code ?? 1);
55
- });
56
-
57
- proc.on('error', async (err) => {
58
- console.error('[pglite] Failed to spawn bun test:', err);
59
- try { await server.stop(); } catch {}
60
- try { await pg.close(); } catch {}
61
- process.exit(1);
62
- });
package/tests/setup.ts DELETED
@@ -1,164 +0,0 @@
1
- /**
2
- * Global test setup file for BunSane
3
- *
4
- * This file is preloaded before all tests run (configured in bunfig.toml).
5
- * It ensures:
6
- * 1. Environment variables are loaded from .env.test
7
- * 2. Database connection is established and ready
8
- * 3. Base tables exist
9
- * 4. ApplicationLifecycle is set to DATABASE_READY
10
- * 5. EntityManager is ready for operations
11
- * 6. Proper cleanup on exit
12
- */
13
-
14
- import { beforeAll, afterAll } from 'bun:test';
15
- import { file } from 'bun';
16
-
17
- // Load .env.test before anything else (skip when PGlite provides env vars)
18
- if (process.env.USE_PGLITE !== 'true') {
19
- const envTestPath = new URL('../.env.test', import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1');
20
- const envFile = file(envTestPath);
21
- if (await envFile.exists()) {
22
- const envContent = await envFile.text();
23
- for (const line of envContent.split('\n')) {
24
- const trimmed = line.trim();
25
- if (trimmed && !trimmed.startsWith('#')) {
26
- const [key, ...valueParts] = trimmed.split('=');
27
- if (key) {
28
- const value = valueParts.join('=');
29
- process.env[key.trim()] = value.trim();
30
- }
31
- }
32
- }
33
- }
34
- }
35
-
36
- // Suppress verbose logging during tests unless LOG_LEVEL is explicitly set
37
- if (!process.env.LOG_LEVEL) {
38
- process.env.LOG_LEVEL = 'warn';
39
- }
40
-
41
- // Now import the rest after env is loaded
42
- import db from '../database';
43
- import { PrepareDatabase, HasValidBaseTable } from '../database/DatabaseHelper';
44
- import ApplicationLifecycle, { ApplicationPhase } from '../core/ApplicationLifecycle';
45
- import EntityManager from '../core/EntityManager';
46
- import { ComponentRegistry } from '../core/components';
47
- import { CacheManager } from '../core/cache';
48
- import { logger } from '../core/Logger';
49
- import { preparedStatementCache } from '../database/PreparedStatementCache';
50
-
51
- let isSetupComplete = false;
52
- let setupError: Error | null = null;
53
-
54
- /**
55
- * Initialize the test environment
56
- */
57
- async function initializeTestEnvironment(): Promise<void> {
58
- if (isSetupComplete) return;
59
- if (setupError) throw setupError;
60
-
61
- try {
62
- logger.info({ scope: 'test-setup' }, 'Initializing test environment...');
63
-
64
- // 1. Verify database connection by running a simple query
65
- const connectionTest = await db`SELECT 1 as connected`;
66
- if (!connectionTest || connectionTest.length === 0) {
67
- throw new Error('Database connection failed');
68
- }
69
- logger.info({ scope: 'test-setup' }, 'Database connection verified');
70
-
71
- // 2. Ensure base tables exist
72
- const hasValidTables = await HasValidBaseTable();
73
- if (!hasValidTables) {
74
- logger.info({ scope: 'test-setup' }, 'Creating base database tables...');
75
- await PrepareDatabase();
76
- logger.info({ scope: 'test-setup' }, 'Base database tables created');
77
- } else {
78
- logger.info({ scope: 'test-setup' }, 'Base database tables already exist');
79
- }
80
-
81
- // 3. Set ApplicationLifecycle to DATABASE_READY
82
- ApplicationLifecycle.setPhase(ApplicationPhase.DATABASE_READY);
83
- logger.info({ scope: 'test-setup' }, 'ApplicationLifecycle set to DATABASE_READY');
84
-
85
- // 4. Set EntityManager as ready
86
- (EntityManager as any).dbReady = true;
87
- logger.info({ scope: 'test-setup' }, 'EntityManager marked as ready');
88
-
89
- // 5. Initialize CacheManager with memory provider for tests
90
- const cacheManager = CacheManager.getInstance();
91
- await cacheManager.initialize({
92
- enabled: true,
93
- provider: 'memory',
94
- strategy: 'write-through',
95
- defaultTTL: 3600000,
96
- entity: { enabled: true, ttl: 3600000 },
97
- component: { enabled: true, ttl: 1800000 },
98
- query: { enabled: false, ttl: 300000, maxSize: 10000 }
99
- });
100
- logger.info({ scope: 'test-setup' }, 'CacheManager initialized with memory provider');
101
-
102
- // 6. Clear prepared statement cache to ensure clean slate
103
- preparedStatementCache.clear();
104
-
105
- isSetupComplete = true;
106
- logger.info({ scope: 'test-setup' }, 'Test environment initialization complete');
107
-
108
- } catch (error) {
109
- setupError = error instanceof Error ? error : new Error(String(error));
110
- logger.error({ scope: 'test-setup', error: setupError }, 'Failed to initialize test environment');
111
- throw setupError;
112
- }
113
- }
114
-
115
- /**
116
- * Clean up the test environment
117
- */
118
- async function cleanupTestEnvironment(): Promise<void> {
119
- try {
120
- logger.info({ scope: 'test-setup' }, 'Cleaning up test environment...');
121
-
122
- // Clear caches
123
- try {
124
- const cacheManager = CacheManager.getInstance();
125
- await cacheManager.clear();
126
- } catch {
127
- // Ignore cache cleanup errors
128
- }
129
-
130
- // Clear prepared statement cache
131
- try {
132
- preparedStatementCache.clear();
133
- } catch {
134
- // Ignore errors
135
- }
136
-
137
- // Note: We don't close the database connection pool here because
138
- // Bun's test runner may still need it for parallel test files.
139
- // The connection pool will be cleaned up when the process exits.
140
-
141
- logger.info({ scope: 'test-setup' }, 'Test environment cleanup complete');
142
- } catch (error) {
143
- logger.warn({ scope: 'test-setup', error }, 'Error during test environment cleanup');
144
- }
145
- }
146
-
147
- // Register global hooks (skip for E2E tests that don't need DB)
148
- if (process.env.SKIP_TEST_DB_SETUP !== 'true') {
149
- beforeAll(async () => {
150
- await initializeTestEnvironment();
151
- });
152
-
153
- afterAll(async () => {
154
- await cleanupTestEnvironment();
155
- });
156
- }
157
-
158
- // Export utilities for tests that need them
159
- export { initializeTestEnvironment, cleanupTestEnvironment };
160
-
161
- // Export a helper to ensure setup is complete (for tests that run before beforeAll)
162
- export async function ensureTestSetup(): Promise<void> {
163
- await initializeTestEnvironment();
164
- }