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.
- package/CHANGELOG.md +445 -318
- package/config/cache.config.ts +35 -1
- package/core/App.ts +24 -1064
- package/core/ArcheType.ts +78 -2110
- package/core/BatchLoader.ts +56 -32
- package/core/Entity.ts +85 -1043
- package/core/EntityHookManager.ts +52 -754
- package/core/Logger.ts +10 -0
- package/core/RequestContext.ts +64 -6
- package/core/RequestLoaders.ts +187 -36
- package/core/SchedulerManager.ts +28 -600
- package/core/app/bootstrap.ts +133 -0
- package/core/app/cors.ts +85 -0
- package/core/app/graphqlSetup.ts +56 -0
- package/core/app/healthEndpoints.ts +31 -0
- package/core/app/metricsCollector.ts +27 -0
- package/core/app/preparedStatementWarmup.ts +15 -0
- package/core/app/processHandlers.ts +43 -0
- package/core/app/requestRouter.ts +310 -0
- package/core/app/restRegistry.ts +80 -0
- package/core/app/shutdown.ts +97 -0
- package/core/app/studioRouter.ts +83 -0
- package/core/archetype/customTypes.ts +100 -0
- package/core/archetype/decorators.ts +171 -0
- package/core/archetype/fieldResolvers.ts +666 -0
- package/core/archetype/helpers.ts +29 -0
- package/core/archetype/relationLoader.ts +161 -0
- package/core/archetype/schemaBuilder.ts +141 -0
- package/core/archetype/weaver.ts +218 -0
- package/core/archetype/zodSchemaBuilder.ts +527 -0
- package/core/cache/CacheManager.ts +173 -267
- 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/AccessLog.ts +8 -1
- 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 +20 -5
- package/database/cancellable.ts +35 -0
- package/database/index.ts +15 -3
- package/database/instrumentedDb.ts +141 -0
- 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 +203 -59
- 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/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/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/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 -367
- package/tests/unit/cache/MemoryCache.test.ts +0 -260
- package/tests/unit/cache/RedisCache.test.ts +0 -411
- 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
|
@@ -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
|
-
});
|
package/tests/pglite-setup.ts
DELETED
|
@@ -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
|
-
}
|