@gencow/core 0.1.24 → 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.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow.js +4 -11
- package/dist/workflows-api.js +5 -12
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -120
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +309 -286
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +69 -5
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +67 -70
- package/src/workflow.ts +99 -116
- package/src/workflows-api.ts +231 -241
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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() {
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
});
|