@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,256 +15,274 @@
15
15
  */
16
16
 
17
17
  import { describe, it, expect } from "bun:test";
18
- import { createScheduler } from "../scheduler";
19
- import type { ScheduledJobRecord } from "../scheduler";
18
+ import { createScheduler } from "../scheduler.js";
19
+ import type { ScheduledJobRecord } from "../scheduler.js";
20
20
 
21
21
  describe("Scheduler Durable v2 — Platform DB 중앙화", () => {
22
- // ── args 전달 정확성 ──
22
+ // ── args 전달 정확성 ──
23
23
 
24
- it("persistJob에 args가 정확히 전달된다 (중첩 객체)", async () => {
25
- const persisted: ScheduledJobRecord[] = [];
24
+ it("persistJob에 args가 정확히 전달된다 (중첩 객체)", async () => {
25
+ const persisted: ScheduledJobRecord[] = [];
26
26
 
27
- const scheduler = createScheduler({
28
- persistJob: async (job) => { persisted.push(job); },
29
- removeJob: async () => true,
30
- });
31
-
32
- scheduler.registerAction("deep.args", async () => {});
33
-
34
- const complexArgs = {
35
- nested: { deeply: { value: 42 } },
36
- array: [1, 2, { x: "y" }],
37
- unicode: "한글테스트 🎯",
38
- nullValue: null,
39
- boolValue: false,
40
- zero: 0,
41
- emptyString: "",
42
- };
43
-
44
- scheduler.runAfter(1000, "deep.args", complexArgs);
45
- await new Promise((r) => setTimeout(r, 50));
27
+ const scheduler = createScheduler({
28
+ persistJob: async (job) => {
29
+ persisted.push(job);
30
+ },
31
+ removeJob: async () => true,
32
+ });
46
33
 
47
- expect(persisted.length).toBe(1);
48
- expect(persisted[0].args).toEqual(complexArgs);
34
+ scheduler.registerAction("deep.args", async () => {});
35
+
36
+ const complexArgs = {
37
+ nested: { deeply: { value: 42 } },
38
+ array: [1, 2, { x: "y" }],
39
+ unicode: "한글테스트 🎯",
40
+ nullValue: null,
41
+ boolValue: false,
42
+ zero: 0,
43
+ emptyString: "",
44
+ };
45
+
46
+ scheduler.runAfter(1000, "deep.args", complexArgs);
47
+ await new Promise((r) => setTimeout(r, 50));
48
+
49
+ expect(persisted.length).toBe(1);
50
+ expect(persisted[0].args).toEqual(complexArgs);
51
+ });
52
+
53
+ it("persistJob에 args 미전달 시 undefined로 전달", async () => {
54
+ const persisted: ScheduledJobRecord[] = [];
55
+
56
+ const scheduler = createScheduler({
57
+ persistJob: async (job) => {
58
+ persisted.push(job);
59
+ },
60
+ removeJob: async () => true,
49
61
  });
50
62
 
51
- it("persistJob에 args 미전달 시 undefined로 전달", async () => {
52
- const persisted: ScheduledJobRecord[] = [];
63
+ scheduler.registerAction("no.args", async () => {});
64
+ scheduler.runAfter(1000, "no.args"); // args 생략
65
+
66
+ await new Promise((r) => setTimeout(r, 50));
53
67
 
54
- const scheduler = createScheduler({
55
- persistJob: async (job) => { persisted.push(job); },
56
- removeJob: async () => true,
57
- });
68
+ expect(persisted.length).toBe(1);
69
+ // args는 undefined 또는 값이어야 함 (Platform에서 {} default 처리)
70
+ expect(
71
+ persisted[0].args === undefined ||
72
+ persisted[0].args === null ||
73
+ JSON.stringify(persisted[0].args) === "{}",
74
+ ).toBe(true);
75
+ });
58
76
 
59
- scheduler.registerAction("no.args", async () => {});
60
- scheduler.runAfter(1000, "no.args"); // args 생략
77
+ // ── 동시 등록 id 고유성 ──
61
78
 
62
- await new Promise((r) => setTimeout(r, 50));
79
+ it("10개 job 동시 등록 시 모든 id가 고유하다", async () => {
80
+ const persisted: ScheduledJobRecord[] = [];
63
81
 
64
- expect(persisted.length).toBe(1);
65
- // args는 undefined 또는 빈 값이어야 함 (Platform에서 {} default 처리)
66
- expect(persisted[0].args === undefined || persisted[0].args === null || JSON.stringify(persisted[0].args) === "{}").toBe(true);
82
+ const scheduler = createScheduler({
83
+ persistJob: async (job) => {
84
+ persisted.push(job);
85
+ },
86
+ removeJob: async () => true,
67
87
  });
68
88
 
69
- // ── 동시 등록 id 고유성 ──
89
+ for (let i = 0; i < 10; i++) {
90
+ scheduler.registerAction(`batch.${i}`, async () => {});
91
+ }
70
92
 
71
- it("10개 job 동시 등록 시 모든 id가 고유하다", async () => {
72
- const persisted: ScheduledJobRecord[] = [];
93
+ const ids: string[] = [];
94
+ for (let i = 0; i < 10; i++) {
95
+ ids.push(scheduler.runAfter(1000, `batch.${i}`, { idx: i }));
96
+ }
73
97
 
74
- const scheduler = createScheduler({
75
- persistJob: async (job) => { persisted.push(job); },
76
- removeJob: async () => true,
77
- });
98
+ await new Promise((r) => setTimeout(r, 100));
78
99
 
79
- for (let i = 0; i < 10; i++) {
80
- scheduler.registerAction(`batch.${i}`, async () => {});
81
- }
100
+ // 모든 id 고유
101
+ const uniqueIds = new Set(ids);
102
+ expect(uniqueIds.size).toBe(10);
82
103
 
83
- const ids: string[] = [];
84
- for (let i = 0; i < 10; i++) {
85
- ids.push(scheduler.runAfter(1000, `batch.${i}`, { idx: i }));
86
- }
104
+ // 모든 persistJob 호출됨
105
+ expect(persisted.length).toBe(10);
106
+ });
87
107
 
88
- await new Promise((r) => setTimeout(r, 100));
108
+ // ── runAfter(0) 즉시 실행 ──
89
109
 
90
- // 모든 id 고유
91
- const uniqueIds = new Set(ids);
92
- expect(uniqueIds.size).toBe(10);
110
+ it("runAfter(0) durable mode에서도 즉시 persistJob 호출", async () => {
111
+ const persisted: ScheduledJobRecord[] = [];
93
112
 
94
- // 모든 persistJob 호출됨
95
- expect(persisted.length).toBe(10);
113
+ const scheduler = createScheduler({
114
+ persistJob: async (job) => {
115
+ persisted.push(job);
116
+ },
117
+ removeJob: async () => true,
96
118
  });
97
119
 
98
- // ── runAfter(0) 즉시 실행 ──
99
-
100
- it("runAfter(0) — durable mode에서도 즉시 persistJob 호출", async () => {
101
- const persisted: ScheduledJobRecord[] = [];
120
+ scheduler.registerAction("instant", async () => {});
102
121
 
103
- const scheduler = createScheduler({
104
- persistJob: async (job) => { persisted.push(job); },
105
- removeJob: async () => true,
106
- });
122
+ const beforeTime = Date.now();
123
+ scheduler.runAfter(0, "instant", { immediate: true });
107
124
 
108
- scheduler.registerAction("instant", async () => {});
125
+ await new Promise((r) => setTimeout(r, 50));
109
126
 
110
- const beforeTime = Date.now();
111
- scheduler.runAfter(0, "instant", { immediate: true });
127
+ expect(persisted.length).toBe(1);
128
+ expect(persisted[0].args).toEqual({ immediate: true });
129
+ // runAt은 현재 시간과 거의 동일해야 함 (±2초 오차 허용)
130
+ expect(Math.abs(persisted[0].runAt.getTime() - beforeTime)).toBeLessThan(2000);
131
+ });
112
132
 
113
- await new Promise((r) => setTimeout(r, 50));
133
+ // ── removeJob 실패 graceful 처리 ──
114
134
 
115
- expect(persisted.length).toBe(1);
116
- expect(persisted[0].args).toEqual({ immediate: true });
117
- // runAt은 현재 시간과 거의 동일해야 함 (±2초 오차 허용)
118
- expect(Math.abs(persisted[0].runAt.getTime() - beforeTime)).toBeLessThan(2000);
135
+ it("removeJob 실패 시 cancel()이 false 반환 (throw 안 함)", async () => {
136
+ const scheduler = createScheduler({
137
+ persistJob: async () => {},
138
+ removeJob: async () => {
139
+ throw new Error("DB connection lost");
140
+ },
119
141
  });
120
142
 
121
- // ── removeJob 실패 시 graceful 처리 ──
143
+ scheduler.registerAction("fail.cancel", async () => {});
144
+ const id = scheduler.runAfter(10000, "fail.cancel");
122
145
 
123
- it("removeJob 실패 시 cancel() false 반환 (throw 안 함)", async () => {
124
- const scheduler = createScheduler({
125
- persistJob: async () => {},
126
- removeJob: async () => {
127
- throw new Error("DB connection lost");
128
- },
129
- });
146
+ await new Promise((r) => setTimeout(r, 50));
130
147
 
131
- scheduler.registerAction("fail.cancel", async () => {});
132
- const id = scheduler.runAfter(10000, "fail.cancel");
148
+ // suppress expected error
149
+ const orig = console.error;
150
+ console.error = () => {};
151
+ const result = scheduler.cancel(id);
152
+ console.error = orig;
133
153
 
134
- await new Promise((r) => setTimeout(r, 50));
154
+ // removeJob 실패 return false, throw 안 함
155
+ expect(typeof result).toBe("boolean");
156
+ });
135
157
 
136
- // suppress expected error
137
- const orig = console.error;
138
- console.error = () => {};
139
- const result = scheduler.cancel(id);
140
- console.error = orig;
158
+ // ── 대형 args 안정성 ──
141
159
 
142
- // removeJob 실패 return false, throw
143
- expect(typeof result).toBe("boolean");
160
+ it("대형 args (10KB) 직렬화 안정성", async () => {
161
+ const persisted: ScheduledJobRecord[] = [];
162
+
163
+ const scheduler = createScheduler({
164
+ persistJob: async (job) => {
165
+ persisted.push(job);
166
+ },
167
+ removeJob: async () => true,
144
168
  });
145
169
 
146
- // ── 대형 args 안정성 ──
170
+ scheduler.registerAction("large.payload", async () => {});
147
171
 
148
- it("대형 args (10KB) 직렬화 안정성", async () => {
149
- const persisted: ScheduledJobRecord[] = [];
172
+ // ~10KB args
173
+ const largeArgs = {
174
+ data: "x".repeat(10_000),
175
+ items: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}` })),
176
+ };
150
177
 
151
- const scheduler = createScheduler({
152
- persistJob: async (job) => { persisted.push(job); },
153
- removeJob: async () => true,
154
- });
178
+ scheduler.runAfter(1000, "large.payload", largeArgs);
179
+ await new Promise((r) => setTimeout(r, 50));
155
180
 
156
- scheduler.registerAction("large.payload", async () => {});
181
+ expect(persisted.length).toBe(1);
182
+ expect((persisted[0].args as any).data.length).toBe(10_000);
183
+ expect((persisted[0].args as any).items.length).toBe(100);
184
+ });
157
185
 
158
- // ~10KB args
159
- const largeArgs = {
160
- data: "x".repeat(10_000),
161
- items: Array.from({ length: 100 }, (_, i) => ({ id: i, name: `item-${i}` })),
162
- };
186
+ // ── onError + args 조합 ──
163
187
 
164
- scheduler.runAfter(1000, "large.payload", largeArgs);
165
- await new Promise((r) => setTimeout(r, 50));
188
+ it("onError와 args가 함께 persistJob에 전달된다", async () => {
189
+ const persisted: ScheduledJobRecord[] = [];
166
190
 
167
- expect(persisted.length).toBe(1);
168
- expect((persisted[0].args as any).data.length).toBe(10_000);
169
- expect((persisted[0].args as any).items.length).toBe(100);
191
+ const scheduler = createScheduler({
192
+ persistJob: async (job) => {
193
+ persisted.push(job);
194
+ },
195
+ removeJob: async () => true,
170
196
  });
171
197
 
172
- // ── onError + args 조합 ──
198
+ scheduler.registerAction("pipeline.step1", async () => {});
199
+ scheduler.registerAction("pipeline.onError", async () => {});
173
200
 
174
- it("onError와 args가 함께 persistJob에 전달된다", async () => {
175
- const persisted: ScheduledJobRecord[] = [];
201
+ scheduler.runAfter(5000, "pipeline.step1", { step: 1, data: "test" }, { onError: "pipeline.onError" });
176
202
 
177
- const scheduler = createScheduler({
178
- persistJob: async (job) => { persisted.push(job); },
179
- removeJob: async () => true,
180
- });
203
+ await new Promise((r) => setTimeout(r, 50));
181
204
 
182
- scheduler.registerAction("pipeline.step1", async () => {});
183
- scheduler.registerAction("pipeline.onError", async () => {});
205
+ expect(persisted.length).toBe(1);
206
+ expect(persisted[0].action).toBe("pipeline.step1");
207
+ expect(persisted[0].args).toEqual({ step: 1, data: "test" });
208
+ expect(persisted[0].onErrorAction).toBe("pipeline.onError");
209
+ });
184
210
 
185
- scheduler.runAfter(5000, "pipeline.step1", { step: 1, data: "test" }, { onError: "pipeline.onError" });
211
+ // ── runAt + durable 조합 ──
186
212
 
187
- await new Promise((r) => setTimeout(r, 50));
213
+ it("runAt(과거 시점) durable mode에서도 persistJob 호출 (즉시 실행 대상)", async () => {
214
+ const persisted: ScheduledJobRecord[] = [];
188
215
 
189
- expect(persisted.length).toBe(1);
190
- expect(persisted[0].action).toBe("pipeline.step1");
191
- expect(persisted[0].args).toEqual({ step: 1, data: "test" });
192
- expect(persisted[0].onErrorAction).toBe("pipeline.onError");
216
+ const scheduler = createScheduler({
217
+ persistJob: async (job) => {
218
+ persisted.push(job);
219
+ },
220
+ removeJob: async () => true,
193
221
  });
194
222
 
195
- // ── runAt + durable 조합 ──
223
+ scheduler.registerAction("past.task", async () => {});
196
224
 
197
- it("runAt(과거 시점) durable mode에서도 persistJob 호출 (즉시 실행 대상)", async () => {
198
- const persisted: ScheduledJobRecord[] = [];
225
+ const pastTime = new Date(Date.now() - 60_000); // 1분
226
+ scheduler.runAt(pastTime, "past.task", { expired: true });
199
227
 
200
- const scheduler = createScheduler({
201
- persistJob: async (job) => { persisted.push(job); },
202
- removeJob: async () => true,
203
- });
228
+ await new Promise((r) => setTimeout(r, 50));
204
229
 
205
- scheduler.registerAction("past.task", async () => {});
230
+ expect(persisted.length).toBe(1);
231
+ expect(persisted[0].args).toEqual({ expired: true });
232
+ // runAt이 과거여도 persistJob은 호출되어야 함 (폴러가 즉시 픽업)
233
+ });
206
234
 
207
- const pastTime = new Date(Date.now() - 60_000); // 1분 전
208
- scheduler.runAt(pastTime, "past.task", { expired: true });
235
+ // ── 다중 cancel ──
209
236
 
210
- await new Promise((r) => setTimeout(r, 50));
237
+ it("같은 job을 2번 cancel — 첫 번째만 removeJob 성공", async () => {
238
+ let removeCount = 0;
211
239
 
212
- expect(persisted.length).toBe(1);
213
- expect(persisted[0].args).toEqual({ expired: true });
214
- // runAt이 과거여도 persistJob은 호출되어야 함 (폴러가 즉시 픽업)
240
+ const scheduler = createScheduler({
241
+ persistJob: async () => {},
242
+ removeJob: async () => {
243
+ removeCount++;
244
+ return removeCount === 1; // 첫 번째만 true
245
+ },
215
246
  });
216
247
 
217
- // ── 다중 cancel ──
248
+ scheduler.registerAction("double.cancel", async () => {});
249
+ const id = scheduler.runAfter(10000, "double.cancel");
218
250
 
219
- it("같은 job을 2번 cancel — 첫 번째만 removeJob 성공", async () => {
220
- let removeCount = 0;
251
+ await new Promise((r) => setTimeout(r, 50));
221
252
 
222
- const scheduler = createScheduler({
223
- persistJob: async () => {},
224
- removeJob: async () => {
225
- removeCount++;
226
- return removeCount === 1; // 첫 번째만 true
227
- },
228
- });
253
+ const r1 = scheduler.cancel(id);
254
+ await new Promise((r) => setTimeout(r, 20));
255
+ const r2 = scheduler.cancel(id);
229
256
 
230
- scheduler.registerAction("double.cancel", async () => {});
231
- const id = scheduler.runAfter(10000, "double.cancel");
257
+ // 첫 번째 cancel 성공 (pendingJobs에서 제거)
258
+ expect(r1).toBe(true);
259
+ // 두 번째 cancel은 이미 제거되었으므로 false
260
+ expect(r2).toBe(false);
261
+ });
232
262
 
233
- await new Promise((r) => setTimeout(r, 50));
263
+ // ── persistJob 순서 보장 ──
234
264
 
235
- const r1 = scheduler.cancel(id);
236
- await new Promise((r) => setTimeout(r, 20));
237
- const r2 = scheduler.cancel(id);
265
+ it("연속 runAfter 호출 시 persistJob 순서 보장", async () => {
266
+ const order: string[] = [];
238
267
 
239
- // 번째 cancel은 성공 (pendingJobs에서 제거)
240
- expect(r1).toBe(true);
241
- // 두 번째 cancel은 이미 제거되었으므로 false
242
- expect(r2).toBe(false);
268
+ const scheduler = createScheduler({
269
+ persistJob: async (job) => {
270
+ order.push(job.action);
271
+ },
272
+ removeJob: async () => true,
243
273
  });
244
274
 
245
- // ── persistJob 순서 보장 ──
246
-
247
- it("연속 runAfter 호출 시 persistJob 순서 보장", async () => {
248
- const order: string[] = [];
249
-
250
- const scheduler = createScheduler({
251
- persistJob: async (job) => {
252
- order.push(job.action);
253
- },
254
- removeJob: async () => true,
255
- });
275
+ scheduler.registerAction("a", async () => {});
276
+ scheduler.registerAction("b", async () => {});
277
+ scheduler.registerAction("c", async () => {});
256
278
 
257
- scheduler.registerAction("a", async () => {});
258
- scheduler.registerAction("b", async () => {});
259
- scheduler.registerAction("c", async () => {});
279
+ scheduler.runAfter(1000, "a");
280
+ scheduler.runAfter(2000, "b");
281
+ scheduler.runAfter(3000, "c");
260
282
 
261
- scheduler.runAfter(1000, "a");
262
- scheduler.runAfter(2000, "b");
263
- scheduler.runAfter(3000, "c");
283
+ await new Promise((r) => setTimeout(r, 100));
264
284
 
265
- await new Promise((r) => setTimeout(r, 100));
266
-
267
- // persistJob은 호출 순서대로 실행되어야 함
268
- expect(order).toEqual(["a", "b", "c"]);
269
- });
285
+ // persistJob은 호출 순서대로 실행되어야
286
+ expect(order).toEqual(["a", "b", "c"]);
287
+ });
270
288
  });