@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,12 +18,12 @@
18
18
 
19
19
  import { describe, it, expect, afterAll } from "bun:test";
20
20
  import {
21
- buildRealtimeCtx,
22
- handleWsMessage,
23
- registerClient,
24
- deregisterClient,
25
- subscribe,
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
- 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;
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
- ws: WebSocket;
63
- messages: any[];
64
- waitForMessage: (filter?: (msg: any) => boolean, timeoutMs?: number) => Promise<any>;
65
- close: () => void;
62
+ ws: WebSocket;
63
+ messages: any[];
64
+ waitForMessage: (filter?: (msg: any) => boolean, timeoutMs?: number) => Promise<any>;
65
+ close: () => void;
66
66
  }> {
67
- return new Promise((resolve, reject) => {
68
- const ws = new WebSocket(`ws://localhost:${port}`);
69
- const messages: any[] = [];
70
- const listeners: Array<{ filter: (msg: any) => boolean; resolve: (msg: any) => void; reject: (e: any) => void; timer: any }> = [];
71
-
72
- ws.onmessage = (e: MessageEvent) => {
73
- const msg = JSON.parse(e.data);
74
- messages.push(msg);
75
- for (const l of listeners) {
76
- if (l.filter(msg)) {
77
- clearTimeout(l.timer);
78
- l.resolve(msg);
79
- }
80
- }
81
- };
82
-
83
- ws.onopen = () => resolve({
84
- ws,
85
- messages,
86
- waitForMessage: (filter = () => true, timeoutMs = 2000) =>
87
- new Promise((res, rej) => {
88
- // 이미 수신된 메시지 중 일치하는 것이 있으면 즉시 반환
89
- const found = messages.find(filter);
90
- if (found) return res(found);
91
- const timer = setTimeout(() => rej(new Error(`waitForMessage timeout (${timeoutMs}ms)`)), timeoutMs);
92
- listeners.push({ filter, resolve: res, reject: rej, timer });
93
- }),
94
- close: () => ws.close(),
95
- });
96
-
97
- ws.onerror = reject;
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
- // force=true: 열려 있는 WebSocket 연결을 즉시 닫고 서버를 종료한다.
108
- server.stop(true);
116
+ // force=true: 열려 있는 WebSocket 연결을 즉시 닫고 서버를 종료한다.
117
+ server.stop(true);
109
118
  });
110
119
 
111
120
  // ─── 1. 단일 구독자 실시간 수신 ──────────────────────────────────────────────
112
121
 
113
122
  describe("[WS Integration] 단일 구독자 emit 수신", () => {
114
- it("subscribe 후 emit하면 query:updated 메시지를 실시간으로 수신한다", async () => {
115
- const client = await connectClient(PORT);
123
+ it("subscribe 후 emit하면 query:updated 메시지를 실시간으로 수신한다", async () => {
124
+ const client = await connectClient(PORT);
116
125
 
117
- // subscribe 메시지 전송
118
- client.ws.send(JSON.stringify({ type: "subscribe", query: "ws.tasks.list" }));
119
- await client.waitForMessage(m => m.type === "subscribed");
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
- // 서버 사이드에서 emit
122
- const freshData = [{ id: 1, title: "Real WebSocket Task" }];
123
- buildRealtimeCtx().emit("ws.tasks.list", freshData);
130
+ // 서버 사이드에서 emit
131
+ const freshData = [{ id: 1, title: "Real WebSocket Task" }];
132
+ buildRealtimeCtx().emit("ws.tasks.list", freshData);
124
133
 
125
- // query:updated 수신 대기
126
- const msg = await client.waitForMessage(m => m.type === "query:updated", 500);
134
+ // query:updated 수신 대기
135
+ const msg = await client.waitForMessage((m) => m.type === "query:updated", 500);
127
136
 
128
- expect(msg.query).toBe("ws.tasks.list");
129
- expect(msg.data).toEqual(freshData);
137
+ expect(msg.query).toBe("ws.tasks.list");
138
+ expect(msg.data).toEqual(freshData);
130
139
 
131
- client.close();
132
- });
140
+ client.close();
141
+ });
133
142
 
134
- it("subscribe 전에는 query:updated를 수신하지 않는다", async () => {
135
- const client = await connectClient(PORT);
143
+ it("subscribe 전에는 query:updated를 수신하지 않는다", async () => {
144
+ const client = await connectClient(PORT);
136
145
 
137
- // subscribe 없이 emit
138
- buildRealtimeCtx().emit("ws.unsubscribed", [{ id: 99 }]);
139
- await new Promise(r => setTimeout(r, 200));
146
+ // subscribe 없이 emit
147
+ buildRealtimeCtx().emit("ws.unsubscribed", [{ id: 99 }]);
148
+ await new Promise((r) => setTimeout(r, 200));
140
149
 
141
- const unrelated = client.messages.filter(m => m.query === "ws.unsubscribed");
142
- expect(unrelated).toHaveLength(0);
150
+ const unrelated = client.messages.filter((m) => m.query === "ws.unsubscribed");
151
+ expect(unrelated).toHaveLength(0);
143
152
 
144
- client.close();
145
- });
153
+ client.close();
154
+ });
146
155
  });
147
156
 
148
157
  // ─── 2. 다수 구독자 동시 수신 ────────────────────────────────────────────────
149
158
 
150
159
  describe("[WS Integration] 다수 구독자 동시 수신", () => {
151
- it("10명 구독자 모두 동일 query:updated를 수신한다", async () => {
152
- const N = 10;
153
- const queryKey = "ws.multi.tasks";
154
- const clients = await Promise.all(
155
- Array.from({ length: N }, () => connectClient(PORT))
156
- );
157
-
158
- // 모두 같은 query 구독
159
- for (const c of clients) {
160
- c.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
161
- }
162
- await Promise.all(clients.map(c => c.waitForMessage(m => m.type === "subscribed")));
163
-
164
- // emit
165
- const payload = [{ id: 1 }, { id: 2 }, { id: 3 }];
166
- buildRealtimeCtx().emit(queryKey, payload);
167
-
168
- // 모두 수신 대기
169
- const results = await Promise.all(
170
- clients.map(c => c.waitForMessage(m => m.type === "query:updated", 500))
171
- );
172
-
173
- expect(results).toHaveLength(N);
174
- for (const msg of results) {
175
- expect(msg.query).toBe(queryKey);
176
- expect(msg.data).toEqual(payload);
177
- }
178
-
179
- clients.forEach(c => c.close());
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
- it("50명 구독자 부하 테스트 — 모두 수신, P99 latency 측정", async () => {
183
- const N = 50;
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
- const p50 = latencies[Math.floor(N * 0.5)];
206
- const p99 = latencies[Math.floor(N * 0.99)];
210
+ const p50 = latencies[Math.floor(N * 0.5)];
211
+ const p99 = latencies[Math.floor(N * 0.99)];
207
212
 
208
- console.log(`[WS/50 clients] P50=${p50.toFixed(1)}ms P99=${p99.toFixed(1)}ms (debounce=50ms incl.)`);
213
+ console.log(`[WS/50 clients] P50=${p50.toFixed(1)}ms P99=${p99.toFixed(1)}ms (debounce=50ms incl.)`);
209
214
 
210
- expect(latencies).toHaveLength(N); // 50/50 수신
211
- expect(p99).toBeLessThan(500); // 디바운스 포함 P99 < 500ms
215
+ expect(latencies).toHaveLength(N); // 50/50 수신
216
+ expect(p99).toBeLessThan(500); // 디바운스 포함 P99 < 500ms
212
217
 
213
- clients.forEach(c => c.close());
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
- it("40ms 내 5번 rapid emit → 단 1회 query:updated 수신", async () => {
221
- const client = await connectClient(PORT);
222
- client.ws.send(JSON.stringify({ type: "subscribe", query: "ws.debounce" }));
223
- await client.waitForMessage(m => m.type === "subscribed");
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
- // debounce(50ms) + 마진
232
- await new Promise(r => setTimeout(r, 150));
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
- const updates = client.messages.filter(m => m.type === "query:updated" && m.query === "ws.debounce");
235
- console.log(`[WS/debounce] 5 emits ${updates.length} messages received`);
236
+ // debounce(50ms) + 마진
237
+ await new Promise((r) => setTimeout(r, 150));
236
238
 
237
- expect(updates).toHaveLength(1); // 1번만 전달
238
- expect(updates[0].data).toEqual([{ id: 4 }]); // 마지막 데이터
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
- client.close();
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
- it("disconnected 클라이언트는 다음 emit에서 에러 없이 제거된다", async () => {
248
- const stableClient = await connectClient(PORT);
249
- const disconnectClient = await connectClient(PORT);
250
-
251
- const queryKey = "ws.cleanup.test";
252
- stableClient.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
253
- disconnectClient.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
254
-
255
- await Promise.all([
256
- stableClient.waitForMessage(m => m.type === "subscribed"),
257
- disconnectClient.waitForMessage(m => m.type === "subscribed"),
258
- ]);
259
-
260
- // 한 클라이언트 강제 종료
261
- disconnectClient.close();
262
- await new Promise(r => setTimeout(r, 100)); // 서버가 close 처리할 시간
263
-
264
- // emit 후 stable client는 정상 수신
265
- buildRealtimeCtx().emit(queryKey, [{ id: 1 }]);
266
- const msg = await stableClient.waitForMessage(m => m.type === "query:updated", 500);
267
-
268
- expect(msg.query).toBe(queryKey);
269
- stableClient.close();
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
- it("20 mutations × 각각 query:updated → 구독자가 모두 수신한다", async () => {
277
- const N_MUTATIONS = 20;
278
- const queryKey = "ws.mutations.load";
279
- const client = await connectClient(PORT);
280
-
281
- client.ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
282
- await client.waitForMessage(m => m.type === "subscribed");
283
-
284
- // 20번 mutation 실행 (각자 별도 buildRealtimeCtx — 서로 독립적 debounce)
285
- const promises: Promise<any>[] = [];
286
- for (let i = 0; i < N_MUTATIONS; i++) {
287
- const rt = buildRealtimeCtx();
288
- rt.emit(queryKey, [{ id: i, title: `Mutation ${i}` }]);
289
- // 각 emit을 60ms 간격으로 — 각자 debounce가 완료된 후 다음 emit
290
- if (i < N_MUTATIONS - 1) await new Promise(r => setTimeout(r, 60));
291
- }
292
-
293
- // 마지막 debounce 완료 대기
294
- await new Promise(r => setTimeout(r, 200));
295
-
296
- const updates = client.messages.filter(m => m.type === "query:updated" && m.query === queryKey);
297
- console.log(`[WS/mutations] ${N_MUTATIONS} mutations → ${updates.length} updates received`);
298
-
299
- // 각 mutation이 별도 buildRealtimeCtx → 각자 debounce → N번 전달
300
- expect(updates.length).toBe(N_MUTATIONS);
301
-
302
- client.close();
303
- }, 30000); // 20 × 60ms = 1200ms + margin
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
  });