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,159 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- CircuitBreaker,
4
- CircuitOpenError,
5
- } from "../../../core/remote/CircuitBreaker";
6
-
7
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
8
-
9
- describe("CircuitBreaker", () => {
10
- describe("state transitions", () => {
11
- test("starts closed", () => {
12
- const cb = new CircuitBreaker();
13
- expect(cb.getState()).toBe("closed");
14
- });
15
-
16
- test("stays closed below threshold", () => {
17
- const cb = new CircuitBreaker({ threshold: 3 });
18
- cb.recordFailure();
19
- cb.recordFailure();
20
- expect(cb.getState()).toBe("closed");
21
- });
22
-
23
- test("opens at threshold", () => {
24
- const cb = new CircuitBreaker({ threshold: 3 });
25
- cb.recordFailure();
26
- cb.recordFailure();
27
- cb.recordFailure();
28
- expect(cb.getState()).toBe("open");
29
- });
30
-
31
- test("transitions to half-open after reset window", async () => {
32
- const cb = new CircuitBreaker({ threshold: 1, resetTimeoutMs: 50 });
33
- cb.recordFailure();
34
- expect(cb.getState()).toBe("open");
35
- await sleep(60);
36
- expect(cb.getState()).toBe("half-open");
37
- });
38
-
39
- test("half-open success closes breaker", async () => {
40
- const cb = new CircuitBreaker({ threshold: 1, resetTimeoutMs: 50 });
41
- cb.recordFailure();
42
- await sleep(60);
43
- expect(cb.getState()).toBe("half-open");
44
- cb.recordSuccess();
45
- expect(cb.getState()).toBe("closed");
46
- });
47
-
48
- test("half-open failure reopens breaker", async () => {
49
- const cb = new CircuitBreaker({ threshold: 1, resetTimeoutMs: 50 });
50
- cb.recordFailure();
51
- await sleep(60);
52
- expect(cb.getState()).toBe("half-open");
53
- cb.recordFailure();
54
- expect(cb.getState()).toBe("open");
55
- });
56
-
57
- test("success in closed state zeroes failure count", () => {
58
- const cb = new CircuitBreaker({ threshold: 3 });
59
- cb.recordFailure();
60
- cb.recordFailure();
61
- cb.recordSuccess();
62
- cb.recordFailure();
63
- cb.recordFailure();
64
- // Still closed — counter reset to 0 on success, only 2 new failures
65
- expect(cb.getState()).toBe("closed");
66
- });
67
- });
68
-
69
- describe("exec()", () => {
70
- test("passes result on success", async () => {
71
- const cb = new CircuitBreaker();
72
- const result = await cb.exec(async () => 42);
73
- expect(result).toBe(42);
74
- });
75
-
76
- test("records failure on thrown error", async () => {
77
- const cb = new CircuitBreaker({ threshold: 2 });
78
- await expect(
79
- cb.exec(async () => {
80
- throw new Error("boom");
81
- })
82
- ).rejects.toThrow("boom");
83
- expect(cb.getStats().failures).toBe(1);
84
- });
85
-
86
- test("rejects immediately when open", async () => {
87
- const cb = new CircuitBreaker({ threshold: 1 });
88
- await expect(
89
- cb.exec(async () => {
90
- throw new Error("fail");
91
- })
92
- ).rejects.toThrow();
93
- // Now open
94
- await expect(cb.exec(async () => "should not run")).rejects.toBeInstanceOf(
95
- CircuitOpenError
96
- );
97
- });
98
-
99
- test("open-state rejection does not call fn", async () => {
100
- const cb = new CircuitBreaker({ threshold: 1 });
101
- await cb.exec(async () => { throw new Error("x"); }).catch(() => {});
102
- let called = false;
103
- await cb.exec(async () => { called = true; }).catch(() => {});
104
- expect(called).toBe(false);
105
- });
106
- });
107
-
108
- describe("hooks", () => {
109
- test("onTrip fires once when opening", () => {
110
- const cb = new CircuitBreaker({ threshold: 2 });
111
- let trips = 0;
112
- cb.onTrip = () => trips++;
113
- cb.recordFailure();
114
- expect(trips).toBe(0);
115
- cb.recordFailure();
116
- expect(trips).toBe(1);
117
- });
118
-
119
- test("onTrip fires again on half-open→open transition", async () => {
120
- const cb = new CircuitBreaker({ threshold: 1, resetTimeoutMs: 30 });
121
- let trips = 0;
122
- cb.onTrip = () => trips++;
123
- cb.recordFailure();
124
- expect(trips).toBe(1);
125
- await sleep(40);
126
- // half-open trial fails
127
- cb.recordFailure();
128
- expect(trips).toBe(2);
129
- });
130
-
131
- test("onReject fires when exec rejected by open breaker", async () => {
132
- const cb = new CircuitBreaker({ threshold: 1 });
133
- let rejects = 0;
134
- cb.onReject = () => rejects++;
135
- await cb.exec(async () => { throw new Error("x"); }).catch(() => {});
136
- await cb.exec(async () => 1).catch(() => {});
137
- expect(rejects).toBe(1);
138
- });
139
- });
140
-
141
- describe("reset()", () => {
142
- test("force-closes an open breaker", () => {
143
- const cb = new CircuitBreaker({ threshold: 1 });
144
- cb.recordFailure();
145
- expect(cb.getState()).toBe("open");
146
- cb.reset();
147
- expect(cb.getState()).toBe("closed");
148
- expect(cb.getStats().failures).toBe(0);
149
- });
150
- });
151
-
152
- describe("CircuitOpenError", () => {
153
- test("has CIRCUIT_OPEN code", () => {
154
- const err = new CircuitOpenError();
155
- expect(err.code).toBe("CIRCUIT_OPEN");
156
- expect(err).toBeInstanceOf(Error);
157
- });
158
- });
159
- });
@@ -1,55 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { RemoteError } from "../../../core/remote/types";
3
-
4
- describe("RemoteError", () => {
5
- test("carries message + code", () => {
6
- const err = new RemoteError("boom", { code: "X" });
7
- expect(err.message).toBe("boom");
8
- expect(err.code).toBe("X");
9
- });
10
-
11
- test("default name is RemoteError", () => {
12
- const err = new RemoteError("m", { code: "X" });
13
- expect(err.name).toBe("RemoteError");
14
- });
15
-
16
- test("is instanceof Error", () => {
17
- const err = new RemoteError("m", { code: "X" });
18
- expect(err).toBeInstanceOf(Error);
19
- expect(err).toBeInstanceOf(RemoteError);
20
- });
21
-
22
- test("sourceApp + extensions are optional", () => {
23
- const err = new RemoteError("m", { code: "X" });
24
- expect(err.sourceApp).toBeUndefined();
25
- expect(err.extensions).toBeUndefined();
26
- });
27
-
28
- test("sourceApp + extensions propagate", () => {
29
- const err = new RemoteError("m", {
30
- code: "FORBIDDEN",
31
- sourceApp: "orders",
32
- extensions: { userId: "u1", reason: "not-owner" },
33
- });
34
- expect(err.sourceApp).toBe("orders");
35
- expect(err.extensions).toEqual({ userId: "u1", reason: "not-owner" });
36
- });
37
-
38
- test("can be thrown + caught with instanceof narrowing", () => {
39
- try {
40
- throw new RemoteError("nope", { code: "NOT_FOUND" });
41
- } catch (e) {
42
- if (e instanceof RemoteError) {
43
- expect(e.code).toBe("NOT_FOUND");
44
- return;
45
- }
46
- throw new Error("did not narrow");
47
- }
48
- });
49
-
50
- test("stack trace is preserved", () => {
51
- const err = new RemoteError("m", { code: "X" });
52
- expect(err.stack).toBeDefined();
53
- expect(typeof err.stack).toBe("string");
54
- });
55
- });
@@ -1,195 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import {
3
- RemoteEvent,
4
- RemoteRpc,
5
- registerRemoteHandlers,
6
- } from "../../../core/remote/decorators";
7
- import {
8
- setRemoteManager,
9
- getRemoteManager,
10
- } from "../../../core/remote/RemoteManager";
11
- import type { RemoteHandler, RpcHandler } from "../../../core/remote/types";
12
-
13
- describe("@RemoteEvent + @RemoteRpc decorators", () => {
14
- test("RemoteEvent stores handler metadata on constructor", () => {
15
- class S {
16
- @RemoteEvent({ event: "foo.bar" })
17
- handleFooBar() {}
18
- }
19
- const meta = (S as any).__remoteHandlers;
20
- expect(meta).toHaveLength(1);
21
- expect(meta[0]).toMatchObject({
22
- event: "foo.bar",
23
- methodName: "handleFooBar",
24
- kind: "event",
25
- });
26
- expect(meta[0].handlerId).toBe("S.handleFooBar");
27
- });
28
-
29
- test("RemoteRpc stores handler metadata with rpc_request kind", () => {
30
- class R {
31
- @RemoteRpc({ event: "order.get" })
32
- getOrder() {
33
- return { id: "x" };
34
- }
35
- }
36
- const meta = (R as any).__remoteHandlers;
37
- expect(meta).toHaveLength(1);
38
- expect(meta[0].kind).toBe("rpc_request");
39
- expect(meta[0].event).toBe("order.get");
40
- });
41
-
42
- test("custom id overrides default", () => {
43
- class S {
44
- @RemoteEvent({ event: "a", id: "custom-id" })
45
- h() {}
46
- }
47
- const meta = (S as any).__remoteHandlers;
48
- expect(meta[0].handlerId).toBe("custom-id");
49
- });
50
-
51
- test("duplicate handler id on same class is skipped", () => {
52
- class S {
53
- @RemoteEvent({ event: "a", id: "dup" })
54
- h1() {}
55
- @RemoteEvent({ event: "b", id: "dup" })
56
- h2() {}
57
- }
58
- const meta = (S as any).__remoteHandlers;
59
- expect(meta).toHaveLength(1);
60
- expect(meta[0].event).toBe("a"); // first wins
61
- });
62
-
63
- test("mixed RemoteEvent + RemoteRpc coexist on one class", () => {
64
- class S {
65
- @RemoteEvent({ event: "e1" })
66
- onE1() {}
67
- @RemoteRpc({ event: "r1" })
68
- handleR1() {
69
- return 1;
70
- }
71
- }
72
- const meta = (S as any).__remoteHandlers;
73
- expect(meta).toHaveLength(2);
74
- expect(meta.map((m: any) => m.kind).sort()).toEqual([
75
- "event",
76
- "rpc_request",
77
- ]);
78
- });
79
-
80
- test("metadata is isolated per class constructor", () => {
81
- class A {
82
- @RemoteEvent({ event: "a.evt" })
83
- h() {}
84
- }
85
- class B {
86
- @RemoteEvent({ event: "b.evt" })
87
- h() {}
88
- }
89
- expect((A as any).__remoteHandlers).toHaveLength(1);
90
- expect((B as any).__remoteHandlers).toHaveLength(1);
91
- expect((A as any).__remoteHandlers[0].event).toBe("a.evt");
92
- expect((B as any).__remoteHandlers[0].event).toBe("b.evt");
93
- });
94
- });
95
-
96
- describe("registerRemoteHandlers", () => {
97
- beforeEach(() => {
98
- setRemoteManager(null);
99
- });
100
-
101
- afterEach(() => {
102
- setRemoteManager(null);
103
- });
104
-
105
- test("no-op when service has no decorated handlers", () => {
106
- class S {}
107
- // Should not throw, should not touch manager
108
- registerRemoteHandlers(new S());
109
- expect(getRemoteManager()).toBeNull();
110
- });
111
-
112
- test("skips registration when manager is not initialized", () => {
113
- class S {
114
- @RemoteEvent({ event: "x" })
115
- h() {}
116
- }
117
- // No manager set — should warn but not throw
118
- expect(() => registerRemoteHandlers(new S())).not.toThrow();
119
- });
120
-
121
- test("routes event handlers to manager.on()", () => {
122
- const calls: Array<{
123
- event: string;
124
- handlerId: string;
125
- kind: "event" | "rpc";
126
- }> = [];
127
- const mockManager = {
128
- on(event: string, _fn: RemoteHandler, handlerId: string) {
129
- calls.push({ event, handlerId, kind: "event" });
130
- },
131
- onRpc(event: string, _fn: RpcHandler, handlerId: string) {
132
- calls.push({ event, handlerId, kind: "rpc" });
133
- },
134
- } as any;
135
- setRemoteManager(mockManager);
136
-
137
- class S {
138
- @RemoteEvent({ event: "e1" })
139
- ehandler() {}
140
- @RemoteRpc({ event: "r1" })
141
- rhandler() {
142
- return 1;
143
- }
144
- }
145
-
146
- registerRemoteHandlers(new S());
147
-
148
- expect(calls).toHaveLength(2);
149
- expect(calls.find((c) => c.event === "e1")?.kind).toBe("event");
150
- expect(calls.find((c) => c.event === "r1")?.kind).toBe("rpc");
151
- });
152
-
153
- test("handler bound to service instance", async () => {
154
- let receivedThis: any = null;
155
- const mockManager = {
156
- on(_event: string, fn: RemoteHandler, _id: string) {
157
- // Invoke right away to verify binding
158
- fn({} as any, {} as any);
159
- },
160
- onRpc() {},
161
- } as any;
162
- setRemoteManager(mockManager);
163
-
164
- class S {
165
- tag = "instance-tag";
166
- @RemoteEvent({ event: "e1" })
167
- handler() {
168
- receivedThis = this.tag;
169
- }
170
- }
171
-
172
- registerRemoteHandlers(new S());
173
- // Give microtask for any await inside handler to settle
174
- await Promise.resolve();
175
- expect(receivedThis).toBe("instance-tag");
176
- });
177
-
178
- test("missing method on instance is skipped (no throw)", () => {
179
- const mockManager = { on() {}, onRpc() {} } as any;
180
- setRemoteManager(mockManager);
181
-
182
- class S {}
183
- // Inject fake metadata referencing a non-existent method
184
- (S as any).__remoteHandlers = [
185
- {
186
- event: "e1",
187
- methodName: "doesNotExist",
188
- handlerId: "S.doesNotExist",
189
- kind: "event",
190
- },
191
- ];
192
-
193
- expect(() => registerRemoteHandlers(new S())).not.toThrow();
194
- });
195
- });
@@ -1,115 +0,0 @@
1
- import { describe, test, expect, beforeEach } from "bun:test";
2
- import { RemoteMetrics } from "../../../core/remote/metrics";
3
-
4
- describe("RemoteMetrics", () => {
5
- let m: RemoteMetrics;
6
- beforeEach(() => {
7
- m = new RemoteMetrics();
8
- });
9
-
10
- test("fresh snapshot is all zeros", () => {
11
- const snap = m.getSnapshot();
12
- expect(snap.emit).toEqual({ direct: 0, outbox: 0, failed: 0 });
13
- expect(snap.events).toEqual({
14
- received: 0,
15
- handled: 0,
16
- handlerFailed: 0,
17
- noHandler: 0,
18
- dlq: 0,
19
- });
20
- expect(snap.rpc).toEqual({
21
- called: 0,
22
- succeeded: 0,
23
- failed: 0,
24
- timedOut: 0,
25
- handlerExecuted: 0,
26
- handlerFailed: 0,
27
- pastDeadline: 0,
28
- });
29
- expect(snap.outbox).toEqual({ claimed: 0, published: 0, publishFailed: 0 });
30
- expect(snap.circuitBreaker).toEqual({ trips: 0, rejected: 0 });
31
- });
32
-
33
- test("emit counters", () => {
34
- m.emitDirect();
35
- m.emitDirect();
36
- m.emitOutbox();
37
- m.emitFailed();
38
- const s = m.getSnapshot();
39
- expect(s.emit.direct).toBe(2);
40
- expect(s.emit.outbox).toBe(1);
41
- expect(s.emit.failed).toBe(1);
42
- });
43
-
44
- test("event counters", () => {
45
- m.eventReceived();
46
- m.eventHandled();
47
- m.eventHandlerFailed();
48
- m.eventNoHandler();
49
- m.eventDlq();
50
- const s = m.getSnapshot();
51
- expect(s.events).toEqual({
52
- received: 1,
53
- handled: 1,
54
- handlerFailed: 1,
55
- noHandler: 1,
56
- dlq: 1,
57
- });
58
- });
59
-
60
- test("rpc counters", () => {
61
- m.rpcCalled();
62
- m.rpcSucceeded();
63
- m.rpcFailed();
64
- m.rpcTimedOut();
65
- m.rpcHandlerExecuted();
66
- m.rpcHandlerFailed();
67
- m.rpcPastDeadline();
68
- const s = m.getSnapshot();
69
- expect(s.rpc.called).toBe(1);
70
- expect(s.rpc.succeeded).toBe(1);
71
- expect(s.rpc.failed).toBe(1);
72
- expect(s.rpc.timedOut).toBe(1);
73
- expect(s.rpc.handlerExecuted).toBe(1);
74
- expect(s.rpc.handlerFailed).toBe(1);
75
- expect(s.rpc.pastDeadline).toBe(1);
76
- });
77
-
78
- test("outbox claimed is summable, not +1", () => {
79
- m.outboxClaimed(5);
80
- m.outboxClaimed(3);
81
- m.outboxPublished(4);
82
- m.outboxPublishFailed();
83
- const s = m.getSnapshot();
84
- expect(s.outbox.claimed).toBe(8);
85
- expect(s.outbox.published).toBe(4);
86
- expect(s.outbox.publishFailed).toBe(1);
87
- });
88
-
89
- test("circuit breaker counters", () => {
90
- m.cbTripped();
91
- m.cbRejected();
92
- m.cbRejected();
93
- const s = m.getSnapshot();
94
- expect(s.circuitBreaker).toEqual({ trips: 1, rejected: 2 });
95
- });
96
-
97
- test("snapshot is a deep copy", () => {
98
- m.emitDirect();
99
- const s1 = m.getSnapshot();
100
- s1.emit.direct = 9999;
101
- const s2 = m.getSnapshot();
102
- expect(s2.emit.direct).toBe(1);
103
- });
104
-
105
- test("reset zeroes all counters", () => {
106
- m.emitDirect();
107
- m.rpcFailed();
108
- m.eventDlq();
109
- m.reset();
110
- const s = m.getSnapshot();
111
- expect(s.emit.direct).toBe(0);
112
- expect(s.rpc.failed).toBe(0);
113
- expect(s.events.dlq).toBe(0);
114
- });
115
- });
@@ -1,104 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { MockRedisStreamServer } from "../../helpers/MockRedisStreamServer";
3
-
4
- const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
5
-
6
- describe("MockRedisStreamServer", () => {
7
- test("xadd + xlen + xrange basics", () => {
8
- const s = new MockRedisStreamServer();
9
- const id1 = s.xadd("k", "*", "data", "v1");
10
- const id2 = s.xadd("k", "*", "data", "v2");
11
- expect(s.xlen("k")).toBe(2);
12
- const r = s.xrange("k", "-", "+");
13
- expect(r.length).toBe(2);
14
- expect(r[0][0]).toBe(id1);
15
- expect(r[1][0]).toBe(id2);
16
- });
17
-
18
- test("xgroup CREATE MKSTREAM + xreadgroup with > delivers entries", async () => {
19
- const s = new MockRedisStreamServer();
20
- s.xgroup("CREATE", "k", "g", "$", "MKSTREAM");
21
- s.xadd("k", "*", "data", "v1");
22
- const r: any = await s.xreadgroup(
23
- "GROUP", "g", "c1",
24
- "COUNT", 10,
25
- "BLOCK", 50,
26
- "STREAMS", "k", ">"
27
- );
28
- expect(r).not.toBeNull();
29
- expect(r[0][0]).toBe("k");
30
- expect(r[0][1][0][1]).toEqual(["data", "v1"]);
31
- });
32
-
33
- test("xack removes from PEL", async () => {
34
- const s = new MockRedisStreamServer();
35
- s.xgroup("CREATE", "k", "g", "$", "MKSTREAM");
36
- s.xadd("k", "*", "data", "v1");
37
- const r: any = await s.xreadgroup(
38
- "GROUP", "g", "c1",
39
- "COUNT", 10,
40
- "BLOCK", 50,
41
- "STREAMS", "k", ">"
42
- );
43
- const msgId = r[0][1][0][0];
44
- expect(s.getPelSize("k", "g")).toBe(1);
45
- s.xack("k", "g", msgId);
46
- expect(s.getPelSize("k", "g")).toBe(0);
47
- });
48
-
49
- test("xautoclaim claims PEL entries past idle, increments deliveryCount", async () => {
50
- const s = new MockRedisStreamServer();
51
- s.xgroup("CREATE", "k", "g", "$", "MKSTREAM");
52
- s.xadd("k", "*", "data", "v1");
53
- await s.xreadgroup(
54
- "GROUP", "g", "c1",
55
- "COUNT", 10,
56
- "BLOCK", 50,
57
- "STREAMS", "k", ">"
58
- );
59
- await wait(20);
60
- const result: any = s.xautoclaim("k", "g", "c2", 1, "0-0");
61
- expect(result[0]).toBe("0-0");
62
- expect(result[1]).toHaveLength(1);
63
-
64
- const pending: any = s.xpending("k", "g", "-", "+", 100);
65
- expect(pending.length).toBe(1);
66
- expect(pending[0][1]).toBe("c2");
67
- expect(pending[0][3]).toBe(2);
68
- });
69
-
70
- test("xpending detail form returns delivery count", async () => {
71
- const s = new MockRedisStreamServer();
72
- s.xgroup("CREATE", "k", "g", "$", "MKSTREAM");
73
- s.xadd("k", "*", "data", "v1");
74
- const r: any = await s.xreadgroup(
75
- "GROUP", "g", "c1",
76
- "COUNT", 10,
77
- "BLOCK", 50,
78
- "STREAMS", "k", ">"
79
- );
80
- const msgId = r[0][1][0][0];
81
- const detail: any = s.xpending("k", "g", msgId, msgId, 1);
82
- expect(detail[0][0]).toBe(msgId);
83
- expect(detail[0][3]).toBe(1);
84
- });
85
-
86
- test("MAXLEN trims old entries", () => {
87
- const s = new MockRedisStreamServer();
88
- for (let i = 0; i < 5; i++) {
89
- s.xadd("k", "MAXLEN", "~", 2, "*", "data", `v${i}`);
90
- }
91
- expect(s.xlen("k")).toBe(2);
92
- });
93
-
94
- test("xread with $ blocks until new entry", async () => {
95
- const s = new MockRedisStreamServer();
96
- s.xadd("k", "*", "data", "before");
97
- const readPromise = s.xread("COUNT", 10, "BLOCK", 200, "STREAMS", "k", "$");
98
- await wait(30);
99
- s.xadd("k", "*", "data", "after");
100
- const r: any = await readPromise;
101
- expect(r).not.toBeNull();
102
- expect(r[0][1][0][1]).toEqual(["data", "after"]);
103
- });
104
- });