@@ -11,13 +11,14 @@ import { createLocalWorld } from '@workflow/world-local';
1111import {
1212 Logger ,
1313 makeWorkerUtils ,
14- run ,
1514 type Runner ,
15+ run ,
1616 type WorkerUtils ,
1717} from 'graphile-worker' ;
18+ import type Postgres from 'postgres' ;
1819import { monotonicFactory } from 'ulid' ;
19- import { MessageData } from './message.js' ;
2020import type { PostgresWorldConfig } from './config.js' ;
21+ import { MessageData } from './message.js' ;
2122
2223// Redirect graphile-worker logs to stderr so CLI --json on stdout stays clean.
2324// TODO: When CI=1 suppresses logging, replace with conditional stdout (e.g. log to stdout when not in JSON/CI mode).
@@ -43,7 +44,10 @@ export type PostgresQueue = Queue & {
4344 close ( ) : Promise < void > ;
4445} ;
4546
46- export function createQueue ( config : PostgresWorldConfig ) : PostgresQueue {
47+ export function createQueue (
48+ config : PostgresWorldConfig ,
49+ postgres : Postgres . Sql
50+ ) : PostgresQueue {
4751 const port = process . env . PORT ? Number ( process . env . PORT ) : undefined ;
4852 const localWorld = createLocalWorld ( { dataDir : undefined , port } ) ;
4953
@@ -66,15 +70,68 @@ export function createQueue(config: PostgresWorldConfig): PostgresQueue {
6670 let runner : Runner | null = null ;
6771 let startPromise : Promise < void > | null = null ;
6872
73+ async function migratePgBossJobs ( utils : WorkerUtils ) : Promise < void > {
74+ // Scenario A: Drizzle migration already ran — staging table exists
75+ const hasStaging = await postgres `
76+ SELECT EXISTS (
77+ SELECT 1 FROM information_schema.tables
78+ WHERE table_schema = 'workflow'
79+ AND table_name = '_pgboss_pending_jobs'
80+ ) AS exists
81+ ` ;
82+ if ( hasStaging [ 0 ] . exists ) {
83+ const jobs = await postgres `
84+ SELECT name, data, singleton_key, retry_limit
85+ FROM "workflow"."_pgboss_pending_jobs"
86+ ` ;
87+ for ( const job of jobs ) {
88+ await utils . addJob ( job . name , job . data as Record < string , unknown > , {
89+ jobKey : job . singleton_key ?? undefined ,
90+ maxAttempts : job . retry_limit ?? 3 ,
91+ } ) ;
92+ }
93+ await postgres `DROP TABLE "workflow"."_pgboss_pending_jobs"` ;
94+ return ;
95+ }
96+
97+ // Scenario B: Drizzle migration didn't run — pgboss schema still exists
98+ const hasPgBoss = await postgres `
99+ SELECT EXISTS (
100+ SELECT 1 FROM information_schema.schemata
101+ WHERE schema_name = 'pgboss'
102+ ) AS exists
103+ ` ;
104+ if ( hasPgBoss [ 0 ] . exists ) {
105+ const jobs = await postgres `
106+ SELECT name, data, singleton_key, retry_limit
107+ FROM pgboss.job
108+ WHERE state IN ('created', 'retry')
109+ ` ;
110+ for ( const job of jobs ) {
111+ await utils . addJob ( job . name , job . data as Record < string , unknown > , {
112+ jobKey : job . singleton_key ?? undefined ,
113+ maxAttempts : job . retry_limit ?? 3 ,
114+ } ) ;
115+ }
116+ await postgres `DROP SCHEMA pgboss CASCADE` ;
117+ }
118+ }
119+
69120 async function start ( ) : Promise < void > {
70121 if ( ! startPromise ) {
71122 startPromise = ( async ( ) => {
72- workerUtils = await makeWorkerUtils ( {
73- connectionString : config . connectionString ,
74- logger : stderrLogger ,
75- } ) ;
76- await workerUtils . migrate ( ) ;
77- await setupListeners ( ) ;
123+ try {
124+ workerUtils = await makeWorkerUtils ( {
125+ connectionString : config . connectionString ,
126+ logger : stderrLogger ,
127+ } ) ;
128+ await workerUtils . migrate ( ) ;
129+ await migratePgBossJobs ( workerUtils ) ;
130+ await setupListeners ( ) ;
131+ } catch ( err ) {
132+ startPromise = null ;
133+ throw err ;
134+ }
78135 } ) ( ) ;
79136 }
80137 await startPromise ;
@@ -155,6 +212,7 @@ export function createQueue(config: PostgresWorldConfig): PostgresQueue {
155212 await workerUtils . release ( ) ;
156213 workerUtils = null ;
157214 }
215+ startPromise = null ;
158216 await localWorld . close ?.( ) ;
159217 } ,
160218 } ;
0 commit comments