@gencow/core 0.1.24 → 0.1.26
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 +45 -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 +47 -41
- 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/dist/db.d.ts +0 -13
- package/dist/db.js +0 -16
- package/src/db.ts +0 -18
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
|
|
19
19
|
import { describe, it, expect, afterAll } from "bun:test";
|
|
20
20
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from "../reactive";
|
|
21
|
+
buildRealtimeCtx,
|
|
22
|
+
handleWsMessage,
|
|
23
|
+
registerClient,
|
|
24
|
+
deregisterClient,
|
|
25
|
+
subscribe,
|
|
26
|
+
} from "../reactive.js";
|
|
27
27
|
|
|
28
28
|
// ─── 최소 WebSocket 서버 (Bun native) ────────────────────────────────────────
|
|
29
29
|
//
|
|
@@ -31,71 +31,80 @@ import {
|
|
|
31
31
|
// 실제 subscribe / handleWsMessage / buildRealtimeCtx를 그대로 사용한다.
|
|
32
32
|
|
|
33
33
|
function startServer(port: number) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
const server = Bun.serve({
|
|
35
|
+
port,
|
|
36
|
+
fetch(req: Request, server: any) {
|
|
37
|
+
if (req.headers.get("upgrade") === "websocket") {
|
|
38
|
+
const upgraded = server.upgrade(req);
|
|
39
|
+
if (!upgraded) return new Response("WS upgrade failed", { status: 500 });
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return new Response("OK");
|
|
43
|
+
},
|
|
44
|
+
websocket: {
|
|
45
|
+
open(ws: any) {
|
|
46
|
+
registerClient(ws);
|
|
47
|
+
},
|
|
48
|
+
message(ws: any, data: string | Buffer) {
|
|
49
|
+
handleWsMessage(ws, typeof data === "string" ? data : data.toString());
|
|
50
|
+
},
|
|
51
|
+
close(ws: any) {
|
|
52
|
+
deregisterClient(ws);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
return server;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// ─── Helper: 클라이언트 연결 + 메시지 수집 ───────────────────────────────────
|
|
60
60
|
|
|
61
61
|
function connectClient(port: number): Promise<{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
ws: WebSocket;
|
|
63
|
+
messages: any[];
|
|
64
|
+
waitForMessage: (filter?: (msg: any) => boolean, timeoutMs?: number) => Promise<any>;
|
|
65
|
+
close: () => void;
|
|
66
66
|
}> {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
69
|
+
const messages: any[] = [];
|
|
70
|
+
const listeners: Array<{
|
|
71
|
+
filter: (msg: any) => boolean;
|
|
72
|
+
resolve: (msg: any) => void;
|
|
73
|
+
reject: (e: any) => void;
|
|
74
|
+
timer: any;
|
|
75
|
+
}> = [];
|
|
76
|
+
|
|
77
|
+
ws.onmessage = (e: MessageEvent) => {
|
|
78
|
+
const msg = JSON.parse(e.data);
|
|
79
|
+
messages.push(msg);
|
|
80
|
+
for (const l of listeners) {
|
|
81
|
+
if (l.filter(msg)) {
|
|
82
|
+
clearTimeout(l.timer);
|
|
83
|
+
l.resolve(msg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
ws.onopen = () =>
|
|
89
|
+
resolve({
|
|
90
|
+
ws,
|
|
91
|
+
messages,
|
|
92
|
+
waitForMessage: (filter = () => true, timeoutMs = 2000) =>
|
|
93
|
+
new Promise((res, rej) => {
|
|
94
|
+
// 이미 수신된 메시지 중 일치하는 것이 있으면 즉시 반환
|
|
95
|
+
const found = messages.find(filter);
|
|
96
|
+
if (found) return res(found);
|
|
97
|
+
const timer = setTimeout(
|
|
98
|
+
() => rej(new Error(`waitForMessage timeout (${timeoutMs}ms)`)),
|
|
99
|
+
timeoutMs,
|
|
100
|
+
);
|
|
101
|
+
listeners.push({ filter, resolve: res, reject: rej, timer });
|
|
102
|
+
}),
|
|
103
|
+
close: () => ws.close(),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
ws.onerror = reject;
|
|
107
|
+
});
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
// ─── 서버 인스턴스 (테스트 파일 당 하나) ─────────────────────────────────────
|
|
@@ -104,201 +113,197 @@ const PORT = 57890; // 충돌 방지용 높은 포트
|
|
|
104
113
|
const server = startServer(PORT);
|
|
105
114
|
|
|
106
115
|
afterAll(() => {
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
// force=true: 열려 있는 WebSocket 연결을 즉시 닫고 서버를 종료한다.
|
|
117
|
+
server.stop(true);
|
|
109
118
|
});
|
|
110
119
|
|
|
111
120
|
// ─── 1. 단일 구독자 실시간 수신 ──────────────────────────────────────────────
|
|
112
121
|
|
|
113
122
|
describe("[WS Integration] 단일 구독자 emit 수신", () => {
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
it("subscribe 후 emit하면 query:updated 메시지를 실시간으로 수신한다", async () => {
|
|
124
|
+
const client = await connectClient(PORT);
|
|
116
125
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
// subscribe 메시지 전송
|
|
127
|
+
client.ws.send(JSON.stringify({ type: "subscribe", query: "ws.tasks.list" }));
|
|
128
|
+
await client.waitForMessage((m) => m.type === "subscribed");
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
// 서버 사이드에서 emit
|
|
131
|
+
const freshData = [{ id: 1, title: "Real WebSocket Task" }];
|
|
132
|
+
buildRealtimeCtx().emit("ws.tasks.list", freshData);
|
|
124
133
|
|
|
125
|
-
|
|
126
|
-
|
|
134
|
+
// query:updated 수신 대기
|
|
135
|
+
const msg = await client.waitForMessage((m) => m.type === "query:updated", 500);
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
137
|
+
expect(msg.query).toBe("ws.tasks.list");
|
|
138
|
+
expect(msg.data).toEqual(freshData);
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
140
|
+
client.close();
|
|
141
|
+
});
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
it("subscribe 전에는 query:updated를 수신하지 않는다", async () => {
|
|
144
|
+
const client = await connectClient(PORT);
|
|
136
145
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
// subscribe 없이 emit
|
|
147
|
+
buildRealtimeCtx().emit("ws.unsubscribed", [{ id: 99 }]);
|
|
148
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
const unrelated = client.messages.filter((m) => m.query === "ws.unsubscribed");
|
|
151
|
+
expect(unrelated).toHaveLength(0);
|
|
143
152
|
|
|
144
|
-
|
|
145
|
-
|
|
153
|
+
client.close();
|
|
154
|
+
});
|
|
146
155
|
});
|
|
147
156
|
|
|
148
157
|
// ─── 2. 다수 구독자 동시 수신 ────────────────────────────────────────────────
|
|
149
158
|
|
|
150
159
|
describe("[WS Integration] 다수 구독자 동시 수신", () => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
it("10명 구독자 모두 동일 query:updated를 수신한다", async () => {
|
|
161
|
+
const N = 10;
|
|
162
|
+
const queryKey = "ws.multi.tasks";
|
|
163
|
+
const clients = await Promise.all(Array.from({ length: N }, () => connectClient(PORT)));
|
|
164
|
+
|
|
165
|
+
// 모두 같은 query 구독
|
|
166
|
+
for (const c of clients) {
|
|
167
|
+
c.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
168
|
+
}
|
|
169
|
+
await Promise.all(clients.map((c) => c.waitForMessage((m) => m.type === "subscribed")));
|
|
170
|
+
|
|
171
|
+
// emit
|
|
172
|
+
const payload = [{ id: 1 }, { id: 2 }, { id: 3 }];
|
|
173
|
+
buildRealtimeCtx().emit(queryKey, payload);
|
|
174
|
+
|
|
175
|
+
// 모두 수신 대기
|
|
176
|
+
const results = await Promise.all(
|
|
177
|
+
clients.map((c) => c.waitForMessage((m) => m.type === "query:updated", 500)),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
expect(results).toHaveLength(N);
|
|
181
|
+
for (const msg of results) {
|
|
182
|
+
expect(msg.query).toBe(queryKey);
|
|
183
|
+
expect(msg.data).toEqual(payload);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
clients.forEach((c) => c.close());
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("50명 구독자 부하 테스트 — 모두 수신, P99 latency 측정", async () => {
|
|
190
|
+
const N = 50;
|
|
191
|
+
const queryKey = "ws.load.tasks";
|
|
192
|
+
|
|
193
|
+
const clients = await Promise.all(Array.from({ length: N }, () => connectClient(PORT)));
|
|
194
|
+
for (const c of clients) {
|
|
195
|
+
c.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
196
|
+
}
|
|
197
|
+
await Promise.all(clients.map((c) => c.waitForMessage((m) => m.type === "subscribed")));
|
|
198
|
+
|
|
199
|
+
const emitTime = performance.now();
|
|
200
|
+
buildRealtimeCtx().emit(queryKey, [{ id: 1, title: "load test" }]);
|
|
201
|
+
|
|
202
|
+
const receivePromises = clients.map(async (c) => {
|
|
203
|
+
const msg = await c.waitForMessage((m) => m.type === "query:updated", 1000);
|
|
204
|
+
return performance.now() - emitTime;
|
|
180
205
|
});
|
|
181
206
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const queryKey = "ws.load.tasks";
|
|
185
|
-
|
|
186
|
-
const clients = await Promise.all(
|
|
187
|
-
Array.from({ length: N }, () => connectClient(PORT))
|
|
188
|
-
);
|
|
189
|
-
for (const c of clients) {
|
|
190
|
-
c.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
191
|
-
}
|
|
192
|
-
await Promise.all(clients.map(c => c.waitForMessage(m => m.type === "subscribed")));
|
|
193
|
-
|
|
194
|
-
const emitTime = performance.now();
|
|
195
|
-
buildRealtimeCtx().emit(queryKey, [{ id: 1, title: "load test" }]);
|
|
196
|
-
|
|
197
|
-
const receivePromises = clients.map(async c => {
|
|
198
|
-
const msg = await c.waitForMessage(m => m.type === "query:updated", 1000);
|
|
199
|
-
return performance.now() - emitTime;
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const latencies = await Promise.all(receivePromises);
|
|
203
|
-
latencies.sort((a, b) => a - b);
|
|
207
|
+
const latencies = await Promise.all(receivePromises);
|
|
208
|
+
latencies.sort((a, b) => a - b);
|
|
204
209
|
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
const p50 = latencies[Math.floor(N * 0.5)];
|
|
211
|
+
const p99 = latencies[Math.floor(N * 0.99)];
|
|
207
212
|
|
|
208
|
-
|
|
213
|
+
console.log(`[WS/50 clients] P50=${p50.toFixed(1)}ms P99=${p99.toFixed(1)}ms (debounce=50ms incl.)`);
|
|
209
214
|
|
|
210
|
-
|
|
211
|
-
|
|
215
|
+
expect(latencies).toHaveLength(N); // 50/50 수신
|
|
216
|
+
expect(p99).toBeLessThan(500); // 디바운스 포함 P99 < 500ms
|
|
212
217
|
|
|
213
|
-
|
|
214
|
-
|
|
218
|
+
clients.forEach((c) => c.close());
|
|
219
|
+
});
|
|
215
220
|
});
|
|
216
221
|
|
|
217
222
|
// ─── 3. 실제 debounce 동작 검증 ──────────────────────────────────────────────
|
|
218
223
|
|
|
219
224
|
describe("[WS Integration] debounce 동작 (실제 타이머)", () => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const rt = buildRealtimeCtx();
|
|
226
|
-
for (let i = 0; i < 5; i++) {
|
|
227
|
-
rt.emit("ws.debounce", [{ id: i }]);
|
|
228
|
-
await new Promise(r => setTimeout(r, 8)); // 8ms 간격 → 40ms < 50ms
|
|
229
|
-
}
|
|
225
|
+
it("40ms 내 5번 rapid emit → 단 1회 query:updated 수신", async () => {
|
|
226
|
+
const client = await connectClient(PORT);
|
|
227
|
+
client.ws.send(JSON.stringify({ type: "subscribe", query: "ws.debounce" }));
|
|
228
|
+
await client.waitForMessage((m) => m.type === "subscribed");
|
|
230
229
|
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
const rt = buildRealtimeCtx();
|
|
231
|
+
for (let i = 0; i < 5; i++) {
|
|
232
|
+
rt.emit("ws.debounce", [{ id: i }]);
|
|
233
|
+
await new Promise((r) => setTimeout(r, 8)); // 8ms 간격 → 40ms < 50ms
|
|
234
|
+
}
|
|
233
235
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
// debounce(50ms) + 마진
|
|
237
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
236
238
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
+
const updates = client.messages.filter((m) => m.type === "query:updated" && m.query === "ws.debounce");
|
|
240
|
+
console.log(`[WS/debounce] 5 emits → ${updates.length} messages received`);
|
|
239
241
|
|
|
240
|
-
|
|
241
|
-
});
|
|
242
|
+
expect(updates).toHaveLength(1); // 1번만 전달
|
|
243
|
+
expect(updates[0].data).toEqual([{ id: 4 }]); // 마지막 데이터
|
|
244
|
+
|
|
245
|
+
client.close();
|
|
246
|
+
});
|
|
242
247
|
});
|
|
243
248
|
|
|
244
249
|
// ─── 4. 클라이언트 연결 해제 후 자동 정리 ────────────────────────────────────
|
|
245
250
|
|
|
246
251
|
describe("[WS Integration] disconnect 후 정리", () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
252
|
+
it("disconnected 클라이언트는 다음 emit에서 에러 없이 제거된다", async () => {
|
|
253
|
+
const stableClient = await connectClient(PORT);
|
|
254
|
+
const disconnectClient = await connectClient(PORT);
|
|
255
|
+
|
|
256
|
+
const queryKey = "ws.cleanup.test";
|
|
257
|
+
stableClient.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
258
|
+
disconnectClient.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
259
|
+
|
|
260
|
+
await Promise.all([
|
|
261
|
+
stableClient.waitForMessage((m) => m.type === "subscribed"),
|
|
262
|
+
disconnectClient.waitForMessage((m) => m.type === "subscribed"),
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
// 한 클라이언트 강제 종료
|
|
266
|
+
disconnectClient.close();
|
|
267
|
+
await new Promise((r) => setTimeout(r, 100)); // 서버가 close 처리할 시간
|
|
268
|
+
|
|
269
|
+
// emit 후 stable client는 정상 수신
|
|
270
|
+
buildRealtimeCtx().emit(queryKey, [{ id: 1 }]);
|
|
271
|
+
const msg = await stableClient.waitForMessage((m) => m.type === "query:updated", 500);
|
|
272
|
+
|
|
273
|
+
expect(msg.query).toBe(queryKey);
|
|
274
|
+
stableClient.close();
|
|
275
|
+
});
|
|
271
276
|
});
|
|
272
277
|
|
|
273
278
|
// ─── 5. 부하: 다수 mutation 연속 실행 ────────────────────────────────────────
|
|
274
279
|
|
|
275
280
|
describe("[WS Integration] 연속 mutations 부하", () => {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
281
|
+
it("20 mutations × 각각 query:updated → 구독자가 모두 수신한다", async () => {
|
|
282
|
+
const N_MUTATIONS = 20;
|
|
283
|
+
const queryKey = "ws.mutations.load";
|
|
284
|
+
const client = await connectClient(PORT);
|
|
285
|
+
|
|
286
|
+
client.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
287
|
+
await client.waitForMessage((m) => m.type === "subscribed");
|
|
288
|
+
|
|
289
|
+
// 20번 mutation 실행 (각자 별도 buildRealtimeCtx — 서로 독립적 debounce)
|
|
290
|
+
const promises: Promise<any>[] = [];
|
|
291
|
+
for (let i = 0; i < N_MUTATIONS; i++) {
|
|
292
|
+
const rt = buildRealtimeCtx();
|
|
293
|
+
rt.emit(queryKey, [{ id: i, title: `Mutation ${i}` }]);
|
|
294
|
+
// 각 emit을 60ms 간격으로 — 각자 debounce가 완료된 후 다음 emit
|
|
295
|
+
if (i < N_MUTATIONS - 1) await new Promise((r) => setTimeout(r, 60));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 마지막 debounce 완료 대기
|
|
299
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
300
|
+
|
|
301
|
+
const updates = client.messages.filter((m) => m.type === "query:updated" && m.query === queryKey);
|
|
302
|
+
console.log(`[WS/mutations] ${N_MUTATIONS} mutations → ${updates.length} updates received`);
|
|
303
|
+
|
|
304
|
+
// 각 mutation이 별도 buildRealtimeCtx → 각자 debounce → N번 전달
|
|
305
|
+
expect(updates.length).toBe(N_MUTATIONS);
|
|
306
|
+
|
|
307
|
+
client.close();
|
|
308
|
+
}, 30000); // 20 × 60ms = 1200ms + margin
|
|
304
309
|
});
|