@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.
Files changed (75) hide show
  1. package/dist/crud.d.ts +2 -2
  2. package/dist/crud.js +225 -208
  3. package/dist/index.d.ts +5 -5
  4. package/dist/index.js +2 -2
  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.js +4 -11
  16. package/dist/workflows-api.js +5 -12
  17. package/package.json +45 -42
  18. package/src/__tests__/auth.test.ts +90 -86
  19. package/src/__tests__/crons.test.ts +69 -67
  20. package/src/__tests__/crud-codegen-integration.test.ts +164 -170
  21. package/src/__tests__/crud-owner-rls.test.ts +308 -301
  22. package/src/__tests__/crud.test.ts +694 -711
  23. package/src/__tests__/dist-exports.test.ts +120 -120
  24. package/src/__tests__/fixtures/basic/auth.ts +16 -16
  25. package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
  26. package/src/__tests__/fixtures/basic/index.ts +1 -1
  27. package/src/__tests__/fixtures/basic/schema.ts +1 -1
  28. package/src/__tests__/fixtures/basic/tasks.ts +4 -4
  29. package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
  30. package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
  31. package/src/__tests__/helpers/pglite-migrations.ts +2 -5
  32. package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
  33. package/src/__tests__/helpers/seed-like-fill.ts +47 -41
  34. package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
  35. package/src/__tests__/httpaction.test.ts +91 -91
  36. package/src/__tests__/image-optimization.test.ts +570 -574
  37. package/src/__tests__/load.test.ts +321 -308
  38. package/src/__tests__/network-sim.test.ts +238 -215
  39. package/src/__tests__/reactive.test.ts +380 -358
  40. package/src/__tests__/retry.test.ts +99 -84
  41. package/src/__tests__/rls-crud-basic.test.ts +172 -245
  42. package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
  43. package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
  44. package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
  45. package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
  46. package/src/__tests__/rls-session-and-policies.test.ts +181 -199
  47. package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
  48. package/src/__tests__/scheduler-durable.test.ts +117 -117
  49. package/src/__tests__/scheduler-exec.test.ts +258 -246
  50. package/src/__tests__/scheduler.test.ts +129 -111
  51. package/src/__tests__/storage.test.ts +282 -269
  52. package/src/__tests__/tsconfig.json +6 -6
  53. package/src/__tests__/validator.test.ts +236 -232
  54. package/src/__tests__/workflow.test.ts +309 -286
  55. package/src/__tests__/ws-integration.test.ts +223 -218
  56. package/src/__tests__/ws-scale.test.ts +168 -159
  57. package/src/auth-config.ts +18 -18
  58. package/src/auth.ts +106 -106
  59. package/src/crons.ts +77 -77
  60. package/src/crud.ts +523 -479
  61. package/src/index.ts +69 -5
  62. package/src/reactive.ts +357 -331
  63. package/src/retry.ts +51 -54
  64. package/src/rls-db.ts +195 -205
  65. package/src/rls.ts +33 -36
  66. package/src/scheduler.ts +237 -211
  67. package/src/server.ts +0 -1
  68. package/src/storage.ts +632 -593
  69. package/src/v.ts +119 -114
  70. package/src/workflow-types.ts +67 -70
  71. package/src/workflow.ts +99 -116
  72. package/src/workflows-api.ts +231 -241
  73. package/dist/db.d.ts +0 -13
  74. package/dist/db.js +0 -16
  75. 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
  });