@gencow/core 0.1.29 → 0.1.30

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.
@@ -0,0 +1,2 @@
1
+ import { sql } from "drizzle-orm";
2
+ export declare function workflowJsonb(value: unknown): ReturnType<typeof sql>;
@@ -0,0 +1,5 @@
1
+ import { sql } from "drizzle-orm";
2
+ export function workflowJsonb(value) {
3
+ const json = JSON.stringify(value ?? {});
4
+ return sql `${json}::text::jsonb`;
5
+ }
package/dist/workflow.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { sql } from "drizzle-orm";
2
2
  import { mutation } from "./reactive-mutation.js";
3
+ import { workflowJsonb } from "./workflow-json.js";
3
4
  import { registerWorkflowsApi } from "./workflows-api.js";
4
5
  const workflowRegistry = (globalThis.__gencow_workflowRegistry ??= new Map());
5
6
  export const DEFAULT_WORKFLOW_MAX_DURATION_MS = 30 * 60 * 1000;
@@ -90,63 +91,60 @@ function isMissingWorkflowV2SchemaError(error) {
90
91
  message.includes('column "retry_count"') ||
91
92
  message.includes('column "user_id"'));
92
93
  }
93
- async function tryInsertWorkflowV2WakeOutbox(db, workflowId) {
94
- try {
95
- await db.execute(sql `
96
- INSERT INTO _gencow_workflow_outbox_v2 (
97
- id,
98
- run_id,
99
- kind,
100
- available_at,
101
- status
102
- )
103
- VALUES (
104
- ${`start:${workflowId}`},
105
- ${workflowId},
106
- 'wake_run',
107
- NOW(),
108
- 'pending'
109
- )
110
- ON CONFLICT (id) DO NOTHING
111
- `);
112
- }
113
- catch (error) {
114
- if (!isMissingWorkflowV2SchemaError(error))
115
- throw error;
116
- }
117
- }
118
94
  async function tryInsertWorkflowV2Run(options) {
119
95
  try {
120
96
  await options.db.execute(sql `
121
- INSERT INTO _gencow_workflow_runs_v2 (
122
- id,
123
- workflow_name,
124
- workflow_version,
125
- args_json,
126
- user_id,
127
- max_active_duration_ms,
128
- lifecycle_deadline_at,
129
- retry_count,
130
- max_retries,
131
- max_attempts
132
- )
133
- VALUES (
134
- ${options.workflowId},
135
- ${options.workflowName},
136
- ${options.workflowVersion ?? null},
137
- ${JSON.stringify(options.args)}::jsonb,
138
- ${options.userId},
139
- ${options.maxActiveDurationMs},
140
- CASE
141
- WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
142
- ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
143
- END,
144
- 0,
145
- ${options.maxRetries},
146
- ${options.maxRetries + 1}
97
+ WITH inserted_run AS (
98
+ INSERT INTO _gencow_workflow_runs_v2 (
99
+ id,
100
+ workflow_name,
101
+ workflow_version,
102
+ args_json,
103
+ user_id,
104
+ max_active_duration_ms,
105
+ lifecycle_deadline_at,
106
+ retry_count,
107
+ max_retries,
108
+ max_attempts
109
+ )
110
+ VALUES (
111
+ ${options.workflowId},
112
+ ${options.workflowName},
113
+ ${options.workflowVersion ?? null},
114
+ ${workflowJsonb(options.args)},
115
+ ${options.userId},
116
+ ${options.maxActiveDurationMs},
117
+ CASE
118
+ WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
119
+ ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
120
+ END,
121
+ 0,
122
+ ${options.maxRetries},
123
+ ${options.maxRetries + 1}
124
+ )
125
+ RETURNING id
126
+ ),
127
+ inserted_outbox AS (
128
+ INSERT INTO _gencow_workflow_outbox_v2 (
129
+ id,
130
+ run_id,
131
+ kind,
132
+ available_at,
133
+ status
134
+ )
135
+ SELECT
136
+ 'start:' || inserted_run.id,
137
+ inserted_run.id,
138
+ 'wake_run',
139
+ NOW(),
140
+ 'pending'
141
+ FROM inserted_run
142
+ ON CONFLICT (id) DO NOTHING
143
+ RETURNING id
147
144
  )
145
+ SELECT id
146
+ FROM inserted_run
148
147
  `);
149
- await tryInsertWorkflowV2WakeOutbox(options.db, options.workflowId);
150
148
  return true;
151
149
  }
152
150
  catch (error) {
@@ -230,7 +228,7 @@ export function workflow(name, options) {
230
228
  VALUES (
231
229
  ${workflowId},
232
230
  ${name},
233
- ${JSON.stringify(persistedArgs)}::jsonb,
231
+ ${workflowJsonb(persistedArgs)},
234
232
  ${realtimeToken},
235
233
  'pending',
236
234
  0,
@@ -2,6 +2,7 @@ import { sql } from "drizzle-orm";
2
2
  import { query } from "./reactive-query.js";
3
3
  import { mutation } from "./reactive-mutation.js";
4
4
  import { createWorkflowRealtimeToken, deserializeWorkflowValue, getWorkflowResumeActionName, getWorkflowRealtimeKey, serializeWorkflowValue, } from "./workflow.js";
5
+ import { workflowJsonb } from "./workflow-json.js";
5
6
  import { GencowValidationError, v } from "./v.js";
6
7
  import { deriveWorkflowStatus } from "./workflow-types.js";
7
8
  const WORKFLOW_STATUSES = new Set(["pending", "running", "completed", "failed"]);
@@ -160,7 +161,7 @@ async function tryRecordWorkflowV2Signal(options) {
160
161
  ${crypto.randomUUID()},
161
162
  ${options.workflowId},
162
163
  ${options.event},
163
- ${JSON.stringify(options.payload)}::jsonb,
164
+ ${workflowJsonb(options.payload)},
164
165
  ${crypto.randomUUID()}
165
166
  )
166
167
  RETURNING run_id
@@ -297,7 +298,7 @@ export function registerWorkflowsApi() {
297
298
  ${crypto.randomUUID()},
298
299
  ${workflow.id},
299
300
  ${normalizedEvent},
300
- ${JSON.stringify(persistedPayload)}::jsonb
301
+ ${workflowJsonb(persistedPayload)}
301
302
  )
302
303
  `);
303
304
  await tryRecordWorkflowV2Signal({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gencow/core",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "description": "Gencow core library — defineQuery, defineMutation, reactive subscriptions",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,6 @@
1
+ import { sql } from "drizzle-orm";
2
+
3
+ export function workflowJsonb(value: unknown): ReturnType<typeof sql> {
4
+ const json = JSON.stringify(value ?? {});
5
+ return sql`${json}::text::jsonb`;
6
+ }
package/src/workflow.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  WorkflowOptions,
8
8
  WorkflowStartResult,
9
9
  } from "./workflow-types.js";
10
+ import { workflowJsonb } from "./workflow-json.js";
10
11
  import { registerWorkflowsApi } from "./workflows-api.js";
11
12
 
12
13
  declare global {
@@ -125,33 +126,6 @@ function isMissingWorkflowV2SchemaError(error: unknown): boolean {
125
126
  );
126
127
  }
127
128
 
128
- async function tryInsertWorkflowV2WakeOutbox(
129
- db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> },
130
- workflowId: string,
131
- ): Promise<void> {
132
- try {
133
- await db.execute(sql`
134
- INSERT INTO _gencow_workflow_outbox_v2 (
135
- id,
136
- run_id,
137
- kind,
138
- available_at,
139
- status
140
- )
141
- VALUES (
142
- ${`start:${workflowId}`},
143
- ${workflowId},
144
- 'wake_run',
145
- NOW(),
146
- 'pending'
147
- )
148
- ON CONFLICT (id) DO NOTHING
149
- `);
150
- } catch (error) {
151
- if (!isMissingWorkflowV2SchemaError(error)) throw error;
152
- }
153
- }
154
-
155
129
  async function tryInsertWorkflowV2Run(options: {
156
130
  db: { execute: (query: ReturnType<typeof sql>) => Promise<unknown> };
157
131
  workflowId: string;
@@ -165,35 +139,57 @@ async function tryInsertWorkflowV2Run(options: {
165
139
  }): Promise<boolean> {
166
140
  try {
167
141
  await options.db.execute(sql`
168
- INSERT INTO _gencow_workflow_runs_v2 (
169
- id,
170
- workflow_name,
171
- workflow_version,
172
- args_json,
173
- user_id,
174
- max_active_duration_ms,
175
- lifecycle_deadline_at,
176
- retry_count,
177
- max_retries,
178
- max_attempts
179
- )
180
- VALUES (
181
- ${options.workflowId},
182
- ${options.workflowName},
183
- ${options.workflowVersion ?? null},
184
- ${JSON.stringify(options.args)}::jsonb,
185
- ${options.userId},
186
- ${options.maxActiveDurationMs},
187
- CASE
188
- WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
189
- ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
190
- END,
191
- 0,
192
- ${options.maxRetries},
193
- ${options.maxRetries + 1}
142
+ WITH inserted_run AS (
143
+ INSERT INTO _gencow_workflow_runs_v2 (
144
+ id,
145
+ workflow_name,
146
+ workflow_version,
147
+ args_json,
148
+ user_id,
149
+ max_active_duration_ms,
150
+ lifecycle_deadline_at,
151
+ retry_count,
152
+ max_retries,
153
+ max_attempts
154
+ )
155
+ VALUES (
156
+ ${options.workflowId},
157
+ ${options.workflowName},
158
+ ${options.workflowVersion ?? null},
159
+ ${workflowJsonb(options.args)},
160
+ ${options.userId},
161
+ ${options.maxActiveDurationMs},
162
+ CASE
163
+ WHEN ${options.lifecycleTimeoutMs}::bigint IS NULL THEN NULL
164
+ ELSE NOW() + (${options.lifecycleTimeoutMs}::bigint * INTERVAL '1 millisecond')
165
+ END,
166
+ 0,
167
+ ${options.maxRetries},
168
+ ${options.maxRetries + 1}
169
+ )
170
+ RETURNING id
171
+ ),
172
+ inserted_outbox AS (
173
+ INSERT INTO _gencow_workflow_outbox_v2 (
174
+ id,
175
+ run_id,
176
+ kind,
177
+ available_at,
178
+ status
179
+ )
180
+ SELECT
181
+ 'start:' || inserted_run.id,
182
+ inserted_run.id,
183
+ 'wake_run',
184
+ NOW(),
185
+ 'pending'
186
+ FROM inserted_run
187
+ ON CONFLICT (id) DO NOTHING
188
+ RETURNING id
194
189
  )
190
+ SELECT id
191
+ FROM inserted_run
195
192
  `);
196
- await tryInsertWorkflowV2WakeOutbox(options.db, options.workflowId);
197
193
  return true;
198
194
  } catch (error) {
199
195
  if (isMissingWorkflowV2SchemaError(error)) return false;
@@ -297,7 +293,7 @@ export function workflow<TSchema = any, TReturn = any>(
297
293
  VALUES (
298
294
  ${workflowId},
299
295
  ${name},
300
- ${JSON.stringify(persistedArgs)}::jsonb,
296
+ ${workflowJsonb(persistedArgs)},
301
297
  ${realtimeToken},
302
298
  'pending',
303
299
  0,
@@ -8,6 +8,7 @@ import {
8
8
  getWorkflowRealtimeKey,
9
9
  serializeWorkflowValue,
10
10
  } from "./workflow.js";
11
+ import { workflowJsonb } from "./workflow-json.js";
11
12
  import { GencowValidationError, v } from "./v.js";
12
13
  import type {
13
14
  WorkflowDerivedStatus,
@@ -246,7 +247,7 @@ async function tryRecordWorkflowV2Signal(options: {
246
247
  ${crypto.randomUUID()},
247
248
  ${options.workflowId},
248
249
  ${options.event},
249
- ${JSON.stringify(options.payload)}::jsonb,
250
+ ${workflowJsonb(options.payload)},
250
251
  ${crypto.randomUUID()}
251
252
  )
252
253
  RETURNING run_id
@@ -397,7 +398,7 @@ export function registerWorkflowsApi(): void {
397
398
  ${crypto.randomUUID()},
398
399
  ${workflow.id},
399
400
  ${normalizedEvent},
400
- ${JSON.stringify(persistedPayload)}::jsonb
401
+ ${workflowJsonb(persistedPayload)}
401
402
  )
402
403
  `);
403
404
  await tryRecordWorkflowV2Signal({