@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.
Files changed (73) 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 +46 -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 +50 -44
  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/src/db.ts +0 -18
package/src/workflow.ts CHANGED
@@ -2,126 +2,109 @@ import { sql } from "drizzle-orm";
2
2
  import { mutation } from "./reactive.js";
3
3
  import type { MutationDef } from "./reactive.js";
4
4
  import type {
5
- WorkflowDef,
6
- WorkflowDuration,
7
- WorkflowOptions,
8
- WorkflowStartResult,
5
+ WorkflowDef,
6
+ WorkflowDuration,
7
+ WorkflowOptions,
8
+ WorkflowStartResult,
9
9
  } from "./workflow-types.js";
10
10
  import { registerWorkflowsApi } from "./workflows-api.js";
11
11
 
12
12
  declare global {
13
- // eslint-disable-next-line no-var
14
- var __gencow_workflowRegistry: Map<string, WorkflowDef<any, any>>;
13
+ // eslint-disable-next-line no-var
14
+ var __gencow_workflowRegistry: Map<string, WorkflowDef<any, any>>;
15
15
  }
16
16
 
17
- const workflowRegistry =
18
- globalThis.__gencow_workflowRegistry ??= new Map<string, WorkflowDef<any, any>>();
17
+ const workflowRegistry = (globalThis.__gencow_workflowRegistry ??= new Map<string, WorkflowDef<any, any>>());
19
18
 
20
19
  export const DEFAULT_WORKFLOW_MAX_DURATION_MS = 30 * 60 * 1000;
21
20
  export const DEFAULT_WORKFLOW_MAX_RETRIES = 3;
22
21
  export const WORKFLOW_RESUME_ACTION_PREFIX = "__gencow.workflow.resume";
23
22
  export const WORKFLOW_REALTIME_KEY_PREFIX = "__gencow.workflow.state";
24
23
 
25
- type SerializedWorkflowValue =
26
- | { __gencowUndefined: true }
27
- | { value: unknown };
24
+ type SerializedWorkflowValue = { __gencowUndefined: true } | { value: unknown };
28
25
 
29
26
  function isSerializedWorkflowValue(value: unknown): value is SerializedWorkflowValue {
30
- return !!value && typeof value === "object" && (
31
- "__gencowUndefined" in value ||
32
- "value" in value
33
- );
27
+ return !!value && typeof value === "object" && ("__gencowUndefined" in value || "value" in value);
34
28
  }
35
29
 
36
30
  export function serializeWorkflowValue(value: unknown): SerializedWorkflowValue {
37
- const payload = value === undefined
38
- ? { __gencowUndefined: true as const }
39
- : { value };
40
-
41
- try {
42
- return JSON.parse(JSON.stringify(payload)) as SerializedWorkflowValue;
43
- } catch (error) {
44
- const reason = error instanceof Error ? error.message : String(error);
45
- throw new Error(
46
- `workflow() only persists JSON-serializable values. Failed to serialize workflow payload: ${reason}`
47
- );
48
- }
31
+ const payload = value === undefined ? { __gencowUndefined: true as const } : { value };
32
+
33
+ try {
34
+ return JSON.parse(JSON.stringify(payload)) as SerializedWorkflowValue;
35
+ } catch (error) {
36
+ const reason = error instanceof Error ? error.message : String(error);
37
+ throw new Error(
38
+ `workflow() only persists JSON-serializable values. Failed to serialize workflow payload: ${reason}`,
39
+ );
40
+ }
49
41
  }
50
42
 
51
43
  export function deserializeWorkflowValue(value: unknown): unknown {
52
- if (!isSerializedWorkflowValue(value)) return value;
53
- if ("__gencowUndefined" in value) return undefined;
54
- return value.value;
44
+ if (!isSerializedWorkflowValue(value)) return value;
45
+ if ("__gencowUndefined" in value) return undefined;
46
+ return value.value;
55
47
  }
56
48
 
57
49
  function clampRetries(retries: number | undefined): number {
58
- if (retries == null) return DEFAULT_WORKFLOW_MAX_RETRIES;
59
- if (!Number.isFinite(retries) || retries < 0) {
60
- throw new Error(`workflow() retries must be a non-negative finite number, got "${retries}"`);
61
- }
62
- return Math.floor(retries);
50
+ if (retries == null) return DEFAULT_WORKFLOW_MAX_RETRIES;
51
+ if (!Number.isFinite(retries) || retries < 0) {
52
+ throw new Error(`workflow() retries must be a non-negative finite number, got "${retries}"`);
53
+ }
54
+ return Math.floor(retries);
63
55
  }
64
56
 
65
57
  function parseDurationString(raw: string, label: string): number {
66
- const normalized = raw.trim().toLowerCase();
67
- const match = normalized.match(/^(\d+)(ms|s|m|h|d)$/);
68
- if (!match) {
69
- throw new Error(
70
- `${label} must be a number of ms or a string like "30m", "90s", "1h" — got "${raw}"`
71
- );
72
- }
73
- const value = Number(match[1]);
74
- const unit = match[2];
75
- const unitMs =
76
- unit === "ms" ? 1 :
77
- unit === "s" ? 1_000 :
78
- unit === "m" ? 60_000 :
79
- unit === "h" ? 3_600_000 :
80
- 86_400_000;
81
- return value * unitMs;
58
+ const normalized = raw.trim().toLowerCase();
59
+ const match = normalized.match(/^(\d+)(ms|s|m|h|d)$/);
60
+ if (!match) {
61
+ throw new Error(`${label} must be a number of ms or a string like "30m", "90s", "1h" — got "${raw}"`);
62
+ }
63
+ const value = Number(match[1]);
64
+ const unit = match[2];
65
+ const unitMs =
66
+ unit === "ms" ? 1 : unit === "s" ? 1_000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
67
+ return value * unitMs;
82
68
  }
83
69
 
84
- export function parseWorkflowDurationMs(
85
- raw: WorkflowDuration,
86
- label = "workflow duration"
87
- ): number {
88
- if (typeof raw === "number") {
89
- if (!Number.isFinite(raw) || raw <= 0) {
90
- throw new Error(`${label} must be a positive finite number, got "${raw}"`);
91
- }
92
- return Math.floor(raw);
93
- }
94
- if (typeof raw !== "string") {
95
- throw new Error(
96
- `${label} must be a positive finite number or a string like "30m", "90s", "1h" — got "${String(raw)}"`
97
- );
70
+ export function parseWorkflowDurationMs(raw: WorkflowDuration, label = "workflow duration"): number {
71
+ if (typeof raw === "number") {
72
+ if (!Number.isFinite(raw) || raw <= 0) {
73
+ throw new Error(`${label} must be a positive finite number, got "${raw}"`);
98
74
  }
99
- return parseDurationString(raw, label);
75
+ return Math.floor(raw);
76
+ }
77
+ if (typeof raw !== "string") {
78
+ throw new Error(
79
+ `${label} must be a positive finite number or a string like "30m", "90s", "1h" — got "${String(raw)}"`,
80
+ );
81
+ }
82
+ return parseDurationString(raw, label);
100
83
  }
101
84
 
102
85
  function normalizeMaxDurationMs(maxDuration: WorkflowDuration | undefined): number {
103
- if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
104
- return parseWorkflowDurationMs(maxDuration, "workflow() maxDuration");
86
+ if (maxDuration == null) return DEFAULT_WORKFLOW_MAX_DURATION_MS;
87
+ return parseWorkflowDurationMs(maxDuration, "workflow() maxDuration");
105
88
  }
106
89
 
107
90
  export function getWorkflowResumeActionName(name: string): string {
108
- return `${WORKFLOW_RESUME_ACTION_PREFIX}.${name}`;
91
+ return `${WORKFLOW_RESUME_ACTION_PREFIX}.${name}`;
109
92
  }
110
93
 
111
94
  export function createWorkflowRealtimeToken(): string {
112
- return crypto.randomUUID().replace(/-/g, "");
95
+ return crypto.randomUUID().replace(/-/g, "");
113
96
  }
114
97
 
115
98
  export function getWorkflowRealtimeKey(workflowId: string, realtimeToken: string): string {
116
- return `${WORKFLOW_REALTIME_KEY_PREFIX}.${workflowId}.${realtimeToken}`;
99
+ return `${WORKFLOW_REALTIME_KEY_PREFIX}.${workflowId}.${realtimeToken}`;
117
100
  }
118
101
 
119
102
  export function getWorkflowDef(name: string): WorkflowDef | undefined {
120
- return workflowRegistry.get(name);
103
+ return workflowRegistry.get(name);
121
104
  }
122
105
 
123
106
  export function getRegisteredWorkflows(): WorkflowDef[] {
124
- return Array.from(workflowRegistry.values());
107
+ return Array.from(workflowRegistry.values());
125
108
  }
126
109
 
127
110
  /**
@@ -131,36 +114,36 @@ export function getRegisteredWorkflows(): WorkflowDef[] {
131
114
  * frontend hooks keep working without extra workflow-specific tooling.
132
115
  */
133
116
  export function workflow<TSchema = any, TReturn = any>(
134
- name: string,
135
- options: WorkflowOptions<TSchema, TReturn>
117
+ name: string,
118
+ options: WorkflowOptions<TSchema, TReturn>,
136
119
  ): MutationDef<TSchema, WorkflowStartResult> {
137
- registerWorkflowsApi();
138
-
139
- const maxDurationMs = normalizeMaxDurationMs(options.maxDuration);
140
- const maxRetries = clampRetries(options.retries);
141
-
142
- const def: WorkflowDef<TSchema, TReturn> = {
143
- name,
144
- argsSchema: options.args,
145
- isPublic: options.public === true,
146
- maxDurationMs,
147
- maxRetries,
148
- handler: options.handler,
149
- };
150
-
151
- workflowRegistry.set(name, def);
152
-
153
- return mutation<TSchema, WorkflowStartResult>(name, {
154
- args: options.args,
155
- public: options.public,
156
- handler: async (ctx, args) => {
157
- const workflowId = crypto.randomUUID();
158
- const resumeAction = getWorkflowResumeActionName(name);
159
- const ownerId = ctx.auth.getUserIdentity()?.id ?? null;
160
- const persistedArgs = serializeWorkflowValue(args ?? {});
161
- const realtimeToken = createWorkflowRealtimeToken();
162
-
163
- await ctx.unsafeDb.execute(sql`
120
+ registerWorkflowsApi();
121
+
122
+ const maxDurationMs = normalizeMaxDurationMs(options.maxDuration);
123
+ const maxRetries = clampRetries(options.retries);
124
+
125
+ const def: WorkflowDef<TSchema, TReturn> = {
126
+ name,
127
+ argsSchema: options.args,
128
+ isPublic: options.public === true,
129
+ maxDurationMs,
130
+ maxRetries,
131
+ handler: options.handler,
132
+ };
133
+
134
+ workflowRegistry.set(name, def);
135
+
136
+ return mutation<TSchema, WorkflowStartResult>(name, {
137
+ args: options.args,
138
+ public: options.public,
139
+ handler: async (ctx, args) => {
140
+ const workflowId = crypto.randomUUID();
141
+ const resumeAction = getWorkflowResumeActionName(name);
142
+ const ownerId = ctx.auth.getUserIdentity()?.id ?? null;
143
+ const persistedArgs = serializeWorkflowValue(args ?? {});
144
+ const realtimeToken = createWorkflowRealtimeToken();
145
+
146
+ await ctx.unsafeDb.execute(sql`
164
147
  INSERT INTO _gencow_workflows (
165
148
  id,
166
149
  name,
@@ -185,21 +168,21 @@ export function workflow<TSchema = any, TReturn = any>(
185
168
  )
186
169
  `);
187
170
 
188
- try {
189
- const scheduledJobId = ctx.scheduler.runAfter(0, resumeAction, { workflowId });
190
- return {
191
- id: workflowId,
192
- name,
193
- status: "pending",
194
- scheduledJobId,
195
- };
196
- } catch (error) {
197
- await ctx.unsafeDb.execute(sql`
171
+ try {
172
+ const scheduledJobId = ctx.scheduler.runAfter(0, resumeAction, { workflowId });
173
+ return {
174
+ id: workflowId,
175
+ name,
176
+ status: "pending",
177
+ scheduledJobId,
178
+ };
179
+ } catch (error) {
180
+ await ctx.unsafeDb.execute(sql`
198
181
  DELETE FROM _gencow_workflows
199
182
  WHERE id = ${workflowId}
200
183
  `);
201
- throw error;
202
- }
203
- },
204
- });
184
+ throw error;
185
+ }
186
+ },
187
+ });
205
188
  }