@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
@@ -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
  }