@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
@@ -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
- port: PORT,
31
- fetch(req: Request, server: any) {
32
- if (req.headers.get("upgrade") === "websocket") {
33
- server.upgrade(req);
34
- return undefined;
35
- }
36
- return new Response("OK");
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
- websocket: {
39
- open(ws: any) { registerClient(ws); },
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
- ws: WebSocket;
53
- receivedAt: number | null;
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
- return new Promise(async (resolveOuter) => {
64
- const entry: ScaleClient = { ws: null as any, receivedAt: null };
65
-
66
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
67
- try {
68
- await new Promise<void>((resolve, reject) => {
69
- const ws = new WebSocket(`ws://localhost:${PORT}`);
70
- let settled = false;
71
-
72
- const timeout = setTimeout(() => {
73
- if (!settled) { settled = true; ws.close(); reject(new Error("timeout")); }
74
- }, 5000);
75
-
76
- ws.onopen = () => {
77
- ws.send(JSON.stringify({ type: "subscribe", query: queryKey }));
78
- };
79
-
80
- ws.onmessage = (e: MessageEvent) => {
81
- const msg = JSON.parse(e.data);
82
- if (msg.type === "subscribed" && !settled) {
83
- settled = true;
84
- clearTimeout(timeout);
85
- entry.ws = ws;
86
- // 이후 query:updated 수신 기록
87
- ws.onmessage = (e2: MessageEvent) => {
88
- const m = JSON.parse(e2.data);
89
- if (m.type === "query:updated" && m.query === queryKey) {
90
- entry.receivedAt = performance.now();
91
- }
92
- };
93
- resolve();
94
- }
95
- };
96
-
97
- ws.onerror = (e) => {
98
- if (!settled) { settled = true; clearTimeout(timeout); reject(e); }
99
- };
100
- });
101
- resolveOuter(entry);
102
- return;
103
- } catch {
104
- if (attempt < maxRetries) await wait(100 * (attempt + 1));
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
- // 모든 retry 실패 시에도 반환 (receivedAt=null으로 실패 추적)
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
- const all: ScaleClient[] = [];
118
- for (let i = 0; i < n; i += batchSize) {
119
- const chunk = Math.min(batchSize, n - i);
120
- const batch = await Promise.all(
121
- Array.from({ length: chunk }, () => connectOne(queryKey))
122
- );
123
- all.push(...batch);
124
- // 배치 사이 짧은 대기 — 서버가 accept queue를 비울 시간
125
- if (i + batchSize < n) await wait(50);
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
- for (const c of clients) {
132
- try { c.ws?.close(); } catch { }
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
- if (sorted.length === 0) return 0;
138
- const idx = Math.ceil((p / 100) * sorted.length) - 1;
139
- return sorted[Math.max(0, idx)];
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
- const key = `scale.${n}.${Date.now()}`;
152
+ const key = `scale.${n}.${Date.now()}`;
146
153
 
147
- const t0 = performance.now();
148
- const clients = await connectBatch(n, key);
149
- const connectTime = performance.now() - t0;
150
- const connected = clients.filter(c => c.ws !== null).length;
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
- const emitTime = performance.now();
153
- buildRealtimeCtx().emit(key, [{ id: 1, title: "Scale Test" }]);
154
- await wait(waitMs);
159
+ const emitTime = performance.now();
160
+ buildRealtimeCtx().emit(key, [{ id: 1, title: "Scale Test" }]);
161
+ await wait(waitMs);
155
162
 
156
- const latencies = clients
157
- .filter(c => c.receivedAt !== null)
158
- .map(c => c.receivedAt! - emitTime)
159
- .sort((a, b) => a - b);
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
- const rate = (latencies.length / n) * 100;
162
- const p50 = percentile(latencies, 50);
163
- const p99 = percentile(latencies, 99);
164
- const max = latencies[latencies.length - 1] ?? 0;
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
- console.log(`[${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`);
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
- disconnectAll(clients);
169
- await wait(200);
177
+ disconnectAll(clients);
178
+ await wait(200);
170
179
 
171
- return { n, connectMs: connectTime, connected, delivered: latencies.length, rate, p50, p99, max };
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
- const results: Awaited<ReturnType<typeof runScaleTest>>[] = [];
178
-
179
- it("100 동시 연결 — baseline", async () => {
180
- const r = await runScaleTest(100, "WS/100 ", 200);
181
- results.push(r);
182
- expect(r.rate).toBe(100);
183
- });
184
-
185
- it("500 동시 연결", async () => {
186
- const r = await runScaleTest(500, "WS/500 ", 300);
187
- results.push(r);
188
- expect(r.rate).toBe(100);
189
- });
190
-
191
- it("1,000 동시 연결", async () => {
192
- const r = await runScaleTest(1_000, "WS/1k ", 500);
193
- results.push(r);
194
- expect(r.rate).toBe(100);
195
- });
196
-
197
- it("2,000 동시 연결", async () => {
198
- const r = await runScaleTest(2_000, "WS/2k ", 1000);
199
- results.push(r);
200
- expect(r.rate).toBe(100);
201
- });
202
-
203
- it("5,000 동시 연결", async () => {
204
- const r = await runScaleTest(5_000, "WS/5k ", 2000);
205
- results.push(r);
206
- expect(r.rate).toBe(100);
207
- });
208
-
209
- it("10,000 동시 연결", async () => {
210
- const r = await runScaleTest(10_000, "WS/10k ", 3000);
211
- results.push(r);
212
- expect(r.rate).toBe(100);
213
- });
214
-
215
- it("결과 요약 테이블 출력", () => {
216
- console.log("\n┌──────────┬────────────┬──────────┬──────────┬──────────┬──────────┐");
217
- console.log("│ 구독자 │ 연결 시간 │ 수신률 │ P50 │ P99 │ Max │");
218
- console.log("├──────────┼────────────┼──────────┼──────────┼──────────┼──────────┤");
219
- for (const r of results) {
220
- const n = String(r.n).padStart(7);
221
- const conn = `${r.connectMs.toFixed(0)}ms`.padStart(8);
222
- const rate = `${r.rate.toFixed(1)}%`.padStart(6);
223
- const p50 = `${r.p50.toFixed(1)}ms`.padStart(7);
224
- const p99 = `${r.p99.toFixed(1)}ms`.padStart(7);
225
- const max = `${r.max.toFixed(1)}ms`.padStart(7);
226
- console.log(`│ ${n} │ ${conn} │ ${rate} │ ${p50} │ ${p99} │ ${max} │`);
227
- }
228
- console.log("└──────────┴────────────┴──────────┴──────────┴──────────┴──────────┘\n");
229
-
230
- expect(results.length).toBeGreaterThanOrEqual(6);
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
  });
@@ -11,28 +11,28 @@
11
11
  // ─── Email Verification ──────────────────────────────────
12
12
 
13
13
  export interface AuthEmailVerification {
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>;
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
- emailVerification?: AuthEmailVerification;
32
- // 확장 예정:
33
- // socialProviders?: { ... }
34
- // passwordPolicy?: { ... }
35
- // sessionExpiry?: number
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
- return config;
58
+ return config;
59
59
  }