@workglow/postgres 0.3.0 → 0.3.2

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.
@@ -139,35 +139,18 @@ import {
139
139
  getPrefixIndexSuffix,
140
140
  PostgresDialect
141
141
  } from "@workglow/storage";
142
- var JOB_STATUS_V1 = [
143
- "PENDING",
144
- "PROCESSING",
145
- "COMPLETED",
146
- "ABORTING",
147
- "FAILED",
148
- "DISABLED"
149
- ];
150
- function assertJobStatusMatchesV1() {
151
- const current = new Set(Object.values(JobStatus));
152
- for (const v of current) {
153
- if (!JOB_STATUS_V1.includes(v)) {
154
- throw new Error(`JobStatus contains "${v}" which is not in JOB_STATUS_V1. ` + `Add a new migration that runs "ALTER TYPE job_status ADD VALUE IF NOT EXISTS '${v}'" ` + `instead of mutating the v1 enum literal.`);
155
- }
156
- }
157
- }
158
142
  function postgresQueueMigrations(tableName, prefixes) {
159
- assertJobStatusMatchesV1();
160
143
  const component = `queue:postgres:${tableName}`;
161
144
  const prefixColumnsSql = buildPrefixColumnsSql(PostgresDialect, prefixes);
162
145
  const prefixIndexPrefix = getPrefixIndexPrefix(prefixes);
163
146
  const indexSuffix = getPrefixIndexSuffix(prefixes);
147
+ const enumLiteral = Object.values(JobStatus).map((v) => `'${v}'`).join(",");
164
148
  return [
165
149
  {
166
150
  component,
167
151
  version: 1,
168
152
  description: "Create job_status enum + queue table + indexes + notify trigger",
169
153
  async up(db) {
170
- const enumLiteral = JOB_STATUS_V1.map((v) => `'${v}'`).join(",");
171
154
  await db.query(`
172
155
  DO $$
173
156
  BEGIN
@@ -185,10 +168,10 @@ function postgresQueueMigrations(tableName, prefixes) {
185
168
  status job_status NOT NULL default 'PENDING',
186
169
  input jsonb NOT NULL,
187
170
  output jsonb,
188
- run_attempts integer default 0,
189
- max_retries integer default 20,
190
- run_after timestamp with time zone DEFAULT now(),
191
- last_ran_at timestamp with time zone,
171
+ attempts integer default 0,
172
+ max_attempts integer default 10,
173
+ visible_at timestamp with time zone DEFAULT now(),
174
+ last_attempted_at timestamp with time zone,
192
175
  created_at timestamp with time zone DEFAULT now(),
193
176
  deadline_at timestamp with time zone,
194
177
  completed_at timestamp with time zone,
@@ -197,21 +180,28 @@ function postgresQueueMigrations(tableName, prefixes) {
197
180
  progress real DEFAULT 0,
198
181
  progress_message text DEFAULT '',
199
182
  progress_details jsonb,
200
- worker_id text
183
+ lease_owner text,
184
+ abort_requested_at timestamp with time zone,
185
+ lease_expires_at timestamp with time zone
201
186
  )
202
187
  `);
203
188
  await db.query(`
204
189
  CREATE INDEX IF NOT EXISTS job_fetcher${indexSuffix}_idx
205
- ON ${tableName} (${prefixIndexPrefix}id, status, run_after)
190
+ ON ${tableName} (${prefixIndexPrefix}id, status, visible_at)
206
191
  `);
207
192
  await db.query(`
208
193
  CREATE INDEX IF NOT EXISTS job_queue_fetcher${indexSuffix}_idx
209
- ON ${tableName} (${prefixIndexPrefix}queue, status, run_after)
194
+ ON ${tableName} (${prefixIndexPrefix}queue, status, visible_at)
210
195
  `);
211
196
  await db.query(`
212
197
  CREATE INDEX IF NOT EXISTS jobs_fingerprint${indexSuffix}_unique_idx
213
198
  ON ${tableName} (${prefixIndexPrefix}queue, fingerprint, status)
214
199
  `);
200
+ await db.query(`
201
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_fingerprint_active
202
+ ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)
203
+ WHERE status IN ('PENDING','PROCESSING')
204
+ `);
215
205
  const fnName = `${tableName}_notify`;
216
206
  const trgName = `${tableName}_notify_trg`;
217
207
  await db.query("SAVEPOINT install_notify_trigger");
@@ -249,92 +239,6 @@ function postgresQueueMigrations(tableName, prefixes) {
249
239
  });
250
240
  }
251
241
  }
252
- },
253
- {
254
- component,
255
- version: 2,
256
- description: "Add abort_requested_at and lease_expires_at columns",
257
- async up(db) {
258
- await db.query(`
259
- ALTER TABLE ${tableName}
260
- ADD COLUMN IF NOT EXISTS abort_requested_at timestamp with time zone,
261
- ADD COLUMN IF NOT EXISTS lease_expires_at timestamp with time zone
262
- `);
263
- }
264
- },
265
- {
266
- component,
267
- version: 3,
268
- description: "Rename run_after→visible_at, last_ran_at→last_attempted_at, run_attempts→attempts, max_retries→max_attempts, worker_id→lease_owner; drop run_after-keyed indexes and recreate visible_at-keyed",
269
- async up(db) {
270
- await db.query(`
271
- DO $$
272
- BEGIN
273
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='run_after' AND table_schema=current_schema()) THEN
274
- EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN run_after TO visible_at';
275
- END IF;
276
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='last_ran_at' AND table_schema=current_schema()) THEN
277
- EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN last_ran_at TO last_attempted_at';
278
- END IF;
279
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='run_attempts' AND table_schema=current_schema()) THEN
280
- EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN run_attempts TO attempts';
281
- END IF;
282
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='max_retries' AND table_schema=current_schema()) THEN
283
- EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN max_retries TO max_attempts';
284
- EXECUTE 'ALTER TABLE ${tableName} ALTER COLUMN max_attempts SET DEFAULT 10';
285
- END IF;
286
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='${tableName}' AND column_name='worker_id' AND table_schema=current_schema()) THEN
287
- EXECUTE 'ALTER TABLE ${tableName} RENAME COLUMN worker_id TO lease_owner';
288
- END IF;
289
- END $$
290
- `);
291
- await db.query(`DROP INDEX IF EXISTS job_fetcher${indexSuffix}_idx`);
292
- await db.query(`DROP INDEX IF EXISTS job_queue_fetcher${indexSuffix}_idx`);
293
- await db.query(`
294
- CREATE INDEX IF NOT EXISTS job_fetcher${indexSuffix}_idx
295
- ON ${tableName} (${prefixIndexPrefix}id, status, visible_at)
296
- `);
297
- await db.query(`
298
- CREATE INDEX IF NOT EXISTS job_queue_fetcher${indexSuffix}_idx
299
- ON ${tableName} (${prefixIndexPrefix}queue, status, visible_at)
300
- `);
301
- }
302
- },
303
- {
304
- component,
305
- version: 4,
306
- description: "Add UNIQUE partial index for findActiveByFingerprint O(1) lookup + fingerprint dedup at the DB layer (H2)",
307
- async up(db) {
308
- await db.query(`
309
- CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_fingerprint_active
310
- ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)
311
- WHERE status IN ('PENDING','PROCESSING')
312
- `);
313
- }
314
- },
315
- {
316
- component,
317
- version: 5,
318
- description: "Converge idx_<table>_fingerprint_active to UNIQUE for DBs that applied the pre-edit v4 (non-unique) variant",
319
- async up(db) {
320
- const indexName = `idx_${tableName}_fingerprint_active`;
321
- const result = await db.query(`SELECT i.indisunique
322
- FROM pg_class c
323
- JOIN pg_index i ON i.indexrelid = c.oid
324
- JOIN pg_namespace n ON n.oid = c.relnamespace
325
- WHERE c.relname = $1
326
- AND n.nspname = current_schema()`, [indexName]);
327
- const existing = result.rows[0];
328
- if (existing && existing.indisunique) {
329
- return;
330
- }
331
- await db.query(`DROP INDEX IF EXISTS ${indexName}`);
332
- await db.query(`
333
- CREATE UNIQUE INDEX IF NOT EXISTS ${indexName}
334
- ON ${tableName}(${prefixIndexPrefix}queue, fingerprint)
335
- WHERE status IN ('PENDING','PROCESSING')
336
- `);
337
- }
338
242
  }
339
243
  ];
340
244
  }
@@ -781,6 +685,17 @@ class PostgresQueueStorage {
781
685
  ...prefixParams
782
686
  ]);
783
687
  }
688
+ async markDisabled(id) {
689
+ const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(2);
690
+ await this.db.query(`UPDATE ${this.tableName}
691
+ SET status = 'DISABLED',
692
+ completed_at = COALESCE(completed_at, NOW() AT TIME ZONE 'UTC'),
693
+ lease_owner = NULL,
694
+ progress = 0,
695
+ progress_message = '',
696
+ progress_details = NULL
697
+ WHERE id = $1 AND queue = $2${prefixConditions}`, [id, this.queueName, ...prefixParams]);
698
+ }
784
699
  async deleteJobsByStatusAndAge(status, olderThanMs) {
785
700
  const cutoffDate = new Date(Date.now() - olderThanMs).toISOString();
786
701
  const { conditions: prefixConditions, params: prefixParams } = this.buildPrefixWhereClause(4);
@@ -1150,24 +1065,20 @@ class PostgresRateLimiterStorage {
1150
1065
  // src/job-queue/PostgresMessageQueue.ts
1151
1066
  class PostgresClaim {
1152
1067
  core;
1153
- pending;
1154
1068
  id;
1155
1069
  body;
1156
1070
  attempts;
1157
1071
  workerId;
1158
- constructor(core, pending, id, body, attempts, workerId) {
1072
+ constructor(core, id, body, attempts, workerId) {
1159
1073
  this.core = core;
1160
- this.pending = pending;
1161
1074
  this.id = id;
1162
1075
  this.body = body;
1163
1076
  this.attempts = attempts;
1164
1077
  this.workerId = workerId;
1165
1078
  }
1166
1079
  async ack(result) {
1167
- const buf = this.pending.get(this.id);
1168
- this.pending.delete(this.id);
1169
1080
  const current = await this.core.get(this.id) ?? this.body;
1170
- const output = result !== undefined ? result : buf?.output !== undefined ? buf.output : current.output ?? null;
1081
+ const output = result !== undefined ? result : current.output ?? null;
1171
1082
  await this.core.finalize(this.id, {
1172
1083
  output,
1173
1084
  error: null,
@@ -1177,7 +1088,6 @@ class PostgresClaim {
1177
1088
  });
1178
1089
  }
1179
1090
  async retry(opts) {
1180
- this.pending.delete(this.id);
1181
1091
  const delay = opts?.delaySeconds ?? 0;
1182
1092
  const current = await this.core.get(this.id) ?? this.body;
1183
1093
  await this.core.complete({
@@ -1193,12 +1103,10 @@ class PostgresClaim {
1193
1103
  }
1194
1104
  async fail(opts) {
1195
1105
  opts?.permanent;
1196
- const buf = this.pending.get(this.id);
1197
- this.pending.delete(this.id);
1198
1106
  const current = await this.core.get(this.id) ?? this.body;
1199
- const error = opts?.error !== undefined ? opts.error : buf?.error !== undefined ? buf.error : current.error ?? null;
1200
- const errorCode = opts?.errorCode !== undefined ? opts.errorCode : buf?.errorCode !== undefined ? buf.errorCode : current.error_code ?? null;
1201
- const abortRequested = opts?.abortRequested !== undefined ? opts.abortRequested : buf?.abortRequested ?? false;
1107
+ const error = opts?.error !== undefined ? opts.error : current.error ?? null;
1108
+ const errorCode = opts?.errorCode !== undefined ? opts.errorCode : current.error_code ?? null;
1109
+ const abortRequested = opts?.abortRequested === true;
1202
1110
  await this.core.finalize(this.id, {
1203
1111
  error,
1204
1112
  error_code: errorCode,
@@ -1211,7 +1119,6 @@ class PostgresClaim {
1211
1119
  await this.core.extendLease(this.id, this.workerId, ms);
1212
1120
  }
1213
1121
  async disable() {
1214
- this.pending.delete(this.id);
1215
1122
  const current = await this.core.get(this.id);
1216
1123
  const completedAt = current?.completed_at ?? new Date().toISOString();
1217
1124
  await this.core.finalize(this.id, {
@@ -1228,10 +1135,8 @@ class PostgresClaim {
1228
1135
  class PostgresMessageQueue {
1229
1136
  scope;
1230
1137
  core;
1231
- pending;
1232
- constructor(core, pending) {
1138
+ constructor(core) {
1233
1139
  this.core = core;
1234
- this.pending = pending;
1235
1140
  this.scope = core.scope;
1236
1141
  }
1237
1142
  async send(body, opts) {
@@ -1251,12 +1156,11 @@ class PostgresMessageQueue {
1251
1156
  const job = await this.core.next(opts.workerId, { leaseMs: opts.leaseMs });
1252
1157
  if (!job)
1253
1158
  break;
1254
- claims.push(new PostgresClaim(this.core, this.pending, job.id, job, job.attempts ?? 0, opts.workerId));
1159
+ claims.push(new PostgresClaim(this.core, job.id, job, job.attempts ?? 0, opts.workerId));
1255
1160
  }
1256
1161
  return claims;
1257
1162
  }
1258
1163
  async releaseClaim(id) {
1259
- this.pending.delete(id);
1260
1164
  await this.core.releaseClaim(id);
1261
1165
  }
1262
1166
  async migrate() {
@@ -1290,10 +1194,8 @@ function applySendOptions(body, opts) {
1290
1194
  // src/job-queue/PostgresJobStore.ts
1291
1195
  class PostgresJobStore {
1292
1196
  core;
1293
- pending;
1294
- constructor(core, pending) {
1197
+ constructor(core) {
1295
1198
  this.core = core;
1296
- this.pending = pending;
1297
1199
  }
1298
1200
  get(id) {
1299
1201
  return this.core.get(id);
@@ -1313,27 +1215,13 @@ class PostgresJobStore {
1313
1215
  async saveProgress(id, progress, message, details) {
1314
1216
  await this.core.saveProgress(id, progress, message, details);
1315
1217
  }
1316
- async saveResult(id, output) {
1317
- const buf = this.pending.get(id) ?? {};
1318
- buf.output = output ?? null;
1319
- this.pending.set(id, buf);
1320
- }
1321
- async saveError(id, error, errorCode, abortRequested) {
1322
- const buf = this.pending.get(id) ?? {};
1323
- buf.error = error;
1324
- buf.errorCode = errorCode;
1325
- buf.abortRequested = abortRequested;
1326
- this.pending.set(id, buf);
1327
- }
1328
1218
  async deleteByStatusAndAge(status, olderThanMs) {
1329
1219
  await this.core.deleteJobsByStatusAndAge(status, olderThanMs);
1330
1220
  }
1331
1221
  async delete(id) {
1332
- this.pending.delete(id);
1333
1222
  await this.core.delete(id);
1334
1223
  }
1335
1224
  async deleteAll() {
1336
- this.pending.clear();
1337
1225
  await this.core.deleteAll();
1338
1226
  }
1339
1227
  async abort(id) {
@@ -1359,13 +1247,14 @@ class PostgresJobStore {
1359
1247
  return this.core.getMany(ids);
1360
1248
  }
1361
1249
  async completeWithResult(id, result) {
1362
- this.pending.delete(id);
1363
1250
  await this.core.completeWithResult(id, result);
1364
1251
  }
1365
1252
  async failWithError(id, opts) {
1366
- this.pending.delete(id);
1367
1253
  await this.core.failWithError(id, opts);
1368
1254
  }
1255
+ async markDisabled(id) {
1256
+ await this.core.markDisabled(id);
1257
+ }
1369
1258
  async markEnqueueDeferred(id, opts) {
1370
1259
  await this.core.finalize(id, {
1371
1260
  visible_at: opts.visible_at.toISOString(),
@@ -1376,11 +1265,9 @@ class PostgresJobStore {
1376
1265
  // src/job-queue/createPostgresQueue.ts
1377
1266
  function createPostgresQueue(queueName, pool, opts) {
1378
1267
  const core = new PostgresQueueStorage(pool, queueName, opts);
1379
- const pending = new Map;
1380
1268
  return {
1381
- messageQueue: new PostgresMessageQueue(core, pending),
1382
- jobStore: new PostgresJobStore(core, pending),
1383
- core
1269
+ messageQueue: new PostgresMessageQueue(core),
1270
+ jobStore: new PostgresJobStore(core)
1384
1271
  };
1385
1272
  }
1386
1273
  export {
@@ -1396,4 +1283,4 @@ export {
1396
1283
  POSTGRES_QUEUE_STORAGE
1397
1284
  };
1398
1285
 
1399
- //# debugId=D9E27F175B26F25364756E2164756E21
1286
+ //# debugId=9FAE6AD52527D77864756E2164756E21