@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
|
@@ -15,33 +15,32 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describe, it, expect, afterAll } from "bun:test";
|
|
18
|
-
import {
|
|
19
|
-
buildRealtimeCtx,
|
|
20
|
-
handleWsMessage,
|
|
21
|
-
registerClient,
|
|
22
|
-
deregisterClient,
|
|
23
|
-
} from "../reactive";
|
|
18
|
+
import { buildRealtimeCtx, handleWsMessage, registerClient, deregisterClient } from "../reactive.js";
|
|
24
19
|
|
|
25
20
|
// ─── WebSocket 서버 ──────────────────────────────────────────────────────────
|
|
26
21
|
|
|
27
22
|
const PORT = 57891;
|
|
28
23
|
|
|
29
24
|
const server = Bun.serve({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
port: PORT,
|
|
26
|
+
fetch(req: Request, server: any) {
|
|
27
|
+
if (req.headers.get("upgrade") === "websocket") {
|
|
28
|
+
server.upgrade(req);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return new Response("OK");
|
|
32
|
+
},
|
|
33
|
+
websocket: {
|
|
34
|
+
open(ws: any) {
|
|
35
|
+
registerClient(ws);
|
|
36
|
+
},
|
|
37
|
+
message(ws: any, data: string | Buffer) {
|
|
38
|
+
handleWsMessage(ws, typeof data === "string" ? data : data.toString());
|
|
37
39
|
},
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
message(ws: any, data: string | Buffer) {
|
|
41
|
-
handleWsMessage(ws, typeof data === "string" ? data : data.toString());
|
|
42
|
-
},
|
|
43
|
-
close(ws: any) { deregisterClient(ws); },
|
|
40
|
+
close(ws: any) {
|
|
41
|
+
deregisterClient(ws);
|
|
44
42
|
},
|
|
43
|
+
},
|
|
45
44
|
});
|
|
46
45
|
|
|
47
46
|
afterAll(() => server.stop(true));
|
|
@@ -49,64 +48,72 @@ afterAll(() => server.stop(true));
|
|
|
49
48
|
// ─── 헬퍼 ────────────────────────────────────────────────────────────────────
|
|
50
49
|
|
|
51
50
|
interface ScaleClient {
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
ws: WebSocket;
|
|
52
|
+
receivedAt: number | null;
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const wait = (ms: number) => new Promise(r => setTimeout(r, ms));
|
|
55
|
+
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
57
56
|
|
|
58
57
|
/**
|
|
59
58
|
* 단일 WebSocket 연결 + subscribe + ready 대기.
|
|
60
59
|
* 연결 실패 시 retry (최대 3회, 100ms 간격).
|
|
61
60
|
*/
|
|
62
61
|
function connectOne(queryKey: string, maxRetries = 3): Promise<ScaleClient> {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
62
|
+
return new Promise(async (resolveOuter) => {
|
|
63
|
+
const entry: ScaleClient = { ws: null as any, receivedAt: null };
|
|
64
|
+
|
|
65
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
66
|
+
try {
|
|
67
|
+
await new Promise<void>((resolve, reject) => {
|
|
68
|
+
const ws = new WebSocket(`ws://localhost:${PORT}`);
|
|
69
|
+
let settled = false;
|
|
70
|
+
|
|
71
|
+
const timeout = setTimeout(() => {
|
|
72
|
+
if (!settled) {
|
|
73
|
+
settled = true;
|
|
74
|
+
ws.close();
|
|
75
|
+
reject(new Error("timeout"));
|
|
76
|
+
}
|
|
77
|
+
}, 5000);
|
|
78
|
+
|
|
79
|
+
ws.onopen = () => {
|
|
80
|
+
ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
ws.onmessage = (e: MessageEvent) => {
|
|
84
|
+
const msg = JSON.parse(e.data);
|
|
85
|
+
if (msg.type === "subscribed" && !settled) {
|
|
86
|
+
settled = true;
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
entry.ws = ws;
|
|
89
|
+
// 이후 query:updated 수신 시 기록
|
|
90
|
+
ws.onmessage = (e2: MessageEvent) => {
|
|
91
|
+
const m = JSON.parse(e2.data);
|
|
92
|
+
if (m.type === "query:updated" && m.query === queryKey) {
|
|
93
|
+
entry.receivedAt = performance.now();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
resolve();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
ws.onerror = (e) => {
|
|
101
|
+
if (!settled) {
|
|
102
|
+
settled = true;
|
|
103
|
+
clearTimeout(timeout);
|
|
104
|
+
reject(e);
|
|
105
105
|
}
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
108
|
resolveOuter(entry);
|
|
109
|
-
|
|
109
|
+
return;
|
|
110
|
+
} catch {
|
|
111
|
+
if (attempt < maxRetries) await wait(100 * (attempt + 1));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// 모든 retry 실패 시에도 반환 (receivedAt=null으로 실패 추적)
|
|
115
|
+
resolveOuter(entry);
|
|
116
|
+
});
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
/**
|
|
@@ -114,119 +121,121 @@ function connectOne(queryKey: string, maxRetries = 3): Promise<ScaleClient> {
|
|
|
114
121
|
* OS listen backlog 포화를 방지.
|
|
115
122
|
*/
|
|
116
123
|
async function connectBatch(n: number, queryKey: string, batchSize = 500): Promise<ScaleClient[]> {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
return all;
|
|
124
|
+
const all: ScaleClient[] = [];
|
|
125
|
+
for (let i = 0; i < n; i += batchSize) {
|
|
126
|
+
const chunk = Math.min(batchSize, n - i);
|
|
127
|
+
const batch = await Promise.all(Array.from({ length: chunk }, () => connectOne(queryKey)));
|
|
128
|
+
all.push(...batch);
|
|
129
|
+
// 배치 사이 짧은 대기 — 서버가 accept queue를 비울 시간
|
|
130
|
+
if (i + batchSize < n) await wait(50);
|
|
131
|
+
}
|
|
132
|
+
return all;
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
function disconnectAll(clients: ScaleClient[]) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
136
|
+
for (const c of clients) {
|
|
137
|
+
try {
|
|
138
|
+
c.ws?.close();
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
function percentile(sorted: number[], p: number): number {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
if (sorted.length === 0) return 0;
|
|
145
|
+
const idx = Math.ceil((p / 100) * sorted.length) - 1;
|
|
146
|
+
return sorted[Math.max(0, idx)];
|
|
140
147
|
}
|
|
141
148
|
|
|
142
149
|
// ─── 공통 테스트 로직 ────────────────────────────────────────────────────────
|
|
143
150
|
|
|
144
151
|
async function runScaleTest(n: number, label: string, waitMs: number) {
|
|
145
|
-
|
|
152
|
+
const key = `scale.${n}.${Date.now()}`;
|
|
146
153
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
const t0 = performance.now();
|
|
155
|
+
const clients = await connectBatch(n, key);
|
|
156
|
+
const connectTime = performance.now() - t0;
|
|
157
|
+
const connected = clients.filter((c) => c.ws !== null).length;
|
|
151
158
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
159
|
+
const emitTime = performance.now();
|
|
160
|
+
buildRealtimeCtx().emit(key, [{ id: 1, title: "Scale Test" }]);
|
|
161
|
+
await wait(waitMs);
|
|
155
162
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
163
|
+
const latencies = clients
|
|
164
|
+
.filter((c) => c.receivedAt !== null)
|
|
165
|
+
.map((c) => c.receivedAt! - emitTime)
|
|
166
|
+
.sort((a, b) => a - b);
|
|
160
167
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
const rate = (latencies.length / n) * 100;
|
|
169
|
+
const p50 = percentile(latencies, 50);
|
|
170
|
+
const p99 = percentile(latencies, 99);
|
|
171
|
+
const max = latencies[latencies.length - 1] ?? 0;
|
|
165
172
|
|
|
166
|
-
|
|
173
|
+
console.log(
|
|
174
|
+
`[${label}] connect=${connectTime.toFixed(0)}ms (${connected}/${n}) deliver: ${latencies.length}/${n} (${rate.toFixed(1)}%) P50=${p50.toFixed(1)}ms P99=${p99.toFixed(1)}ms Max=${max.toFixed(1)}ms`,
|
|
175
|
+
);
|
|
167
176
|
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
disconnectAll(clients);
|
|
178
|
+
await wait(200);
|
|
170
179
|
|
|
171
|
-
|
|
180
|
+
return { n, connectMs: connectTime, connected, delivered: latencies.length, rate, p50, p99, max };
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
// ─── 테스트 ──────────────────────────────────────────────────────────────────
|
|
175
184
|
|
|
176
185
|
describe("[WS Scale] 실제 TCP WebSocket 대규모 동시 연결", () => {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
186
|
+
const results: Awaited<ReturnType<typeof runScaleTest>>[] = [];
|
|
187
|
+
|
|
188
|
+
it("100 동시 연결 — baseline", async () => {
|
|
189
|
+
const r = await runScaleTest(100, "WS/100 ", 200);
|
|
190
|
+
results.push(r);
|
|
191
|
+
expect(r.rate).toBe(100);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("500 동시 연결", async () => {
|
|
195
|
+
const r = await runScaleTest(500, "WS/500 ", 300);
|
|
196
|
+
results.push(r);
|
|
197
|
+
expect(r.rate).toBe(100);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("1,000 동시 연결", async () => {
|
|
201
|
+
const r = await runScaleTest(1_000, "WS/1k ", 500);
|
|
202
|
+
results.push(r);
|
|
203
|
+
expect(r.rate).toBe(100);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("2,000 동시 연결", async () => {
|
|
207
|
+
const r = await runScaleTest(2_000, "WS/2k ", 1000);
|
|
208
|
+
results.push(r);
|
|
209
|
+
expect(r.rate).toBe(100);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("5,000 동시 연결", async () => {
|
|
213
|
+
const r = await runScaleTest(5_000, "WS/5k ", 2000);
|
|
214
|
+
results.push(r);
|
|
215
|
+
expect(r.rate).toBe(100);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("10,000 동시 연결", async () => {
|
|
219
|
+
const r = await runScaleTest(10_000, "WS/10k ", 3000);
|
|
220
|
+
results.push(r);
|
|
221
|
+
expect(r.rate).toBe(100);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("결과 요약 테이블 출력", () => {
|
|
225
|
+
console.log("\n┌──────────┬────────────┬──────────┬──────────┬──────────┬──────────┐");
|
|
226
|
+
console.log("│ 구독자 │ 연결 시간 │ 수신률 │ P50 │ P99 │ Max │");
|
|
227
|
+
console.log("├──────────┼────────────┼──────────┼──────────┼──────────┼──────────┤");
|
|
228
|
+
for (const r of results) {
|
|
229
|
+
const n = String(r.n).padStart(7);
|
|
230
|
+
const conn = `${r.connectMs.toFixed(0)}ms`.padStart(8);
|
|
231
|
+
const rate = `${r.rate.toFixed(1)}%`.padStart(6);
|
|
232
|
+
const p50 = `${r.p50.toFixed(1)}ms`.padStart(7);
|
|
233
|
+
const p99 = `${r.p99.toFixed(1)}ms`.padStart(7);
|
|
234
|
+
const max = `${r.max.toFixed(1)}ms`.padStart(7);
|
|
235
|
+
console.log(`│ ${n} │ ${conn} │ ${rate} │ ${p50} │ ${p99} │ ${max} │`);
|
|
236
|
+
}
|
|
237
|
+
console.log("└──────────┴────────────┴──────────┴──────────┴──────────┴──────────┘\n");
|
|
238
|
+
|
|
239
|
+
expect(results.length).toBeGreaterThanOrEqual(6);
|
|
240
|
+
});
|
|
232
241
|
});
|
package/src/auth-config.ts
CHANGED
|
@@ -11,28 +11,28 @@
|
|
|
11
11
|
// ─── Email Verification ──────────────────────────────────
|
|
12
12
|
|
|
13
13
|
export interface AuthEmailVerification {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
/** 가입 시 인증 메일 자동 발송 (default: true) */
|
|
15
|
+
sendOnSignUp?: boolean;
|
|
16
|
+
/** 이메일 미인증 시 로그인 차단 (default: true) */
|
|
17
|
+
requireEmailVerification?: boolean;
|
|
18
|
+
/** 인증 완료 후 자동 로그인 (default: true) */
|
|
19
|
+
autoSignInAfterVerification?: boolean;
|
|
20
|
+
/** 인증 메일 발송 함수 — 사용자가 직접 구현 */
|
|
21
|
+
sendVerificationEmail: (data: {
|
|
22
|
+
user: { email: string; name: string };
|
|
23
|
+
url: string;
|
|
24
|
+
token: string;
|
|
25
|
+
}) => Promise<void>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// ─── Auth Config ─────────────────────────────────────────
|
|
29
29
|
|
|
30
30
|
export interface GencowAuthConfig {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
emailVerification?: AuthEmailVerification;
|
|
32
|
+
// 확장 예정:
|
|
33
|
+
// socialProviders?: { ... }
|
|
34
|
+
// passwordPolicy?: { ... }
|
|
35
|
+
// sessionExpiry?: number
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
// ─── defineAuth() ────────────────────────────────────────
|
|
@@ -55,5 +55,5 @@ export interface GencowAuthConfig {
|
|
|
55
55
|
* ```
|
|
56
56
|
*/
|
|
57
57
|
export function defineAuth(config: GencowAuthConfig): GencowAuthConfig {
|
|
58
|
-
|
|
58
|
+
return config;
|
|
59
59
|
}
|