@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
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
91
|
+
return `${WORKFLOW_RESUME_ACTION_PREFIX}.${name}`;
|
|
109
92
|
}
|
|
110
93
|
|
|
111
94
|
export function createWorkflowRealtimeToken(): string {
|
|
112
|
-
|
|
95
|
+
return crypto.randomUUID().replace(/-/g, "");
|
|
113
96
|
}
|
|
114
97
|
|
|
115
98
|
export function getWorkflowRealtimeKey(workflowId: string, realtimeToken: string): string {
|
|
116
|
-
|
|
99
|
+
return `${WORKFLOW_REALTIME_KEY_PREFIX}.${workflowId}.${realtimeToken}`;
|
|
117
100
|
}
|
|
118
101
|
|
|
119
102
|
export function getWorkflowDef(name: string): WorkflowDef | undefined {
|
|
120
|
-
|
|
103
|
+
return workflowRegistry.get(name);
|
|
121
104
|
}
|
|
122
105
|
|
|
123
106
|
export function getRegisteredWorkflows(): WorkflowDef[] {
|
|
124
|
-
|
|
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
|
-
|
|
135
|
-
|
|
117
|
+
name: string,
|
|
118
|
+
options: WorkflowOptions<TSchema, TReturn>,
|
|
136
119
|
): MutationDef<TSchema, WorkflowStartResult> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
});
|
|
205
188
|
}
|