@gencow/core 0.1.23 → 0.1.25

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 (77) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +7 -3
  4. package/dist/index.js +4 -1
  5. package/dist/reactive.js +10 -3
  6. package/dist/retry.js +1 -1
  7. package/dist/rls-db.d.ts +2 -2
  8. package/dist/rls-db.js +1 -5
  9. package/dist/scheduler.d.ts +2 -0
  10. package/dist/scheduler.js +16 -6
  11. package/dist/server.d.ts +0 -1
  12. package/dist/server.js +0 -1
  13. package/dist/storage.js +29 -22
  14. package/dist/v.d.ts +2 -2
  15. package/dist/workflow-types.d.ts +81 -0
  16. package/dist/workflow-types.js +12 -0
  17. package/dist/workflow.d.ts +30 -0
  18. package/dist/workflow.js +150 -0
  19. package/dist/workflows-api.d.ts +13 -0
  20. package/dist/workflows-api.js +321 -0
  21. package/package.json +46 -42
  22. package/src/__tests__/auth.test.ts +90 -86
  23. package/src/__tests__/crons.test.ts +69 -67
  24. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  25. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  26. package/src/__tests__/crud.test.ts +694 -711
  27. package/src/__tests__/dist-exports.test.ts +120 -114
  28. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  29. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  30. package/src/__tests__/fixtures/basic/index.ts +1 -1
  31. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  32. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  33. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  34. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  35. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  36. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  37. package/src/__tests__/helpers/seed-like-fill.ts +50 -44
  38. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  39. package/src/__tests__/httpaction.test.ts +91 -91
  40. package/src/__tests__/image-optimization.test.ts +570 -574
  41. package/src/__tests__/load.test.ts +321 -308
  42. package/src/__tests__/network-sim.test.ts +238 -215
  43. package/src/__tests__/reactive.test.ts +380 -358
  44. package/src/__tests__/retry.test.ts +99 -84
  45. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  46. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  47. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  48. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  49. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  50. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  51. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  52. package/src/__tests__/scheduler-durable.test.ts +117 -117
  53. package/src/__tests__/scheduler-exec.test.ts +258 -246
  54. package/src/__tests__/scheduler.test.ts +129 -111
  55. package/src/__tests__/storage.test.ts +282 -269
  56. package/src/__tests__/tsconfig.json +6 -6
  57. package/src/__tests__/validator.test.ts +236 -232
  58. package/src/__tests__/workflow.test.ts +606 -0
  59. package/src/__tests__/ws-integration.test.ts +223 -218
  60. package/src/__tests__/ws-scale.test.ts +168 -159
  61. package/src/auth-config.ts +18 -18
  62. package/src/auth.ts +106 -106
  63. package/src/crons.ts +77 -77
  64. package/src/crud.ts +523 -479
  65. package/src/index.ts +71 -6
  66. package/src/reactive.ts +357 -331
  67. package/src/retry.ts +51 -54
  68. package/src/rls-db.ts +195 -205
  69. package/src/rls.ts +33 -36
  70. package/src/scheduler.ts +237 -211
  71. package/src/server.ts +0 -1
  72. package/src/storage.ts +632 -593
  73. package/src/v.ts +119 -114
  74. package/src/workflow-types.ts +108 -0
  75. package/src/workflow.ts +188 -0
  76. package/src/workflows-api.ts +415 -0
  77. package/src/db.ts +0 -18
@@ -18,164 +18,178 @@
18
18
  */
19
19
 
20
20
  import { describe, it, expect } from "bun:test";
21
- import { buildRealtimeCtx, subscribe, deregisterClient } from "../reactive";
21
+ import { buildRealtimeCtx, subscribe, deregisterClient } from "../reactive.js";
22
22
 
23
23
  // ─── 네트워크 조건 정의 ──────────────────────────────────────────────────────
24
24
 
25
25
  interface NetworkProfile {
26
- name: string;
27
- p50LatencyMs: number; // 중간값 지연
28
- p99LatencyMs: number; // 99번째 백분위 지연
29
- lossRate: number; // 패킷 손실률 0~1
26
+ name: string;
27
+ p50LatencyMs: number; // 중간값 지연
28
+ p99LatencyMs: number; // 99번째 백분위 지연
29
+ lossRate: number; // 패킷 손실률 0~1
30
30
  }
31
31
 
32
32
  const PROFILES: Record<string, NetworkProfile> = {
33
- LAN: { name: "LAN", p50LatencyMs: 0.5, p99LatencyMs: 2, lossRate: 0.000 },
34
- WAN: { name: "WAN", p50LatencyMs: 30, p99LatencyMs: 120, lossRate: 0.001 },
35
- Mobile: { name: "Mobile 4G", p50LatencyMs: 80, p99LatencyMs: 400, lossRate: 0.020 },
36
- Congested: { name: "혼잡", p50LatencyMs: 200, p99LatencyMs: 2000, lossRate: 0.050 },
33
+ LAN: { name: "LAN", p50LatencyMs: 0.5, p99LatencyMs: 2, lossRate: 0.0 },
34
+ WAN: { name: "WAN", p50LatencyMs: 30, p99LatencyMs: 120, lossRate: 0.001 },
35
+ Mobile: { name: "Mobile 4G", p50LatencyMs: 80, p99LatencyMs: 400, lossRate: 0.02 },
36
+ Congested: { name: "혼잡", p50LatencyMs: 200, p99LatencyMs: 2000, lossRate: 0.05 },
37
37
  };
38
38
 
39
39
  // ─── 네트워크 지연 샘플러 (log-normal 분포 근사) ────────────────────────────
40
40
  // log-normal은 실제 네트워크 지연 분포와 유사함
41
41
 
42
42
  function sampleLatency(p50: number, p99: number): number {
43
- // log-normal: μ = ln(p50), σ 역산
44
- const mu = Math.log(p50);
45
- const sigma = (Math.log(p99) - mu) / 2.326; // z=2.326 = 99th percentile
46
- // Box-Muller transform
47
- const u1 = Math.random();
48
- const u2 = Math.random();
49
- const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
50
- return Math.max(0, Math.exp(mu + sigma * z));
43
+ // log-normal: μ = ln(p50), σ 역산
44
+ const mu = Math.log(p50);
45
+ const sigma = (Math.log(p99) - mu) / 2.326; // z=2.326 = 99th percentile
46
+ // Box-Muller transform
47
+ const u1 = Math.random();
48
+ const u2 = Math.random();
49
+ const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
50
+ return Math.max(0, Math.exp(mu + sigma * z));
51
51
  }
52
52
 
53
53
  // ─── 네트워크 시뮬레이션 WS mock ─────────────────────────────────────────────
54
54
 
55
55
  function makeNetworkWs(profile: NetworkProfile) {
56
- const deliveryTimes: number[] = [];
57
- let dropped = 0;
58
- let sendCallCount = 0;
59
- const sendStart = performance.now();
60
-
61
- return {
62
- send: (msg: string) => {
63
- sendCallCount++;
64
- // 패킷 손실 시뮬레이션
65
- if (Math.random() < profile.lossRate) {
66
- dropped++;
67
- return; // 손실 — 전달 안 됨
68
- }
69
- // 지연 시뮬레이션 (비동기 — 실제 I/O 기다림)
70
- const latency = sampleLatency(profile.p50LatencyMs, profile.p99LatencyMs);
71
- setTimeout(() => {
72
- deliveryTimes.push(performance.now() - sendStart);
73
- }, latency);
74
- },
75
- readyState: 1,
76
- get deliveryTimes() { return deliveryTimes; },
77
- get dropped() { return dropped; },
78
- get sendCallCount() { return sendCallCount; },
79
- } as any;
56
+ const deliveryTimes: number[] = [];
57
+ let dropped = 0;
58
+ let sendCallCount = 0;
59
+ const sendStart = performance.now();
60
+
61
+ return {
62
+ send: (msg: string) => {
63
+ sendCallCount++;
64
+ // 패킷 손실 시뮬레이션
65
+ if (Math.random() < profile.lossRate) {
66
+ dropped++;
67
+ return; // 손실 — 전달 안 됨
68
+ }
69
+ // 지연 시뮬레이션 (비동기 — 실제 I/O 기다림)
70
+ const latency = sampleLatency(profile.p50LatencyMs, profile.p99LatencyMs);
71
+ setTimeout(() => {
72
+ deliveryTimes.push(performance.now() - sendStart);
73
+ }, latency);
74
+ },
75
+ readyState: 1,
76
+ get deliveryTimes() {
77
+ return deliveryTimes;
78
+ },
79
+ get dropped() {
80
+ return dropped;
81
+ },
82
+ get sendCallCount() {
83
+ return sendCallCount;
84
+ },
85
+ } as any;
80
86
  }
81
87
 
82
88
  // ─── 퍼센타일 계산 ───────────────────────────────────────────────────────────
83
89
 
84
90
  function percentile(sorted: number[], p: number): number {
85
- if (sorted.length === 0) return 0;
86
- const idx = Math.ceil((p / 100) * sorted.length) - 1;
87
- return sorted[Math.max(0, idx)];
91
+ if (sorted.length === 0) return 0;
92
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
93
+ return sorted[Math.max(0, idx)];
88
94
  }
89
95
 
90
96
  function stats(times: number[]) {
91
- const sorted = [...times].sort((a, b) => a - b);
92
- return {
93
- count: sorted.length,
94
- p50: percentile(sorted, 50),
95
- p95: percentile(sorted, 95),
96
- p99: percentile(sorted, 99),
97
- max: sorted[sorted.length - 1] ?? 0,
98
- };
97
+ const sorted = [...times].sort((a, b) => a - b);
98
+ return {
99
+ count: sorted.length,
100
+ p50: percentile(sorted, 50),
101
+ p95: percentile(sorted, 95),
102
+ p99: percentile(sorted, 99),
103
+ max: sorted[sorted.length - 1] ?? 0,
104
+ };
99
105
  }
100
106
 
101
- const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
102
- function key() { return `net.${Math.random().toString(36).slice(2)}`; }
107
+ const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
108
+ function key() {
109
+ return `net.${Math.random().toString(36).slice(2)}`;
110
+ }
103
111
 
104
112
  // ─── 1. LAN 환경 ─────────────────────────────────────────────────────────────
105
113
 
106
114
  describe("[NetSim] LAN 환경 (P50=0.5ms, 손실 0%)", () => {
107
- it("1,000 구독자 전체 delivery P99 < 10ms", async () => {
108
- const N = 1_000;
109
- const k = key();
110
- const profile = PROFILES.LAN;
111
- const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
112
- clients.forEach(ws => subscribe(k, ws));
113
-
114
- buildRealtimeCtx().emit(k, [{ id: 1 }]);
115
- await wait(60 + 20); // debounce + max LAN latency
116
-
117
- const allTimes = clients.flatMap(ws => ws.deliveryTimes);
118
- const s = stats(allTimes);
119
- const successRate = (s.count / N) * 100;
120
-
121
- console.log(`[LAN] delivered=${s.count}/${N} (${successRate.toFixed(1)}%), P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`);
122
-
123
- expect(s.count).toBe(N); // 손실 없음
124
- expect(s.p99).toBeLessThan(200); // debounce(50) + LAN P99(2) + margin
125
- clients.forEach(ws => deregisterClient(ws));
126
- });
115
+ it("1,000 구독자 전체 delivery P99 < 10ms", async () => {
116
+ const N = 1_000;
117
+ const k = key();
118
+ const profile = PROFILES.LAN;
119
+ const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
120
+ clients.forEach((ws) => subscribe(k, ws));
121
+
122
+ buildRealtimeCtx().emit(k, [{ id: 1 }]);
123
+ await wait(60 + 20); // debounce + max LAN latency
124
+
125
+ const allTimes = clients.flatMap((ws) => ws.deliveryTimes);
126
+ const s = stats(allTimes);
127
+ const successRate = (s.count / N) * 100;
128
+
129
+ console.log(
130
+ `[LAN] delivered=${s.count}/${N} (${successRate.toFixed(1)}%), P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`,
131
+ );
132
+
133
+ expect(s.count).toBe(N); // 손실 없음
134
+ expect(s.p99).toBeLessThan(200); // debounce(50) + LAN P99(2) + margin
135
+ clients.forEach((ws) => deregisterClient(ws));
136
+ });
127
137
  });
128
138
 
129
139
  // ─── 2. WAN 환경 ─────────────────────────────────────────────────────────────
130
140
 
131
141
  describe("[NetSim] WAN 환경 (P50=30ms, 손실 0.1%)", () => {
132
- it("1,000 구독자 전체 delivery P95 < 300ms, 수신률 99% 이상", async () => {
133
- const N = 1_000;
134
- const k = key();
135
- const profile = PROFILES.WAN;
136
- const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
137
- clients.forEach(ws => subscribe(k, ws));
138
-
139
- buildRealtimeCtx().emit(k, [{ id: 1 }]);
140
- await wait(60 + 300); // debounce + WAN P99
141
-
142
- const allTimes = clients.flatMap(ws => ws.deliveryTimes);
143
- const s = stats(allTimes);
144
- const successRate = (s.count / N) * 100;
145
- const dropped = clients.reduce((a, ws) => a + ws.dropped, 0);
146
-
147
- console.log(`[WAN] delivered=${s.count}/${N} (${successRate.toFixed(2)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`);
148
-
149
- expect(successRate).toBeGreaterThan(99); // 0.1% 손실 → 99% 이상 수신
150
- expect(s.p95).toBeLessThan(300);
151
- clients.forEach(ws => deregisterClient(ws));
152
- });
142
+ it("1,000 구독자 전체 delivery P95 < 300ms, 수신률 99% 이상", async () => {
143
+ const N = 1_000;
144
+ const k = key();
145
+ const profile = PROFILES.WAN;
146
+ const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
147
+ clients.forEach((ws) => subscribe(k, ws));
148
+
149
+ buildRealtimeCtx().emit(k, [{ id: 1 }]);
150
+ await wait(60 + 300); // debounce + WAN P99
151
+
152
+ const allTimes = clients.flatMap((ws) => ws.deliveryTimes);
153
+ const s = stats(allTimes);
154
+ const successRate = (s.count / N) * 100;
155
+ const dropped = clients.reduce((a, ws) => a + ws.dropped, 0);
156
+
157
+ console.log(
158
+ `[WAN] delivered=${s.count}/${N} (${successRate.toFixed(2)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`,
159
+ );
160
+
161
+ expect(successRate).toBeGreaterThan(99); // 0.1% 손실 → 99% 이상 수신
162
+ expect(s.p95).toBeLessThan(300);
163
+ clients.forEach((ws) => deregisterClient(ws));
164
+ });
153
165
  });
154
166
 
155
167
  // ─── 3. 모바일 환경 ──────────────────────────────────────────────────────────
156
168
 
157
169
  describe("[NetSim] Mobile 4G 환경 (P50=80ms, 손실 2%)", () => {
158
- it("500 구독자 전체 delivery P95 < 700ms, 수신률 95% 이상", async () => {
159
- const N = 500;
160
- const k = key();
161
- const profile = PROFILES.Mobile;
162
- const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
163
- clients.forEach(ws => subscribe(k, ws));
164
-
165
- buildRealtimeCtx().emit(k, [{ id: 1 }]);
166
- await wait(60 + 600); // debounce + Mobile P99
167
-
168
- const allTimes = clients.flatMap(ws => ws.deliveryTimes);
169
- const s = stats(allTimes);
170
- const successRate = (s.count / N) * 100;
171
- const dropped = clients.reduce((a, ws) => a + ws.dropped, 0);
172
-
173
- console.log(`[Mobile] delivered=${s.count}/${N} (${successRate.toFixed(2)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`);
174
-
175
- expect(successRate).toBeGreaterThan(95); // 2% 손실 → 95%+ 수신
176
- expect(s.p95).toBeLessThan(700);
177
- clients.forEach(ws => deregisterClient(ws));
178
- });
170
+ it("500 구독자 전체 delivery P95 < 700ms, 수신률 95% 이상", async () => {
171
+ const N = 500;
172
+ const k = key();
173
+ const profile = PROFILES.Mobile;
174
+ const clients = Array.from({ length: N }, () => makeNetworkWs(profile));
175
+ clients.forEach((ws) => subscribe(k, ws));
176
+
177
+ buildRealtimeCtx().emit(k, [{ id: 1 }]);
178
+ await wait(60 + 600); // debounce + Mobile P99
179
+
180
+ const allTimes = clients.flatMap((ws) => ws.deliveryTimes);
181
+ const s = stats(allTimes);
182
+ const successRate = (s.count / N) * 100;
183
+ const dropped = clients.reduce((a, ws) => a + ws.dropped, 0);
184
+
185
+ console.log(
186
+ `[Mobile] delivered=${s.count}/${N} (${successRate.toFixed(2)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms P99=${s.p99.toFixed(1)}ms`,
187
+ );
188
+
189
+ expect(successRate).toBeGreaterThan(95); // 2% 손실 → 95%+ 수신
190
+ expect(s.p95).toBeLessThan(700);
191
+ clients.forEach((ws) => deregisterClient(ws));
192
+ });
179
193
  });
180
194
 
181
195
  // ─── 4. Slow Client (back-pressure) 격리 ─────────────────────────────────────
@@ -184,113 +198,122 @@ describe("[NetSim] Mobile 4G 환경 (P50=80ms, 손실 2%)", () => {
184
198
  // (현재 구현: for loop의 send()가 동기적으로 setTimeout만 등록하므로 격리됨)
185
199
 
186
200
  describe("[NetSim] Slow client back-pressure 격리", () => {
187
- it("일부 slow client가 있어도 fast client 전달 시간은 영향 없다", async () => {
188
- const N_FAST = 900;
189
- const N_SLOW = 100;
190
- const k = key();
191
-
192
- const fastClients = Array.from({ length: N_FAST }, () => makeNetworkWs(PROFILES.LAN));
193
- const slowClients = Array.from({ length: N_SLOW }, () => makeNetworkWs(PROFILES.Congested));
194
-
195
- fastClients.forEach(ws => subscribe(k, ws));
196
- slowClients.forEach(ws => subscribe(k, ws));
197
-
198
- const emitStart = performance.now();
199
- buildRealtimeCtx().emit(k, [{ id: 1 }]);
200
- await wait(60 + 30); // debounce + LAN P99만 기다림 (slow client는 아직 미도달)
201
- const elapsed = performance.now() - emitStart;
202
-
203
- const fastDelivered = fastClients.flatMap(ws => ws.deliveryTimes).length;
204
- const slowDelivered = slowClients.flatMap(ws => ws.deliveryTimes).length;
205
-
206
- const fastRate = (fastDelivered / N_FAST) * 100;
207
- const slowRate = (slowDelivered / N_SLOW) * 100;
208
-
209
- console.log(`[back-pressure] fast: ${fastDelivered}/${N_FAST} (${fastRate.toFixed(1)}%) in ${elapsed.toFixed(1)}ms`);
210
- console.log(`[back-pressure] slow: ${slowDelivered}/${N_SLOW} (${slowRate.toFixed(1)}%) — still in flight`);
211
-
212
- // fast client는 LAN latency 이내에 거의 모두 도달
213
- expect(fastRate).toBeGreaterThan(95);
214
- // slow client는 혼잡 환경이라 아직 미도달 (격리 확인)
215
- expect(slowRate).toBeLessThan(50); // 혼잡 환경 P50=200ms, 아직 30ms만 기다렸으므로
216
-
217
- fastClients.forEach(ws => deregisterClient(ws));
218
- slowClients.forEach(ws => deregisterClient(ws));
219
- });
201
+ it("일부 slow client가 있어도 fast client 전달 시간은 영향 없다", async () => {
202
+ const N_FAST = 900;
203
+ const N_SLOW = 100;
204
+ const k = key();
205
+
206
+ const fastClients = Array.from({ length: N_FAST }, () => makeNetworkWs(PROFILES.LAN));
207
+ const slowClients = Array.from({ length: N_SLOW }, () => makeNetworkWs(PROFILES.Congested));
208
+
209
+ fastClients.forEach((ws) => subscribe(k, ws));
210
+ slowClients.forEach((ws) => subscribe(k, ws));
211
+
212
+ const emitStart = performance.now();
213
+ buildRealtimeCtx().emit(k, [{ id: 1 }]);
214
+ await wait(60 + 30); // debounce + LAN P99만 기다림 (slow client는 아직 미도달)
215
+ const elapsed = performance.now() - emitStart;
216
+
217
+ const fastDelivered = fastClients.flatMap((ws) => ws.deliveryTimes).length;
218
+ const slowDelivered = slowClients.flatMap((ws) => ws.deliveryTimes).length;
219
+
220
+ const fastRate = (fastDelivered / N_FAST) * 100;
221
+ const slowRate = (slowDelivered / N_SLOW) * 100;
222
+
223
+ console.log(
224
+ `[back-pressure] fast: ${fastDelivered}/${N_FAST} (${fastRate.toFixed(1)}%) in ${elapsed.toFixed(1)}ms`,
225
+ );
226
+ console.log(
227
+ `[back-pressure] slow: ${slowDelivered}/${N_SLOW} (${slowRate.toFixed(1)}%) — still in flight`,
228
+ );
229
+
230
+ // fast client는 LAN latency 이내에 거의 모두 도달
231
+ expect(fastRate).toBeGreaterThan(95);
232
+ // slow client는 혼잡 환경이라 아직 미도달 (격리 확인)
233
+ expect(slowRate).toBeLessThan(50); // 혼잡 환경 P50=200ms, 아직 30ms만 기다렸으므로
234
+
235
+ fastClients.forEach((ws) => deregisterClient(ws));
236
+ slowClients.forEach((ws) => deregisterClient(ws));
237
+ });
220
238
  });
221
239
 
222
240
  // ─── 5. 혼합 환경: LAN + WAN + Mobile 혼재 ──────────────────────────────────
223
241
 
224
242
  describe("[NetSim] 혼합 환경 (LAN + WAN + Mobile)", () => {
225
- it("500명 혼합 구독자 전체 delivery → 그룹별 지연 분포 확인", async () => {
226
- const k = key();
227
- const groups = {
228
- lan: { n: 200, profile: PROFILES.LAN, clients: [] as any[] },
229
- wan: { n: 200, profile: PROFILES.WAN, clients: [] as any[] },
230
- mobile: { n: 100, profile: PROFILES.Mobile, clients: [] as any[] },
231
- };
232
-
233
- for (const g of Object.values(groups)) {
234
- g.clients = Array.from({ length: g.n }, () => makeNetworkWs(g.profile));
235
- g.clients.forEach((ws: any) => subscribe(k, ws));
236
- }
237
-
238
- buildRealtimeCtx().emit(k, [{ id: 1 }]);
239
- await wait(60 + 600); // 가장 느린 Mobile P99까지 대기
240
-
241
- for (const [name, g] of Object.entries(groups)) {
242
- const allTimes = g.clients.flatMap((ws: any) => ws.deliveryTimes);
243
- const s = stats(allTimes);
244
- const dropped = g.clients.reduce((a: number, ws: any) => a + ws.dropped, 0);
245
- const successRate = (s.count / g.n) * 100;
246
- console.log(`[mixed/${name}] ${s.count}/${g.n} (${successRate.toFixed(1)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms`);
247
- }
248
-
249
- // 전체 수신률
250
- const totalDelivered = Object.values(groups).reduce(
251
- (a, g) => a + g.clients.flatMap((ws: any) => ws.deliveryTimes).length, 0
252
- );
253
- const totalN = Object.values(groups).reduce((a, g) => a + g.n, 0);
254
- const overallRate = (totalDelivered / totalN) * 100;
255
-
256
- console.log(`[mixed/total] ${totalDelivered}/${totalN} (${overallRate.toFixed(1)}%)`);
257
- expect(overallRate).toBeGreaterThan(97); // Mobile 2% 손실 반영
258
-
259
- for (const g of Object.values(groups)) {
260
- g.clients.forEach((ws: any) => deregisterClient(ws));
261
- }
262
- });
243
+ it("500명 혼합 구독자 전체 delivery → 그룹별 지연 분포 확인", async () => {
244
+ const k = key();
245
+ const groups = {
246
+ lan: { n: 200, profile: PROFILES.LAN, clients: [] as any[] },
247
+ wan: { n: 200, profile: PROFILES.WAN, clients: [] as any[] },
248
+ mobile: { n: 100, profile: PROFILES.Mobile, clients: [] as any[] },
249
+ };
250
+
251
+ for (const g of Object.values(groups)) {
252
+ g.clients = Array.from({ length: g.n }, () => makeNetworkWs(g.profile));
253
+ g.clients.forEach((ws: any) => subscribe(k, ws));
254
+ }
255
+
256
+ buildRealtimeCtx().emit(k, [{ id: 1 }]);
257
+ await wait(60 + 600); // 가장 느린 Mobile P99까지 대기
258
+
259
+ for (const [name, g] of Object.entries(groups)) {
260
+ const allTimes = g.clients.flatMap((ws: any) => ws.deliveryTimes);
261
+ const s = stats(allTimes);
262
+ const dropped = g.clients.reduce((a: number, ws: any) => a + ws.dropped, 0);
263
+ const successRate = (s.count / g.n) * 100;
264
+ console.log(
265
+ `[mixed/${name}] ${s.count}/${g.n} (${successRate.toFixed(1)}%), dropped=${dropped}, P50=${s.p50.toFixed(1)}ms P95=${s.p95.toFixed(1)}ms`,
266
+ );
267
+ }
268
+
269
+ // 전체 수신률
270
+ const totalDelivered = Object.values(groups).reduce(
271
+ (a, g) => a + g.clients.flatMap((ws: any) => ws.deliveryTimes).length,
272
+ 0,
273
+ );
274
+ const totalN = Object.values(groups).reduce((a, g) => a + g.n, 0);
275
+ const overallRate = (totalDelivered / totalN) * 100;
276
+
277
+ console.log(`[mixed/total] ${totalDelivered}/${totalN} (${overallRate.toFixed(1)}%)`);
278
+ expect(overallRate).toBeGreaterThan(97); // Mobile 2% 손실 반영
279
+
280
+ for (const g of Object.values(groups)) {
281
+ g.clients.forEach((ws: any) => deregisterClient(ws));
282
+ }
283
+ });
263
284
  });
264
285
 
265
286
  // ─── 6. debounce + 네트워크 지연 조합 효과 ──────────────────────────────────
266
287
 
267
288
  describe("[NetSim] debounce + 네트워크 지연 조합", () => {
268
- it("100번 rapid emit 후 단 1회 WAN 전송으로 전체 delivery 완료된다", async () => {
269
- const N = 200;
270
- const k = key();
271
- const clients = Array.from({ length: N }, () => makeNetworkWs(PROFILES.WAN));
272
- clients.forEach(ws => subscribe(k, ws));
273
-
274
- const rt = buildRealtimeCtx();
275
- // 30ms 간격으로 10번 emit (각 emit 사이에 짧은 간격, 50ms debounce 이내)
276
- for (let i = 0; i < 10; i++) {
277
- rt.emit(k, [{ id: i, title: `Update ${i}` }]);
278
- await wait(4); // 4ms 간격 → 총 40ms < 50ms debounce window
279
- }
280
-
281
- // 마지막 emit 후 debounce(50ms) + WAN P99(120ms) 대기
282
- await wait(50 + 200);
283
-
284
- const allSendCounts = clients.map(ws => ws.sendCallCount);
285
- const allDelivered = clients.flatMap(ws => ws.deliveryTimes);
286
-
287
- // send()는 debounce로 인해 1번만 호출됨
288
- expect(allSendCounts.every(c => c === 1)).toBe(true);
289
-
290
- const successRate = (allDelivered.length / N) * 100;
291
- console.log(`[debounce+net] sends: 1 per client, WAN delivered: ${allDelivered.length}/${N} (${successRate.toFixed(1)}%)`);
292
-
293
- expect(successRate).toBeGreaterThan(99); // WAN 0.1% 손실
294
- clients.forEach(ws => deregisterClient(ws));
295
- });
289
+ it("100번 rapid emit 후 단 1회 WAN 전송으로 전체 delivery 완료된다", async () => {
290
+ const N = 200;
291
+ const k = key();
292
+ const clients = Array.from({ length: N }, () => makeNetworkWs(PROFILES.WAN));
293
+ clients.forEach((ws) => subscribe(k, ws));
294
+
295
+ const rt = buildRealtimeCtx();
296
+ // 30ms 간격으로 10번 emit (각 emit 사이에 짧은 간격, 50ms debounce 이내)
297
+ for (let i = 0; i < 10; i++) {
298
+ rt.emit(k, [{ id: i, title: `Update ${i}` }]);
299
+ await wait(4); // 4ms 간격 → 총 40ms < 50ms debounce window
300
+ }
301
+
302
+ // 마지막 emit 후 debounce(50ms) + WAN P99(120ms) 대기
303
+ await wait(50 + 200);
304
+
305
+ const allSendCounts = clients.map((ws) => ws.sendCallCount);
306
+ const allDelivered = clients.flatMap((ws) => ws.deliveryTimes);
307
+
308
+ // send()는 debounce로 인해 1번만 호출됨
309
+ expect(allSendCounts.every((c) => c === 1)).toBe(true);
310
+
311
+ const successRate = (allDelivered.length / N) * 100;
312
+ console.log(
313
+ `[debounce+net] sends: 1 per client, WAN delivered: ${allDelivered.length}/${N} (${successRate.toFixed(1)}%)`,
314
+ );
315
+
316
+ expect(successRate).toBeGreaterThan(99); // WAN 0.1% 손실
317
+ clients.forEach((ws) => deregisterClient(ws));
318
+ });
296
319
  });