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