bunqueue 2.9.0 → 2.9.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.
Files changed (150) hide show
  1. package/README.md +7 -3
  2. package/dist/application/background/recovery/active.d.ts +1 -0
  3. package/dist/application/background/recovery/active.js +12 -0
  4. package/dist/application/background/recovery/index.js +3 -1
  5. package/dist/application/background/recovery/pending.js +1 -0
  6. package/dist/application/cleanupTasks.js +4 -16
  7. package/dist/application/contextFactory.js +3 -0
  8. package/dist/application/dlqManager.js +1 -0
  9. package/dist/application/dlqRetry.js +1 -0
  10. package/dist/application/eventsManager.d.ts +8 -1
  11. package/dist/application/eventsManager.js +76 -29
  12. package/dist/application/operations/ackHelpers.d.ts +1 -4
  13. package/dist/application/operations/ackHelpers.js +39 -14
  14. package/dist/application/operations/flowPush.js +1 -0
  15. package/dist/application/operations/flowValidation.js +4 -4
  16. package/dist/application/operations/pull.d.ts +7 -2
  17. package/dist/application/operations/pull.js +15 -57
  18. package/dist/application/operations/pullFinalization.d.ts +6 -0
  19. package/dist/application/operations/pullFinalization.js +69 -0
  20. package/dist/application/operations/pullStateTransition.d.ts +8 -7
  21. package/dist/application/operations/pullStateTransition.js +25 -18
  22. package/dist/application/operations/push.js +3 -0
  23. package/dist/application/operations/pushBatch.js +14 -7
  24. package/dist/application/operations/pushContext.d.ts +2 -1
  25. package/dist/application/postgres-queue-manager/control.d.ts +4 -0
  26. package/dist/application/postgres-queue-manager/control.js +24 -0
  27. package/dist/application/postgres-queue-manager/delivery.d.ts +5 -4
  28. package/dist/application/postgres-queue-manager/delivery.js +13 -10
  29. package/dist/application/postgres-queue-manager/groupQueries.d.ts +10 -0
  30. package/dist/application/postgres-queue-manager/groupQueries.js +40 -0
  31. package/dist/application/postgres-queue-manager/queries.d.ts +2 -2
  32. package/dist/application/postgres-queue-manager/queries.js +2 -2
  33. package/dist/application/queue-manager/control.js +1 -0
  34. package/dist/application/queue-manager/delivery.d.ts +5 -4
  35. package/dist/application/queue-manager/delivery.js +15 -8
  36. package/dist/application/queue-manager/limits.d.ts +11 -0
  37. package/dist/application/queue-manager/limits.js +61 -0
  38. package/dist/application/queue-manager/state.js +1 -1
  39. package/dist/application/queueTelemetryJournal.d.ts +2 -0
  40. package/dist/application/queueTelemetryJournal.js +23 -0
  41. package/dist/application/types/ack.d.ts +3 -1
  42. package/dist/cli/commandRouter.d.ts +1 -1
  43. package/dist/client/flowPush.js +6 -0
  44. package/dist/client/groupId.d.ts +1 -0
  45. package/dist/client/groupId.js +1 -0
  46. package/dist/client/index.d.ts +1 -1
  47. package/dist/client/jobHelpers.js +1 -0
  48. package/dist/client/queue/operations/add/bulk.js +4 -4
  49. package/dist/client/queue/operations/add/payload.d.ts +1 -0
  50. package/dist/client/queue/operations/add/payload.js +7 -1
  51. package/dist/client/queue/operations/add/single.js +3 -3
  52. package/dist/client/queue/operations/groups.d.ts +21 -0
  53. package/dist/client/queue/operations/groups.js +88 -0
  54. package/dist/client/queue/runtime/queries.d.ts +13 -0
  55. package/dist/client/queue/runtime/queries.js +31 -0
  56. package/dist/client/queue/runtime/state.d.ts +4 -0
  57. package/dist/client/queue/runtime/state.js +1 -0
  58. package/dist/client/types/index.d.ts +2 -2
  59. package/dist/client/types/options.d.ts +4 -0
  60. package/dist/client/types/worker.d.ts +8 -0
  61. package/dist/client/worker/runtime/execution.js +1 -0
  62. package/dist/client/worker/runtime/options.js +3 -0
  63. package/dist/client/worker/types/options.d.ts +2 -1
  64. package/dist/client/worker/workerPull.d.ts +2 -0
  65. package/dist/client/worker/workerPull.js +6 -4
  66. package/dist/domain/job/groupFifoOrder.d.ts +9 -0
  67. package/dist/domain/job/groupFifoOrder.js +26 -0
  68. package/dist/domain/queue/groupLimiterManager.d.ts +23 -0
  69. package/dist/domain/queue/groupLimiterManager.js +107 -0
  70. package/dist/domain/queue/groupScheduler.d.ts +31 -0
  71. package/dist/domain/queue/groupScheduler.js +256 -0
  72. package/dist/domain/queue/groupSchedulerOrder.d.ts +6 -0
  73. package/dist/domain/queue/groupSchedulerOrder.js +30 -0
  74. package/dist/domain/queue/priorityQueue.d.ts +13 -23
  75. package/dist/domain/queue/priorityQueue.js +53 -42
  76. package/dist/domain/queue/shard/keys.d.ts +1 -0
  77. package/dist/domain/queue/shard/keys.js +25 -2
  78. package/dist/domain/queue/shard/lifecycle.js +4 -0
  79. package/dist/domain/queue/shard/limits.d.ts +20 -1
  80. package/dist/domain/queue/shard/limits.js +56 -0
  81. package/dist/domain/queue/shard/state.d.ts +5 -0
  82. package/dist/domain/queue/shard/state.js +9 -1
  83. package/dist/domain/types/commands/core.d.ts +3 -0
  84. package/dist/domain/types/commands/limits.d.ts +41 -0
  85. package/dist/domain/types/commands/union.d.ts +1 -1
  86. package/dist/domain/types/group.d.ts +21 -0
  87. package/dist/domain/types/group.js +78 -0
  88. package/dist/domain/types/priorityQueue.d.ts +1 -0
  89. package/dist/infrastructure/persistence/jobOptionsBlob.d.ts +1 -0
  90. package/dist/infrastructure/persistence/jobOptionsBlob.js +10 -2
  91. package/dist/infrastructure/persistence/migrations.js +11 -0
  92. package/dist/infrastructure/persistence/postgres/admission.js +5 -2
  93. package/dist/infrastructure/persistence/postgres/batchAdmission.js +22 -4
  94. package/dist/infrastructure/persistence/postgres/claimBatch.d.ts +3 -0
  95. package/dist/infrastructure/persistence/postgres/claimBatch.js +16 -3
  96. package/dist/infrastructure/persistence/postgres/claimSelection.d.ts +8 -1
  97. package/dist/infrastructure/persistence/postgres/claimSelection.js +73 -131
  98. package/dist/infrastructure/persistence/postgres/claims.d.ts +8 -1
  99. package/dist/infrastructure/persistence/postgres/claims.js +19 -3
  100. package/dist/infrastructure/persistence/postgres/eventCatchupCursors.d.ts +32 -0
  101. package/dist/infrastructure/persistence/postgres/eventCatchupCursors.js +66 -0
  102. package/dist/infrastructure/persistence/postgres/eventJournalSchema.d.ts +2 -2
  103. package/dist/infrastructure/persistence/postgres/eventJournalSchema.js +16 -2
  104. package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.d.ts +2 -0
  105. package/dist/infrastructure/persistence/postgres/eventPruneWatermarks.js +21 -3
  106. package/dist/infrastructure/persistence/postgres/events.d.ts +3 -3
  107. package/dist/infrastructure/persistence/postgres/events.js +44 -17
  108. package/dist/infrastructure/persistence/postgres/groupClaims.d.ts +15 -0
  109. package/dist/infrastructure/persistence/postgres/groupClaims.js +95 -0
  110. package/dist/infrastructure/persistence/postgres/groupSchema.d.ts +2 -0
  111. package/dist/infrastructure/persistence/postgres/groupSchema.js +155 -0
  112. package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.d.ts +3 -0
  113. package/dist/infrastructure/persistence/postgres/groupSchemaFingerprint.js +127 -0
  114. package/dist/infrastructure/persistence/postgres/groupStateRetention.d.ts +3 -0
  115. package/dist/infrastructure/persistence/postgres/groupStateRetention.js +93 -0
  116. package/dist/infrastructure/persistence/postgres/groups.d.ts +11 -0
  117. package/dist/infrastructure/persistence/postgres/groups.js +117 -0
  118. package/dist/infrastructure/persistence/postgres/queueDestruction.js +4 -0
  119. package/dist/infrastructure/persistence/postgres/runtime.d.ts +0 -1
  120. package/dist/infrastructure/persistence/postgres/runtime.js +3 -1
  121. package/dist/infrastructure/persistence/postgres/schema.d.ts +2 -2
  122. package/dist/infrastructure/persistence/postgres/schema.js +8 -4
  123. package/dist/infrastructure/persistence/postgres/schemaInitialization.js +6 -1
  124. package/dist/infrastructure/persistence/postgres/store.d.ts +11 -1
  125. package/dist/infrastructure/persistence/postgres/store.js +47 -3
  126. package/dist/infrastructure/persistence/postgres/telemetry.d.ts +2 -0
  127. package/dist/infrastructure/persistence/postgres/telemetry.js +33 -21
  128. package/dist/infrastructure/persistence/postgres/types.d.ts +1 -0
  129. package/dist/infrastructure/persistence/schema.d.ts +2 -2
  130. package/dist/infrastructure/persistence/schema.js +12 -1
  131. package/dist/infrastructure/persistence/sqlite/control.d.ts +14 -0
  132. package/dist/infrastructure/persistence/sqlite/control.js +69 -0
  133. package/dist/infrastructure/persistence/sqlite/jobs.d.ts +12 -0
  134. package/dist/infrastructure/persistence/sqlite/jobs.js +36 -0
  135. package/dist/infrastructure/persistence/sqlite/telemetry.d.ts +2 -1
  136. package/dist/infrastructure/persistence/sqlite/telemetry.js +5 -64
  137. package/dist/infrastructure/persistence/sqlite/telemetryWrites.d.ts +4 -0
  138. package/dist/infrastructure/persistence/sqlite/telemetryWrites.js +123 -0
  139. package/dist/infrastructure/persistence/sqliteSerializer.js +2 -0
  140. package/dist/infrastructure/server/handler-routes/control.js +13 -0
  141. package/dist/infrastructure/server/handlers/core.d.ts +0 -5
  142. package/dist/infrastructure/server/handlers/core.js +15 -18
  143. package/dist/infrastructure/server/handlers/groups.d.ts +11 -0
  144. package/dist/infrastructure/server/handlers/groups.js +55 -0
  145. package/dist/infrastructure/server/handlers/pushBatchValidation.js +4 -1
  146. package/dist/infrastructure/server/protocol/validation.d.ts +1 -0
  147. package/dist/infrastructure/server/protocol/validation.js +1 -0
  148. package/dist/infrastructure/server/protocol.d.ts +1 -1
  149. package/dist/infrastructure/server/protocol.js +1 -1
  150. package/package.json +2 -1
@@ -0,0 +1,127 @@
1
+ /** Verify every catalog property required by durable group scheduling. */
2
+ export async function hasCurrentPostgresGroupSchema(tx) {
3
+ const [schema] = await tx `
4
+ WITH expected_columns(table_name, column_name, type_name, not_null, default_kind) AS (
5
+ VALUES
6
+ ('bunqueue_jobs', 'group_order', 'bigint', FALSE, 'none'),
7
+ ('bunqueue_group_state', 'namespace', 'text', TRUE, 'none'),
8
+ ('bunqueue_group_state', 'queue', 'text', TRUE, 'none'),
9
+ ('bunqueue_group_state', 'group_id', 'text', TRUE, 'none'),
10
+ ('bunqueue_group_state', 'rate_limit', 'bigint', FALSE, 'none'),
11
+ ('bunqueue_group_state', 'rate_duration_ms', 'bigint', FALSE, 'none'),
12
+ ('bunqueue_group_state', 'rate_window_started_at', 'bigint', FALSE, 'none'),
13
+ ('bunqueue_group_state', 'rate_count', 'bigint', TRUE, 'zero'),
14
+ ('bunqueue_group_state', 'rate_effective_max', 'bigint', FALSE, 'none'),
15
+ ('bunqueue_group_state', 'rate_effective_duration_ms', 'bigint', FALSE, 'none'),
16
+ ('bunqueue_group_state', 'concurrency_limit', 'bigint', FALSE, 'none'),
17
+ ('bunqueue_group_state', 'last_served', 'bigint', FALSE, 'none')
18
+ ), expected_indexes(
19
+ index_name, table_name, columns, descending, predicate
20
+ ) AS (
21
+ VALUES
22
+ ('bunqueue_jobs_group_ready_idx', 'bunqueue_jobs',
23
+ ARRAY['namespace', 'queue', 'group_id', 'run_at', 'group_order', 'id'],
24
+ ARRAY[FALSE, FALSE, FALSE, FALSE, FALSE, FALSE],
25
+ 'group_idisnotnullandstate=anyarray[''waiting''::text,''prioritized''::text,' ||
26
+ '''delayed''::text]'),
27
+ ('bunqueue_group_state_rotation_idx', 'bunqueue_group_state',
28
+ ARRAY['namespace', 'queue', 'last_served', 'group_id'],
29
+ ARRAY[FALSE, FALSE, FALSE, FALSE], '')
30
+ )
31
+ SELECT
32
+ to_regclass('bunqueue_group_state') IS NOT NULL
33
+ AND EXISTS (
34
+ SELECT 1 FROM pg_sequence
35
+ WHERE seqrelid = to_regclass('bunqueue_group_order_seq')
36
+ AND seqtypid = to_regtype('bigint')
37
+ AND seqincrement = 1 AND seqmin = 1
38
+ AND seqmax = 9223372036854775807
39
+ AND NOT seqcycle AND seqcache = 1
40
+ )
41
+ AND (
42
+ SELECT COALESCE(bool_and(COALESCE(
43
+ attribute.atttypid = to_regtype(expected.type_name)
44
+ AND attribute.attnotnull = expected.not_null
45
+ AND CASE expected.default_kind
46
+ WHEN 'zero' THEN regexp_replace(
47
+ lower(COALESCE(pg_get_expr(defaults.adbin, defaults.adrelid), '')),
48
+ '[()[:space:]]', '', 'g'
49
+ ) IN ('0', '0::bigint')
50
+ ELSE defaults.oid IS NULL
51
+ END,
52
+ FALSE
53
+ )), FALSE)
54
+ FROM expected_columns AS expected
55
+ LEFT JOIN pg_attribute AS attribute
56
+ ON attribute.attrelid = to_regclass(expected.table_name)
57
+ AND attribute.attname = expected.column_name
58
+ AND NOT attribute.attisdropped
59
+ LEFT JOIN pg_attrdef AS defaults
60
+ ON defaults.adrelid = attribute.attrelid
61
+ AND defaults.adnum = attribute.attnum
62
+ )
63
+ AND (
64
+ SELECT COALESCE(bool_and(COALESCE(
65
+ indexes.indrelid = to_regclass(expected.table_name)
66
+ AND indexes.indisvalid AND indexes.indisready
67
+ AND NOT indexes.indisunique AND NOT indexes.indisexclusion
68
+ AND indexes.indnkeyatts = cardinality(expected.columns)
69
+ AND indexes.indnatts = indexes.indnkeyatts
70
+ AND indexes.indexprs IS NULL
71
+ AND access_method.amname = 'btree'
72
+ AND ARRAY(
73
+ SELECT attribute.attname::text
74
+ FROM unnest(indexes.indkey::smallint[]) WITH ORDINALITY AS key(attnum, position)
75
+ JOIN pg_attribute AS attribute
76
+ ON attribute.attrelid = indexes.indrelid
77
+ AND attribute.attnum = key.attnum
78
+ ORDER BY key.position
79
+ ) = expected.columns
80
+ AND ARRAY(
81
+ SELECT (option & 1) = 1
82
+ FROM unnest(indexes.indoption::smallint[])
83
+ WITH ORDINALITY AS ordering(option, position)
84
+ ORDER BY ordering.position
85
+ ) = expected.descending
86
+ AND regexp_replace(
87
+ lower(COALESCE(pg_get_expr(indexes.indpred, indexes.indrelid), '')),
88
+ '[()[:space:]]', '', 'g'
89
+ ) = expected.predicate,
90
+ FALSE
91
+ )), FALSE)
92
+ FROM expected_indexes AS expected
93
+ LEFT JOIN pg_class AS index_class
94
+ ON index_class.oid = to_regclass(expected.index_name)
95
+ LEFT JOIN pg_index AS indexes ON indexes.indexrelid = index_class.oid
96
+ LEFT JOIN pg_am AS access_method ON access_method.oid = index_class.relam
97
+ )
98
+ AND EXISTS (
99
+ SELECT 1
100
+ FROM pg_constraint AS constraint_state
101
+ JOIN pg_index AS index_state ON index_state.indexrelid = constraint_state.conindid
102
+ JOIN pg_class AS index_class ON index_class.oid = index_state.indexrelid
103
+ JOIN pg_am AS access_method ON access_method.oid = index_class.relam
104
+ WHERE constraint_state.conrelid = to_regclass('bunqueue_group_state')
105
+ AND constraint_state.contype = 'p'
106
+ AND constraint_state.convalidated
107
+ AND NOT constraint_state.condeferrable
108
+ AND NOT constraint_state.condeferred
109
+ AND index_state.indisprimary AND index_state.indisunique
110
+ AND index_state.indisvalid AND index_state.indisready
111
+ AND index_state.indimmediate
112
+ AND index_state.indnkeyatts = 3 AND index_state.indnatts = 3
113
+ AND index_state.indexprs IS NULL AND index_state.indpred IS NULL
114
+ AND access_method.amname = 'btree'
115
+ AND ARRAY(
116
+ SELECT attribute.attname::text
117
+ FROM unnest(constraint_state.conkey)
118
+ WITH ORDINALITY AS key(attnum, position)
119
+ JOIN pg_attribute AS attribute
120
+ ON attribute.attrelid = constraint_state.conrelid
121
+ AND attribute.attnum = key.attnum
122
+ ORDER BY key.position
123
+ ) = ARRAY['namespace', 'queue', 'group_id']
124
+ ) AS valid
125
+ `;
126
+ return schema.valid;
127
+ }
@@ -0,0 +1,3 @@
1
+ import { type PostgresContext } from './context';
2
+ /** Reclaim bounded batches of inactive policyless group scheduler state. */
3
+ export declare function prunePostgresGroupStates(ctx: PostgresContext, batchSize?: number, maxBatches?: number): Promise<number>;
@@ -0,0 +1,93 @@
1
+ import { databaseNow } from './context';
2
+ async function pruneBatch(tx, ctx, batchSize) {
3
+ const now = await databaseNow(tx);
4
+ const rows = await tx `
5
+ SELECT groups.queue, groups.group_id
6
+ FROM bunqueue_group_state AS groups
7
+ WHERE groups.namespace = ${ctx.config.namespace}
8
+ AND NOT EXISTS (
9
+ SELECT 1 FROM bunqueue_jobs AS job
10
+ WHERE job.namespace = groups.namespace
11
+ AND job.queue = groups.queue
12
+ AND job.group_id = groups.group_id
13
+ AND job.state IN (
14
+ 'waiting', 'prioritized', 'delayed', 'waiting-children', 'active'
15
+ )
16
+ )
17
+ AND (
18
+ groups.last_served IS NOT NULL
19
+ OR (
20
+ groups.rate_limit IS NULL
21
+ AND groups.rate_duration_ms IS NULL
22
+ AND groups.concurrency_limit IS NULL
23
+ AND (
24
+ groups.rate_window_started_at IS NULL
25
+ OR (
26
+ groups.rate_effective_duration_ms IS NOT NULL
27
+ AND groups.rate_window_started_at + groups.rate_effective_duration_ms <= ${now}
28
+ )
29
+ )
30
+ )
31
+ )
32
+ ORDER BY groups.queue, groups.group_id
33
+ LIMIT ${batchSize}
34
+ FOR UPDATE OF groups SKIP LOCKED
35
+ `;
36
+ if (rows.length === 0)
37
+ return { deleted: 0, selected: 0 };
38
+ const queues = rows.map(({ queue }) => queue);
39
+ const groupIds = rows.map(({ group_id }) => group_id);
40
+ await tx `
41
+ UPDATE bunqueue_group_state AS groups
42
+ SET last_served = NULL
43
+ FROM unnest(
44
+ ${tx.array(queues, 'TEXT')},
45
+ ${tx.array(groupIds, 'TEXT')}
46
+ ) AS target(queue, group_id)
47
+ WHERE groups.namespace = ${ctx.config.namespace}
48
+ AND groups.queue = target.queue
49
+ AND groups.group_id = target.group_id
50
+ `;
51
+ const deleted = await tx `
52
+ DELETE FROM bunqueue_group_state AS groups
53
+ USING unnest(
54
+ ${tx.array(queues, 'TEXT')},
55
+ ${tx.array(groupIds, 'TEXT')}
56
+ ) AS target(queue, group_id)
57
+ WHERE groups.namespace = ${ctx.config.namespace}
58
+ AND groups.queue = target.queue
59
+ AND groups.group_id = target.group_id
60
+ AND groups.rate_limit IS NULL
61
+ AND groups.rate_duration_ms IS NULL
62
+ AND groups.concurrency_limit IS NULL
63
+ AND (
64
+ groups.rate_window_started_at IS NULL
65
+ OR (
66
+ groups.rate_effective_duration_ms IS NOT NULL
67
+ AND groups.rate_window_started_at + groups.rate_effective_duration_ms <= ${now}
68
+ )
69
+ )
70
+ AND NOT EXISTS (
71
+ SELECT 1 FROM bunqueue_jobs AS job
72
+ WHERE job.namespace = groups.namespace
73
+ AND job.queue = groups.queue
74
+ AND job.group_id = groups.group_id
75
+ AND job.state IN (
76
+ 'waiting', 'prioritized', 'delayed', 'waiting-children', 'active'
77
+ )
78
+ )
79
+ RETURNING groups.group_id
80
+ `;
81
+ return { deleted: deleted.length, selected: rows.length };
82
+ }
83
+ /** Reclaim bounded batches of inactive policyless group scheduler state. */
84
+ export async function prunePostgresGroupStates(ctx, batchSize = 1000, maxBatches = 10) {
85
+ let deleted = 0;
86
+ for (let batch = 0; batch < maxBatches; batch++) {
87
+ const result = await ctx.sql.begin((tx) => pruneBatch(tx, ctx, batchSize));
88
+ deleted += result.deleted;
89
+ if (result.selected < batchSize)
90
+ break;
91
+ }
92
+ return deleted;
93
+ }
@@ -0,0 +1,11 @@
1
+ import { type GroupRateLimitOverride } from '../../../domain/types/group';
2
+ import { type PostgresContext } from './context';
3
+ export declare function getPostgresGroupJobsCount(ctx: PostgresContext, queue: string, groupId?: string): Promise<number>;
4
+ export declare function getPostgresGroupActiveCount(ctx: PostgresContext, queue: string, groupId: string): Promise<number>;
5
+ export declare function setPostgresGroupRateLimit(ctx: PostgresContext, queue: string, groupId: string, max: number, duration: number): Promise<void>;
6
+ export declare function getPostgresGroupRateLimit(ctx: PostgresContext, queue: string, groupId: string): Promise<GroupRateLimitOverride | null>;
7
+ export declare function removePostgresGroupRateLimit(ctx: PostgresContext, queue: string, groupId: string): Promise<number>;
8
+ export declare function getPostgresGroupRateLimitTtl(ctx: PostgresContext, queue: string, groupId: string, maxJobs?: number): Promise<number>;
9
+ export declare function setPostgresGroupConcurrency(ctx: PostgresContext, queue: string, groupId: string, concurrency: number): Promise<void>;
10
+ export declare function getPostgresGroupConcurrency(ctx: PostgresContext, queue: string, groupId: string): Promise<number | null>;
11
+ export declare function removePostgresGroupConcurrency(ctx: PostgresContext, queue: string, groupId: string): Promise<number>;
@@ -0,0 +1,117 @@
1
+ import { assertGroupId, assertPositiveSafeInteger, } from '../../../domain/types/group';
2
+ import { databaseNow } from './context';
3
+ export async function getPostgresGroupJobsCount(ctx, queue, groupId) {
4
+ if (groupId !== undefined)
5
+ assertGroupId(groupId);
6
+ const [row] = groupId === undefined
7
+ ? await ctx.sql `
8
+ SELECT COUNT(*)::bigint AS count FROM bunqueue_jobs
9
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
10
+ AND group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed')
11
+ `
12
+ : await ctx.sql `
13
+ SELECT COUNT(*)::bigint AS count FROM bunqueue_jobs
14
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
15
+ AND group_id = ${groupId} AND state IN ('waiting', 'prioritized', 'delayed')
16
+ `;
17
+ return Number(row.count);
18
+ }
19
+ export async function getPostgresGroupActiveCount(ctx, queue, groupId) {
20
+ assertGroupId(groupId);
21
+ const now = await databaseNow(ctx.sql);
22
+ const [row] = await ctx.sql `
23
+ SELECT COUNT(*)::bigint AS count FROM bunqueue_jobs
24
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
25
+ AND group_id = ${groupId} AND state = 'active' AND lease_until > ${now}
26
+ `;
27
+ return Number(row.count);
28
+ }
29
+ export async function setPostgresGroupRateLimit(ctx, queue, groupId, max, duration) {
30
+ assertGroupId(groupId);
31
+ assertPositiveSafeInteger(max, 'max');
32
+ assertPositiveSafeInteger(duration, 'duration');
33
+ await ctx.sql `
34
+ INSERT INTO bunqueue_group_state (
35
+ namespace, queue, group_id, rate_limit, rate_duration_ms
36
+ ) VALUES (${ctx.config.namespace}, ${queue}, ${groupId}, ${max}, ${duration})
37
+ ON CONFLICT (namespace, queue, group_id) DO UPDATE
38
+ SET rate_limit = EXCLUDED.rate_limit,
39
+ rate_duration_ms = EXCLUDED.rate_duration_ms,
40
+ rate_window_started_at = NULL,
41
+ rate_count = 0,
42
+ rate_effective_max = NULL,
43
+ rate_effective_duration_ms = NULL
44
+ `;
45
+ }
46
+ export async function getPostgresGroupRateLimit(ctx, queue, groupId) {
47
+ assertGroupId(groupId);
48
+ const rows = await ctx.sql `
49
+ SELECT rate_limit, rate_duration_ms, rate_window_started_at, rate_count,
50
+ rate_effective_duration_ms, concurrency_limit
51
+ FROM bunqueue_group_state
52
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue} AND group_id = ${groupId}
53
+ `;
54
+ const row = rows[0];
55
+ if (!row || row.rate_limit === null || row.rate_duration_ms === null)
56
+ return null;
57
+ return { max: Number(row.rate_limit), duration: Number(row.rate_duration_ms) };
58
+ }
59
+ export async function removePostgresGroupRateLimit(ctx, queue, groupId) {
60
+ assertGroupId(groupId);
61
+ const rows = await ctx.sql `
62
+ UPDATE bunqueue_group_state
63
+ SET rate_limit = NULL, rate_duration_ms = NULL, rate_window_started_at = NULL,
64
+ rate_count = 0, rate_effective_max = NULL, rate_effective_duration_ms = NULL
65
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue} AND group_id = ${groupId}
66
+ AND rate_limit IS NOT NULL
67
+ RETURNING group_id
68
+ `;
69
+ return rows.length;
70
+ }
71
+ export async function getPostgresGroupRateLimitTtl(ctx, queue, groupId, maxJobs) {
72
+ assertGroupId(groupId);
73
+ const rows = await ctx.sql `
74
+ SELECT rate_limit, rate_duration_ms, rate_window_started_at, rate_count,
75
+ rate_effective_duration_ms, concurrency_limit
76
+ FROM bunqueue_group_state
77
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue} AND group_id = ${groupId}
78
+ `;
79
+ const row = rows[0];
80
+ if (!row?.rate_window_started_at || row.rate_effective_duration_ms === null)
81
+ return -2;
82
+ if (maxJobs !== undefined && Number(row.rate_count) < maxJobs)
83
+ return 0;
84
+ const remaining = Number(row.rate_window_started_at) +
85
+ Number(row.rate_effective_duration_ms) -
86
+ (await databaseNow(ctx.sql));
87
+ return remaining > 0 ? remaining : -2;
88
+ }
89
+ export async function setPostgresGroupConcurrency(ctx, queue, groupId, concurrency) {
90
+ assertGroupId(groupId);
91
+ assertPositiveSafeInteger(concurrency, 'concurrency');
92
+ await ctx.sql `
93
+ INSERT INTO bunqueue_group_state (namespace, queue, group_id, concurrency_limit)
94
+ VALUES (${ctx.config.namespace}, ${queue}, ${groupId}, ${concurrency})
95
+ ON CONFLICT (namespace, queue, group_id) DO UPDATE
96
+ SET concurrency_limit = EXCLUDED.concurrency_limit
97
+ `;
98
+ }
99
+ export async function getPostgresGroupConcurrency(ctx, queue, groupId) {
100
+ assertGroupId(groupId);
101
+ const rows = await ctx.sql `
102
+ SELECT concurrency_limit FROM bunqueue_group_state
103
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue} AND group_id = ${groupId}
104
+ `;
105
+ const value = rows[0]?.concurrency_limit;
106
+ return value === undefined || value === null ? null : Number(value);
107
+ }
108
+ export async function removePostgresGroupConcurrency(ctx, queue, groupId) {
109
+ assertGroupId(groupId);
110
+ const rows = await ctx.sql `
111
+ UPDATE bunqueue_group_state SET concurrency_limit = NULL
112
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue} AND group_id = ${groupId}
113
+ AND concurrency_limit IS NOT NULL
114
+ RETURNING group_id
115
+ `;
116
+ return rows.length;
117
+ }
@@ -135,6 +135,10 @@ export async function obliteratePostgresQueue(ctx, queue) {
135
135
  `;
136
136
  }
137
137
  await tx `
138
+ DELETE FROM bunqueue_group_state
139
+ WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
140
+ `;
141
+ await tx `
138
142
  DELETE FROM bunqueue_queue_state
139
143
  WHERE namespace = ${ctx.config.namespace} AND queue = ${queue}
140
144
  `;
@@ -1,7 +1,6 @@
1
1
  import { PostgresEventStream } from './events';
2
2
  import type { PostgresContext } from './context';
3
3
  import type { PostgresDeliveredStoreEvent, PostgresStorageConfig, PostgresStorageHealth } from './types';
4
- /** Connection, schema, event stream, health, and maintenance lifecycle. */
5
4
  export declare class PostgresQueueStoreRuntime {
6
5
  readonly context: PostgresContext;
7
6
  readonly events: PostgresEventStream;
@@ -9,13 +9,13 @@ import { initializePostgresSchema } from './schemaInitialization';
9
9
  import { PostgresPostCommitMaintenance } from './postCommitMaintenance';
10
10
  import { sweepPostgresEventRetention } from './telemetry';
11
11
  import { prunePostgresCompletionTombstones } from './completionLifecycle';
12
+ import { prunePostgresGroupStates } from './groupStateRetention';
12
13
  import { PostgresMaintenanceFlights } from './maintenanceFlights';
13
14
  import { resolvePostgresRuntimeConfig } from './runtimeConfig';
14
15
  import { PostgresEventCommitGc } from './eventCommitGc';
15
16
  import { createPostgresConnection } from './connection';
16
17
  import { PostgresStorageHealthTracker } from './storageHealth';
17
18
  import { heartbeatPostgresBroker, PostgresBrokerSessionFencedError, purgeStalePostgresBrokers, registerPostgresBroker, unregisterPostgresBroker, } from './brokerSessions';
18
- /** Connection, schema, event stream, health, and maintenance lifecycle. */
19
19
  export class PostgresQueueStoreRuntime {
20
20
  context;
21
21
  events;
@@ -192,6 +192,7 @@ export class PostgresQueueStoreRuntime {
192
192
  await recoverExpiredPostgresLeases(this.context);
193
193
  await repairPostgresDlq(this.context);
194
194
  await prunePostgresCompletionTombstones(this.context);
195
+ await prunePostgresGroupStates(this.context);
195
196
  await sweepPostgresEventRetention(this.context);
196
197
  await prunePostgresEventCommits(this.context);
197
198
  await this.events.start();
@@ -217,6 +218,7 @@ export class PostgresQueueStoreRuntime {
217
218
  }, 'dlq');
218
219
  void this.runMaintenance(() => sweepPostgresEventRetention(this.context), 'event-retention');
219
220
  void this.postCommitMaintenance.run('completion-retention', () => prunePostgresCompletionTombstones(this.context));
221
+ void this.postCommitMaintenance.run('group-state-retention', () => prunePostgresGroupStates(this.context));
220
222
  }, 60_000);
221
223
  this.cronTimer = setInterval(() => void this.runMaintenance(() => processPostgresCrons(this.context), 'cron'), this.config.pollIntervalMs);
222
224
  }
@@ -1,4 +1,4 @@
1
- export declare const POSTGRES_SCHEMA_VERSION = 17;
1
+ export declare const POSTGRES_SCHEMA_VERSION = 19;
2
2
  /** PostgreSQL schema for the database-authoritative distributed queue engine. */
3
- export declare const POSTGRES_SCHEMA = "\nCREATE TABLE IF NOT EXISTS bunqueue_schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at BIGINT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS bunqueue_jobs (\n namespace TEXT NOT NULL,\n id TEXT NOT NULL,\n queue TEXT NOT NULL,\n payload BYTEA NOT NULL,\n state TEXT NOT NULL CHECK (\n state IN ('waiting', 'prioritized', 'delayed', 'waiting-children',\n 'active', 'completed', 'failed')\n ),\n priority INTEGER NOT NULL DEFAULT 0,\n lifo BOOLEAN NOT NULL DEFAULT FALSE,\n run_at BIGINT NOT NULL,\n created_at BIGINT NOT NULL,\n started_at BIGINT,\n completed_at BIGINT,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n ttl BIGINT,\n timeout BIGINT,\n unique_key TEXT,\n unique_expires_at BIGINT,\n custom_id TEXT,\n group_id TEXT,\n parent_id TEXT,\n lease_owner TEXT,\n lease_broker_id TEXT,\n lease_broker_session_id TEXT,\n lease_token TEXT,\n lease_until BIGINT,\n lease_renewals INTEGER NOT NULL DEFAULT 0,\n result BYTEA,\n dlq_entry BYTEA,\n dlq_retry_state BYTEA,\n error TEXT,\n failure_reason TEXT,\n version BIGINT NOT NULL DEFAULT 0,\n PRIMARY KEY (namespace, id)\n);\n\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_ready_idx\n ON bunqueue_jobs(namespace, queue, priority DESC, run_at ASC, id ASC)\n WHERE state IN ('waiting', 'prioritized', 'delayed');\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_state_idx\n ON bunqueue_jobs(namespace, queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_lease_idx\n ON bunqueue_jobs(namespace, lease_until)\n WHERE state = 'active';\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_parent_idx\n ON bunqueue_jobs(namespace, parent_id)\n WHERE parent_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_group_ready_idx\n ON bunqueue_jobs(namespace, queue, group_id, priority DESC, run_at ASC, id ASC)\n WHERE group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_group_active_idx\n ON bunqueue_jobs(namespace, queue, group_id, lease_until)\n WHERE group_id IS NOT NULL AND state = 'active';\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_lifo_ready_idx\n ON bunqueue_jobs(namespace, queue, priority DESC, id DESC)\n WHERE group_id IS NULL AND lifo AND state IN ('waiting', 'prioritized', 'delayed');\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_ttl_pending_idx\n ON bunqueue_jobs(namespace, queue, created_at, id)\n WHERE ttl IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS unique_expires_at BIGINT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_broker_id TEXT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_broker_session_id TEXT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_renewals INTEGER NOT NULL DEFAULT 0;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS dlq_retry_state BYTEA;\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_broker_session_lease_idx\n ON bunqueue_jobs(namespace, lease_broker_id, lease_broker_session_id, id)\n WHERE state = 'active';\n\nCREATE TABLE IF NOT EXISTS bunqueue_dependencies (\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n dependency_id TEXT NOT NULL,\n PRIMARY KEY (namespace, job_id, dependency_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_dependencies_dependency_idx\n ON bunqueue_dependencies(namespace, dependency_id, job_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_flow_failures (\n namespace TEXT NOT NULL,\n parent_id TEXT NOT NULL,\n child_id TEXT NOT NULL,\n child_queue TEXT NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('fail', 'remove', 'ignore', 'continue')),\n error TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, parent_id, child_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_flow_failures_parent_idx\n ON bunqueue_flow_failures(namespace, parent_id, mode);\n\nCREATE TABLE IF NOT EXISTS bunqueue_repeat_links (\n namespace TEXT NOT NULL,\n original_id TEXT NOT NULL,\n successor_id TEXT NOT NULL,\n PRIMARY KEY (namespace, original_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_repeat_links_successor_idx\n ON bunqueue_repeat_links(namespace, successor_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_job_logs (\n id BIGSERIAL PRIMARY KEY,\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n timestamp BIGINT NOT NULL,\n level TEXT NOT NULL CHECK (level IN ('info', 'warn', 'error')),\n message TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS bunqueue_job_logs_job_idx\n ON bunqueue_job_logs(namespace, job_id, id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_crons (\n namespace TEXT NOT NULL,\n name TEXT NOT NULL,\n payload BYTEA NOT NULL,\n next_run BIGINT NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n updated_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, name)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_crons_due_idx\n ON bunqueue_crons(namespace, next_run, name);\n\nCREATE TABLE IF NOT EXISTS bunqueue_workers (\n namespace TEXT NOT NULL,\n id TEXT NOT NULL,\n broker_id TEXT NOT NULL,\n broker_session_id TEXT,\n client_id TEXT,\n queues TEXT[] NOT NULL,\n payload BYTEA NOT NULL,\n last_seen BIGINT NOT NULL,\n PRIMARY KEY (namespace, id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_workers_queue_idx\n ON bunqueue_workers USING GIN(queues);\nCREATE INDEX IF NOT EXISTS bunqueue_workers_broker_idx\n ON bunqueue_workers(namespace, broker_id, client_id);\nALTER TABLE bunqueue_workers\n ADD COLUMN IF NOT EXISTS broker_session_id TEXT;\nCREATE INDEX IF NOT EXISTS bunqueue_workers_broker_session_idx\n ON bunqueue_workers(namespace, broker_id, broker_session_id, client_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_metric_buckets (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n metric_type TEXT NOT NULL CHECK (metric_type IN ('completed', 'failed')),\n minute BIGINT NOT NULL,\n count INTEGER NOT NULL,\n PRIMARY KEY (namespace, queue, metric_type, minute)\n);\nCREATE TABLE IF NOT EXISTS bunqueue_metric_totals (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n metric_type TEXT NOT NULL CHECK (metric_type IN ('completed', 'failed')),\n total_count BIGINT NOT NULL,\n prev_ts BIGINT NOT NULL,\n prev_count INTEGER NOT NULL,\n PRIMARY KEY (namespace, queue, metric_type)\n);\n\nCREATE TABLE IF NOT EXISTS bunqueue_completions (\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n completed_at BIGINT NOT NULL,\n result BYTEA,\n PRIMARY KEY (namespace, job_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_completions_queue_idx\n ON bunqueue_completions(namespace, queue, job_id);\nCREATE INDEX IF NOT EXISTS bunqueue_completions_recent_idx\n ON bunqueue_completions(namespace, completed_at DESC, job_id DESC);\n\nCREATE TABLE IF NOT EXISTS bunqueue_queue_state (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n paused BOOLEAN NOT NULL DEFAULT FALSE,\n rate_limit INTEGER,\n rate_duration_ms BIGINT,\n rate_window_started_at BIGINT,\n rate_expires_at BIGINT,\n rate_count INTEGER NOT NULL DEFAULT 0,\n concurrency_limit INTEGER,\n stall_config BYTEA,\n dlq_config BYTEA,\n PRIMARY KEY (namespace, queue)\n);\n\nALTER TABLE bunqueue_queue_state\n ADD COLUMN IF NOT EXISTS rate_expires_at BIGINT;\n\nINSERT INTO bunqueue_queue_state (namespace, queue)\nSELECT DISTINCT namespace, queue FROM bunqueue_jobs\nON CONFLICT (namespace, queue) DO NOTHING;\n\nCREATE TABLE IF NOT EXISTS bunqueue_events (\n id BIGSERIAL PRIMARY KEY,\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n event_type TEXT NOT NULL,\n job_id TEXT NOT NULL,\n occurred_at BIGINT NOT NULL,\n payload BYTEA\n);\nCREATE INDEX IF NOT EXISTS bunqueue_events_namespace_id_idx\n ON bunqueue_events(namespace, id);\nCREATE INDEX IF NOT EXISTS bunqueue_events_queue_id_idx\n ON bunqueue_events(namespace, queue, id DESC);\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_prune_watermarks (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n source_event_id BIGINT NOT NULL,\n pruned_through BIGINT NOT NULL,\n PRIMARY KEY (namespace, queue, source_event_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_event_prune_watermarks_namespace_queue_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, pruned_through DESC);\n\n\nCREATE SEQUENCE IF NOT EXISTS bunqueue_event_commit_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_event_commit_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_commits (\n namespace TEXT NOT NULL,\n transaction_id BIGINT NOT NULL,\n commit_seq BIGINT,\n PRIMARY KEY (namespace, transaction_id)\n);\n\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS namespace TEXT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint;\n\nALTER TABLE bunqueue_events\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id DROP DEFAULT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\n\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS prunes_current_transaction BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id DROP DEFAULT,\n ALTER COLUMN pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN prunes_current_transaction DROP DEFAULT;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint,\n ALTER COLUMN pruned_commit_seq TYPE BIGINT USING pruned_commit_seq::bigint,\n ALTER COLUMN prunes_current_transaction TYPE BOOLEAN\n USING prunes_current_transaction::boolean;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN prunes_current_transaction SET DEFAULT FALSE;\n\nUPDATE bunqueue_events\nSET transaction_id = -id\nWHERE transaction_id IS NULL;\nUPDATE bunqueue_event_prune_watermarks\nSET transaction_id = COALESCE(transaction_id, -source_event_id),\n commit_seq = COALESCE(commit_seq, source_event_id),\n pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id)\nWHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL;\n\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET NOT NULL,\n ALTER COLUMN pruned_commit_seq SET NOT NULL,\n ALTER COLUMN prunes_current_transaction SET NOT NULL;\n\nDO $migration$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('bunqueue_events')\n AND attname = 'commit_seq' AND NOT attisdropped\n ) THEN\n EXECUTE $sql$\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(commit_seq)\n FROM bunqueue_events\n WHERE commit_seq IS NOT NULL\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n )\n $sql$;\n ELSE\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(id)\n FROM bunqueue_events\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO NOTHING;\n END IF;\nEND;\n$migration$;\n\nINSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\nSELECT namespace, transaction_id, MAX(commit_seq)\nFROM bunqueue_event_prune_watermarks\nWHERE commit_seq IS NOT NULL\nGROUP BY namespace, transaction_id\nON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n );\n\nDO $block$\nDECLARE\n journal_floor BIGINT := 0;\n legacy_head_floor BIGINT := 0;\n sequence_floor BIGINT := 0;\nBEGIN\n SELECT COALESCE(MAX(commit_seq), 0) INTO journal_floor\n FROM bunqueue_event_commits;\n SELECT last_value INTO sequence_floor FROM bunqueue_event_commit_seq;\n IF to_regclass('bunqueue_event_commit_heads') IS NOT NULL THEN\n EXECUTE 'SELECT COALESCE(MAX(commit_seq), 0) FROM bunqueue_event_commit_heads'\n INTO legacy_head_floor;\n END IF;\n PERFORM setval(\n 'bunqueue_event_commit_seq',\n GREATEST(journal_floor, legacy_head_floor, sequence_floor, 1),\n TRUE\n );\nEND;\n$block$;\n\nDROP TABLE IF EXISTS bunqueue_event_commit_heads;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TABLE IF EXISTS bunqueue_event_commit_assignments CASCADE;\nDROP INDEX IF EXISTS bunqueue_events_commit_idx;\nDROP INDEX IF EXISTS bunqueue_events_pending_commit_idx;\nALTER TABLE bunqueue_events DROP COLUMN IF EXISTS commit_seq;\n\nDROP INDEX IF EXISTS bunqueue_events_transaction_idx;\nDROP INDEX IF EXISTS bunqueue_event_commits_replay_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_pending_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_transaction_idx;\n\nCREATE INDEX bunqueue_events_transaction_idx\n ON bunqueue_events(namespace, transaction_id, id);\nCREATE INDEX bunqueue_event_commits_replay_idx\n ON bunqueue_event_commits(namespace, commit_seq, transaction_id)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, commit_seq DESC)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_pending_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id)\n WHERE commit_seq IS NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_transaction_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id);\n\nCREATE OR REPLACE FUNCTION bunqueue_register_event_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_event_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_register_watermark_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_watermark_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_assign_event_commit()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nDECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;\n$function$;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nCREATE TRIGGER bunqueue_events_register_commit\nAFTER INSERT ON bunqueue_events\nREFERENCING NEW TABLE AS new_event_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_event_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_insert_register_commit\nAFTER INSERT ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_update_register_commit\nAFTER UPDATE ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_assign_event_commit\n ON bunqueue_event_commits;\nCREATE CONSTRAINT TRIGGER bunqueue_assign_event_commit\nAFTER INSERT ON bunqueue_event_commits\nDEFERRABLE INITIALLY DEFERRED\nFOR EACH ROW EXECUTE FUNCTION bunqueue_assign_event_commit();\n\n\nCREATE TABLE IF NOT EXISTS bunqueue_brokers (\n namespace TEXT NOT NULL,\n broker_id TEXT NOT NULL,\n session_id TEXT,\n started_at BIGINT NOT NULL,\n heartbeat_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, broker_id)\n);\nALTER TABLE bunqueue_brokers\n ADD COLUMN IF NOT EXISTS session_id TEXT;\n\nDROP INDEX IF EXISTS bunqueue_jobs_live_unique_key_idx;\nCREATE UNIQUE INDEX bunqueue_jobs_live_unique_key_idx\n ON bunqueue_jobs(namespace, queue, unique_key)\n WHERE unique_key IS NOT NULL\n AND state IN ('waiting', 'prioritized', 'delayed', 'waiting-children', 'active');\n";
3
+ export declare const POSTGRES_SCHEMA = "\nCREATE TABLE IF NOT EXISTS bunqueue_schema_migrations (\n version INTEGER PRIMARY KEY,\n applied_at BIGINT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS bunqueue_jobs (\n namespace TEXT NOT NULL,\n id TEXT NOT NULL,\n queue TEXT NOT NULL,\n payload BYTEA NOT NULL,\n state TEXT NOT NULL CHECK (\n state IN ('waiting', 'prioritized', 'delayed', 'waiting-children',\n 'active', 'completed', 'failed')\n ),\n priority INTEGER NOT NULL DEFAULT 0,\n lifo BOOLEAN NOT NULL DEFAULT FALSE,\n run_at BIGINT NOT NULL,\n created_at BIGINT NOT NULL,\n started_at BIGINT,\n completed_at BIGINT,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n ttl BIGINT,\n timeout BIGINT,\n unique_key TEXT,\n unique_expires_at BIGINT,\n custom_id TEXT,\n group_id TEXT,\n group_order BIGINT,\n parent_id TEXT,\n lease_owner TEXT,\n lease_broker_id TEXT,\n lease_broker_session_id TEXT,\n lease_token TEXT,\n lease_until BIGINT,\n lease_renewals INTEGER NOT NULL DEFAULT 0,\n result BYTEA,\n dlq_entry BYTEA,\n dlq_retry_state BYTEA,\n error TEXT,\n failure_reason TEXT,\n version BIGINT NOT NULL DEFAULT 0,\n PRIMARY KEY (namespace, id)\n);\n\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_ready_idx\n ON bunqueue_jobs(namespace, queue, priority DESC, run_at ASC, id ASC)\n WHERE state IN ('waiting', 'prioritized', 'delayed');\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_state_idx\n ON bunqueue_jobs(namespace, queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_lease_idx\n ON bunqueue_jobs(namespace, lease_until)\n WHERE state = 'active';\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_parent_idx\n ON bunqueue_jobs(namespace, parent_id)\n WHERE parent_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_group_active_idx\n ON bunqueue_jobs(namespace, queue, group_id, lease_until)\n WHERE group_id IS NOT NULL AND state = 'active';\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_lifo_ready_idx\n ON bunqueue_jobs(namespace, queue, priority DESC, id DESC)\n WHERE group_id IS NULL AND lifo AND state IN ('waiting', 'prioritized', 'delayed');\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_ttl_pending_idx\n ON bunqueue_jobs(namespace, queue, created_at, id)\n WHERE ttl IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS unique_expires_at BIGINT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_broker_id TEXT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_broker_session_id TEXT;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS lease_renewals INTEGER NOT NULL DEFAULT 0;\nALTER TABLE bunqueue_jobs\n ADD COLUMN IF NOT EXISTS dlq_retry_state BYTEA;\nCREATE INDEX IF NOT EXISTS bunqueue_jobs_broker_session_lease_idx\n ON bunqueue_jobs(namespace, lease_broker_id, lease_broker_session_id, id)\n WHERE state = 'active';\n\nCREATE TABLE IF NOT EXISTS bunqueue_dependencies (\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n dependency_id TEXT NOT NULL,\n PRIMARY KEY (namespace, job_id, dependency_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_dependencies_dependency_idx\n ON bunqueue_dependencies(namespace, dependency_id, job_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_flow_failures (\n namespace TEXT NOT NULL,\n parent_id TEXT NOT NULL,\n child_id TEXT NOT NULL,\n child_queue TEXT NOT NULL,\n mode TEXT NOT NULL CHECK (mode IN ('fail', 'remove', 'ignore', 'continue')),\n error TEXT NOT NULL,\n created_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, parent_id, child_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_flow_failures_parent_idx\n ON bunqueue_flow_failures(namespace, parent_id, mode);\n\nCREATE TABLE IF NOT EXISTS bunqueue_repeat_links (\n namespace TEXT NOT NULL,\n original_id TEXT NOT NULL,\n successor_id TEXT NOT NULL,\n PRIMARY KEY (namespace, original_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_repeat_links_successor_idx\n ON bunqueue_repeat_links(namespace, successor_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_job_logs (\n id BIGSERIAL PRIMARY KEY,\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n timestamp BIGINT NOT NULL,\n level TEXT NOT NULL CHECK (level IN ('info', 'warn', 'error')),\n message TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS bunqueue_job_logs_job_idx\n ON bunqueue_job_logs(namespace, job_id, id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_crons (\n namespace TEXT NOT NULL,\n name TEXT NOT NULL,\n payload BYTEA NOT NULL,\n next_run BIGINT NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n updated_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, name)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_crons_due_idx\n ON bunqueue_crons(namespace, next_run, name);\n\nCREATE TABLE IF NOT EXISTS bunqueue_workers (\n namespace TEXT NOT NULL,\n id TEXT NOT NULL,\n broker_id TEXT NOT NULL,\n broker_session_id TEXT,\n client_id TEXT,\n queues TEXT[] NOT NULL,\n payload BYTEA NOT NULL,\n last_seen BIGINT NOT NULL,\n PRIMARY KEY (namespace, id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_workers_queue_idx\n ON bunqueue_workers USING GIN(queues);\nCREATE INDEX IF NOT EXISTS bunqueue_workers_broker_idx\n ON bunqueue_workers(namespace, broker_id, client_id);\nALTER TABLE bunqueue_workers\n ADD COLUMN IF NOT EXISTS broker_session_id TEXT;\nCREATE INDEX IF NOT EXISTS bunqueue_workers_broker_session_idx\n ON bunqueue_workers(namespace, broker_id, broker_session_id, client_id);\n\nCREATE TABLE IF NOT EXISTS bunqueue_metric_buckets (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n metric_type TEXT NOT NULL CHECK (metric_type IN ('completed', 'failed')),\n minute BIGINT NOT NULL,\n count INTEGER NOT NULL,\n PRIMARY KEY (namespace, queue, metric_type, minute)\n);\nCREATE TABLE IF NOT EXISTS bunqueue_metric_totals (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n metric_type TEXT NOT NULL CHECK (metric_type IN ('completed', 'failed')),\n total_count BIGINT NOT NULL,\n prev_ts BIGINT NOT NULL,\n prev_count INTEGER NOT NULL,\n PRIMARY KEY (namespace, queue, metric_type)\n);\n\nCREATE TABLE IF NOT EXISTS bunqueue_completions (\n namespace TEXT NOT NULL,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n completed_at BIGINT NOT NULL,\n result BYTEA,\n PRIMARY KEY (namespace, job_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_completions_queue_idx\n ON bunqueue_completions(namespace, queue, job_id);\nCREATE INDEX IF NOT EXISTS bunqueue_completions_recent_idx\n ON bunqueue_completions(namespace, completed_at DESC, job_id DESC);\n\nCREATE TABLE IF NOT EXISTS bunqueue_queue_state (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n paused BOOLEAN NOT NULL DEFAULT FALSE,\n rate_limit INTEGER,\n rate_duration_ms BIGINT,\n rate_window_started_at BIGINT,\n rate_expires_at BIGINT,\n rate_count INTEGER NOT NULL DEFAULT 0,\n concurrency_limit INTEGER,\n stall_config BYTEA,\n dlq_config BYTEA,\n group_sequence BIGINT NOT NULL DEFAULT 0,\n PRIMARY KEY (namespace, queue)\n);\n\nALTER TABLE bunqueue_queue_state\n ADD COLUMN IF NOT EXISTS rate_expires_at BIGINT;\nALTER TABLE bunqueue_queue_state\n ADD COLUMN IF NOT EXISTS group_sequence BIGINT NOT NULL DEFAULT 0;\n\n\nCREATE SEQUENCE IF NOT EXISTS bunqueue_group_order_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_group_order_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nALTER TABLE bunqueue_jobs ADD COLUMN IF NOT EXISTS group_order BIGINT;\nALTER TABLE bunqueue_jobs\n ALTER COLUMN group_order TYPE BIGINT USING group_order::bigint,\n ALTER COLUMN group_order DROP DEFAULT,\n ALTER COLUMN group_order DROP NOT NULL;\n\nDO $group_order_backfill$\nDECLARE\n target RECORD;\nBEGIN\n PERFORM setval(\n 'bunqueue_group_order_seq',\n GREATEST(\n (SELECT last_value FROM bunqueue_group_order_seq),\n COALESCE((SELECT MAX(group_order) FROM bunqueue_jobs), 0),\n 1\n ),\n TRUE\n );\n FOR target IN\n SELECT namespace, id\n FROM bunqueue_jobs\n WHERE group_id IS NOT NULL AND group_order IS NULL\n ORDER BY created_at, id, namespace\n LOOP\n UPDATE bunqueue_jobs\n SET group_order = nextval('bunqueue_group_order_seq')\n WHERE namespace = target.namespace AND id = target.id;\n END LOOP;\nEND;\n$group_order_backfill$;\n\nCREATE TABLE IF NOT EXISTS bunqueue_group_state (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n group_id TEXT NOT NULL,\n rate_limit BIGINT,\n rate_duration_ms BIGINT,\n rate_window_started_at BIGINT,\n rate_count BIGINT NOT NULL DEFAULT 0,\n rate_effective_max BIGINT,\n rate_effective_duration_ms BIGINT,\n concurrency_limit BIGINT,\n last_served BIGINT,\n PRIMARY KEY (namespace, queue, group_id)\n);\nALTER TABLE bunqueue_group_state\n ADD COLUMN IF NOT EXISTS namespace TEXT,\n ADD COLUMN IF NOT EXISTS queue TEXT,\n ADD COLUMN IF NOT EXISTS group_id TEXT,\n ADD COLUMN IF NOT EXISTS rate_limit BIGINT,\n ADD COLUMN IF NOT EXISTS rate_duration_ms BIGINT,\n ADD COLUMN IF NOT EXISTS rate_window_started_at BIGINT,\n ADD COLUMN IF NOT EXISTS rate_count BIGINT DEFAULT 0,\n ADD COLUMN IF NOT EXISTS rate_effective_max BIGINT,\n ADD COLUMN IF NOT EXISTS rate_effective_duration_ms BIGINT,\n ADD COLUMN IF NOT EXISTS concurrency_limit BIGINT,\n ADD COLUMN IF NOT EXISTS last_served BIGINT;\nALTER TABLE bunqueue_group_state\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN queue TYPE TEXT USING queue::text,\n ALTER COLUMN group_id TYPE TEXT USING group_id::text,\n ALTER COLUMN rate_limit TYPE BIGINT USING rate_limit::bigint,\n ALTER COLUMN rate_duration_ms TYPE BIGINT USING rate_duration_ms::bigint,\n ALTER COLUMN rate_window_started_at TYPE BIGINT USING rate_window_started_at::bigint,\n ALTER COLUMN rate_count TYPE BIGINT USING rate_count::bigint,\n ALTER COLUMN rate_effective_max TYPE BIGINT USING rate_effective_max::bigint,\n ALTER COLUMN rate_effective_duration_ms TYPE BIGINT USING rate_effective_duration_ms::bigint,\n ALTER COLUMN concurrency_limit TYPE BIGINT USING concurrency_limit::bigint,\n ALTER COLUMN last_served TYPE BIGINT USING last_served::bigint;\nUPDATE bunqueue_group_state SET rate_count = 0 WHERE rate_count IS NULL;\nALTER TABLE bunqueue_group_state\n ALTER COLUMN namespace DROP DEFAULT,\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN queue DROP DEFAULT,\n ALTER COLUMN queue SET NOT NULL,\n ALTER COLUMN group_id DROP DEFAULT,\n ALTER COLUMN group_id SET NOT NULL,\n ALTER COLUMN rate_limit DROP DEFAULT,\n ALTER COLUMN rate_limit DROP NOT NULL,\n ALTER COLUMN rate_duration_ms DROP DEFAULT,\n ALTER COLUMN rate_duration_ms DROP NOT NULL,\n ALTER COLUMN rate_window_started_at DROP DEFAULT,\n ALTER COLUMN rate_window_started_at DROP NOT NULL,\n ALTER COLUMN rate_count SET DEFAULT 0,\n ALTER COLUMN rate_count SET NOT NULL,\n ALTER COLUMN rate_effective_max DROP DEFAULT,\n ALTER COLUMN rate_effective_max DROP NOT NULL,\n ALTER COLUMN rate_effective_duration_ms DROP DEFAULT,\n ALTER COLUMN rate_effective_duration_ms DROP NOT NULL,\n ALTER COLUMN concurrency_limit DROP DEFAULT,\n ALTER COLUMN concurrency_limit DROP NOT NULL,\n ALTER COLUMN last_served DROP DEFAULT,\n ALTER COLUMN last_served DROP NOT NULL;\n\nDO $group_state_primary_key$\nDECLARE\n constraint_name TEXT;\nBEGIN\n IF NOT EXISTS (\n SELECT 1\n FROM pg_constraint AS constraint_state\n JOIN pg_index AS index_state ON index_state.indexrelid = constraint_state.conindid\n JOIN pg_class AS index_class ON index_class.oid = index_state.indexrelid\n JOIN pg_am AS access_method ON access_method.oid = index_class.relam\n WHERE constraint_state.conrelid = to_regclass('bunqueue_group_state')\n AND constraint_state.contype = 'p'\n AND constraint_state.convalidated\n AND NOT constraint_state.condeferrable\n AND NOT constraint_state.condeferred\n AND index_state.indisprimary AND index_state.indisunique\n AND index_state.indisvalid AND index_state.indisready\n AND access_method.amname = 'btree'\n AND ARRAY(\n SELECT attribute.attname::text\n FROM unnest(constraint_state.conkey) WITH ORDINALITY AS key(attnum, position)\n JOIN pg_attribute AS attribute\n ON attribute.attrelid = constraint_state.conrelid\n AND attribute.attnum = key.attnum\n ORDER BY key.position\n ) = ARRAY['namespace', 'queue', 'group_id']\n ) THEN\n FOR constraint_name IN\n SELECT conname FROM pg_constraint\n WHERE conrelid = to_regclass('bunqueue_group_state')\n AND (contype = 'p' OR conname = 'bunqueue_group_state_pkey')\n LOOP\n EXECUTE format(\n 'ALTER TABLE bunqueue_group_state DROP CONSTRAINT %I',\n constraint_name\n );\n END LOOP;\n IF to_regclass('bunqueue_group_state_pkey') IS NOT NULL THEN\n DROP INDEX bunqueue_group_state_pkey;\n END IF;\n ALTER TABLE bunqueue_group_state\n ADD CONSTRAINT bunqueue_group_state_pkey PRIMARY KEY (namespace, queue, group_id);\n END IF;\nEND;\n$group_state_primary_key$;\n\nDROP INDEX IF EXISTS bunqueue_jobs_group_ready_idx;\nCREATE INDEX bunqueue_jobs_group_ready_idx\n ON bunqueue_jobs(namespace, queue, group_id, run_at ASC, group_order ASC, id ASC)\n WHERE group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');\nDROP INDEX IF EXISTS bunqueue_group_state_rotation_idx;\nCREATE INDEX bunqueue_group_state_rotation_idx\n ON bunqueue_group_state(namespace, queue, last_served, group_id);\n\n\nINSERT INTO bunqueue_queue_state (namespace, queue)\nSELECT DISTINCT namespace, queue FROM bunqueue_jobs\nON CONFLICT (namespace, queue) DO NOTHING;\n\nCREATE TABLE IF NOT EXISTS bunqueue_events (\n id BIGSERIAL PRIMARY KEY,\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n event_type TEXT NOT NULL,\n job_id TEXT NOT NULL,\n occurred_at BIGINT NOT NULL,\n payload BYTEA\n);\nCREATE INDEX IF NOT EXISTS bunqueue_events_namespace_id_idx\n ON bunqueue_events(namespace, id);\nCREATE INDEX IF NOT EXISTS bunqueue_events_queue_id_idx\n ON bunqueue_events(namespace, queue, id DESC);\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_prune_watermarks (\n namespace TEXT NOT NULL,\n queue TEXT NOT NULL,\n source_event_id BIGINT NOT NULL,\n pruned_through BIGINT NOT NULL,\n PRIMARY KEY (namespace, queue, source_event_id)\n);\nCREATE INDEX IF NOT EXISTS bunqueue_event_prune_watermarks_namespace_queue_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, pruned_through DESC);\n\n\nCREATE SEQUENCE IF NOT EXISTS bunqueue_event_commit_seq AS BIGINT CACHE 1;\nALTER SEQUENCE bunqueue_event_commit_seq\n AS BIGINT INCREMENT BY 1 NO MINVALUE NO MAXVALUE NO CYCLE CACHE 1;\n\nCREATE TABLE IF NOT EXISTS bunqueue_event_commits (\n namespace TEXT NOT NULL,\n transaction_id BIGINT NOT NULL,\n commit_seq BIGINT,\n PRIMARY KEY (namespace, transaction_id)\n);\n\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS namespace TEXT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_commits\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace TYPE TEXT USING namespace::text,\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint;\n\nALTER TABLE bunqueue_events\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id DROP DEFAULT;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint;\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\n\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS transaction_id BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS self_pruned_commit_seq BIGINT;\nALTER TABLE bunqueue_event_prune_watermarks\n ADD COLUMN IF NOT EXISTS prunes_current_transaction BOOLEAN NOT NULL DEFAULT FALSE;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id DROP DEFAULT,\n ALTER COLUMN pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN self_pruned_commit_seq DROP DEFAULT,\n ALTER COLUMN prunes_current_transaction DROP DEFAULT;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id TYPE BIGINT USING transaction_id::bigint,\n ALTER COLUMN commit_seq TYPE BIGINT USING commit_seq::bigint,\n ALTER COLUMN pruned_commit_seq TYPE BIGINT USING pruned_commit_seq::bigint,\n ALTER COLUMN self_pruned_commit_seq TYPE BIGINT USING self_pruned_commit_seq::bigint,\n ALTER COLUMN prunes_current_transaction TYPE BOOLEAN\n USING prunes_current_transaction::boolean;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET DEFAULT (pg_current_xact_id()::text::bigint);\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN self_pruned_commit_seq SET DEFAULT 0,\n ALTER COLUMN prunes_current_transaction SET DEFAULT FALSE;\n\nUPDATE bunqueue_events\nSET transaction_id = -id\nWHERE transaction_id IS NULL;\nUPDATE bunqueue_event_prune_watermarks\nSET transaction_id = COALESCE(transaction_id, -source_event_id),\n commit_seq = COALESCE(commit_seq, source_event_id),\n pruned_commit_seq = COALESCE(pruned_commit_seq, commit_seq, source_event_id),\n self_pruned_commit_seq = COALESCE(self_pruned_commit_seq, 0)\nWHERE transaction_id IS NULL OR commit_seq IS NULL OR pruned_commit_seq IS NULL\n OR self_pruned_commit_seq IS NULL;\n\nALTER TABLE bunqueue_events\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_commits\n ALTER COLUMN namespace SET NOT NULL,\n ALTER COLUMN transaction_id SET NOT NULL;\nALTER TABLE bunqueue_event_prune_watermarks\n ALTER COLUMN transaction_id SET NOT NULL,\n ALTER COLUMN pruned_commit_seq SET NOT NULL,\n ALTER COLUMN self_pruned_commit_seq SET NOT NULL,\n ALTER COLUMN prunes_current_transaction SET NOT NULL;\n\nDO $migration$\nBEGIN\n IF EXISTS (\n SELECT 1 FROM pg_attribute\n WHERE attrelid = to_regclass('bunqueue_events')\n AND attname = 'commit_seq' AND NOT attisdropped\n ) THEN\n EXECUTE $sql$\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(commit_seq)\n FROM bunqueue_events\n WHERE commit_seq IS NOT NULL\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n )\n $sql$;\n ELSE\n INSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\n SELECT namespace, transaction_id, MAX(id)\n FROM bunqueue_events\n GROUP BY namespace, transaction_id\n ON CONFLICT (namespace, transaction_id) DO NOTHING;\n END IF;\nEND;\n$migration$;\n\nINSERT INTO bunqueue_event_commits (namespace, transaction_id, commit_seq)\nSELECT namespace, transaction_id, MAX(commit_seq)\nFROM bunqueue_event_prune_watermarks\nWHERE commit_seq IS NOT NULL\nGROUP BY namespace, transaction_id\nON CONFLICT (namespace, transaction_id) DO UPDATE SET\n commit_seq = GREATEST(\n COALESCE(bunqueue_event_commits.commit_seq, 0),\n excluded.commit_seq\n );\n\nDO $block$\nDECLARE\n journal_floor BIGINT := 0;\n legacy_head_floor BIGINT := 0;\n sequence_floor BIGINT := 0;\nBEGIN\n SELECT COALESCE(MAX(commit_seq), 0) INTO journal_floor\n FROM bunqueue_event_commits;\n SELECT last_value INTO sequence_floor FROM bunqueue_event_commit_seq;\n IF to_regclass('bunqueue_event_commit_heads') IS NOT NULL THEN\n EXECUTE 'SELECT COALESCE(MAX(commit_seq), 0) FROM bunqueue_event_commit_heads'\n INTO legacy_head_floor;\n END IF;\n PERFORM setval(\n 'bunqueue_event_commit_seq',\n GREATEST(journal_floor, legacy_head_floor, sequence_floor, 1),\n TRUE\n );\nEND;\n$block$;\n\nDROP TABLE IF EXISTS bunqueue_event_commit_heads;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nDROP TABLE IF EXISTS bunqueue_event_commit_assignments CASCADE;\nDROP INDEX IF EXISTS bunqueue_events_commit_idx;\nDROP INDEX IF EXISTS bunqueue_events_pending_commit_idx;\nALTER TABLE bunqueue_events DROP COLUMN IF EXISTS commit_seq;\n\nDROP INDEX IF EXISTS bunqueue_events_transaction_idx;\nDROP INDEX IF EXISTS bunqueue_event_commits_replay_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_pending_commit_idx;\nDROP INDEX IF EXISTS bunqueue_event_prune_watermarks_transaction_idx;\n\nCREATE INDEX bunqueue_events_transaction_idx\n ON bunqueue_events(namespace, transaction_id, id);\nCREATE INDEX bunqueue_event_commits_replay_idx\n ON bunqueue_event_commits(namespace, commit_seq, transaction_id)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, queue, commit_seq DESC)\n WHERE commit_seq IS NOT NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_pending_commit_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id)\n WHERE commit_seq IS NULL;\nCREATE INDEX bunqueue_event_prune_watermarks_transaction_idx\n ON bunqueue_event_prune_watermarks(namespace, transaction_id);\n\nCREATE OR REPLACE FUNCTION bunqueue_register_event_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_event_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_register_watermark_rows()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nBEGIN\n INSERT INTO bunqueue_event_commits (namespace, transaction_id)\n SELECT DISTINCT namespace, transaction_id FROM new_watermark_rows\n ORDER BY namespace, transaction_id\n ON CONFLICT DO NOTHING;\n RETURN NULL;\nEND;\n$function$;\n\nCREATE OR REPLACE FUNCTION bunqueue_assign_event_commit()\nRETURNS TRIGGER LANGUAGE plpgsql AS $function$\nDECLARE\n assigned_commit_seq BIGINT;\nBEGIN\n PERFORM pg_advisory_xact_lock(hashtextextended(\n 'bunqueue:event-commit:' || length(NEW.namespace)::text || ':' || NEW.namespace,\n 0\n ));\n assigned_commit_seq := nextval('bunqueue_event_commit_seq');\n\n UPDATE bunqueue_event_commits\n SET commit_seq = assigned_commit_seq\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n UPDATE bunqueue_event_prune_watermarks\n SET commit_seq = assigned_commit_seq,\n pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE pruned_commit_seq\n END,\n self_pruned_commit_seq = CASE\n WHEN prunes_current_transaction THEN GREATEST(\n COALESCE(self_pruned_commit_seq, 0), assigned_commit_seq\n )\n ELSE self_pruned_commit_seq\n END,\n prunes_current_transaction = FALSE\n WHERE namespace = NEW.namespace\n AND transaction_id = NEW.transaction_id\n AND commit_seq IS NULL;\n RETURN NULL;\nEND;\n$function$;\n\nDROP TRIGGER IF EXISTS bunqueue_events_register_commit ON bunqueue_events;\nCREATE TRIGGER bunqueue_events_register_commit\nAFTER INSERT ON bunqueue_events\nREFERENCING NEW TABLE AS new_event_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_event_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_insert_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_insert_register_commit\nAFTER INSERT ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_watermarks_update_register_commit\n ON bunqueue_event_prune_watermarks;\nCREATE TRIGGER bunqueue_watermarks_update_register_commit\nAFTER UPDATE ON bunqueue_event_prune_watermarks\nREFERENCING NEW TABLE AS new_watermark_rows\nFOR EACH STATEMENT EXECUTE FUNCTION bunqueue_register_watermark_rows();\n\nDROP TRIGGER IF EXISTS bunqueue_assign_event_commit\n ON bunqueue_event_commits;\nCREATE CONSTRAINT TRIGGER bunqueue_assign_event_commit\nAFTER INSERT ON bunqueue_event_commits\nDEFERRABLE INITIALLY DEFERRED\nFOR EACH ROW EXECUTE FUNCTION bunqueue_assign_event_commit();\n\n\nCREATE TABLE IF NOT EXISTS bunqueue_brokers (\n namespace TEXT NOT NULL,\n broker_id TEXT NOT NULL,\n session_id TEXT,\n started_at BIGINT NOT NULL,\n heartbeat_at BIGINT NOT NULL,\n PRIMARY KEY (namespace, broker_id)\n);\nALTER TABLE bunqueue_brokers\n ADD COLUMN IF NOT EXISTS session_id TEXT;\n\nDROP INDEX IF EXISTS bunqueue_jobs_live_unique_key_idx;\nCREATE UNIQUE INDEX bunqueue_jobs_live_unique_key_idx\n ON bunqueue_jobs(namespace, queue, unique_key)\n WHERE unique_key IS NOT NULL\n AND state IN ('waiting', 'prioritized', 'delayed', 'waiting-children', 'active');\n";
4
4
  export declare const POSTGRES_EVENT_CHANNEL = "bunqueue_jobs_changed";
@@ -1,5 +1,6 @@
1
1
  import { POSTGRES_EVENT_JOURNAL_SCHEMA } from './eventJournalSchema';
2
- export const POSTGRES_SCHEMA_VERSION = 17;
2
+ import { POSTGRES_GROUP_SCHEMA } from './groupSchema';
3
+ export const POSTGRES_SCHEMA_VERSION = 19;
3
4
  /** PostgreSQL schema for the database-authoritative distributed queue engine. */
4
5
  export const POSTGRES_SCHEMA = `
5
6
  CREATE TABLE IF NOT EXISTS bunqueue_schema_migrations (
@@ -30,6 +31,7 @@ CREATE TABLE IF NOT EXISTS bunqueue_jobs (
30
31
  unique_expires_at BIGINT,
31
32
  custom_id TEXT,
32
33
  group_id TEXT,
34
+ group_order BIGINT,
33
35
  parent_id TEXT,
34
36
  lease_owner TEXT,
35
37
  lease_broker_id TEXT,
@@ -57,9 +59,6 @@ CREATE INDEX IF NOT EXISTS bunqueue_jobs_lease_idx
57
59
  CREATE INDEX IF NOT EXISTS bunqueue_jobs_parent_idx
58
60
  ON bunqueue_jobs(namespace, parent_id)
59
61
  WHERE parent_id IS NOT NULL;
60
- CREATE INDEX IF NOT EXISTS bunqueue_jobs_group_ready_idx
61
- ON bunqueue_jobs(namespace, queue, group_id, priority DESC, run_at ASC, id ASC)
62
- WHERE group_id IS NOT NULL AND state IN ('waiting', 'prioritized', 'delayed');
63
62
  CREATE INDEX IF NOT EXISTS bunqueue_jobs_group_active_idx
64
63
  ON bunqueue_jobs(namespace, queue, group_id, lease_until)
65
64
  WHERE group_id IS NOT NULL AND state = 'active';
@@ -201,11 +200,16 @@ CREATE TABLE IF NOT EXISTS bunqueue_queue_state (
201
200
  concurrency_limit INTEGER,
202
201
  stall_config BYTEA,
203
202
  dlq_config BYTEA,
203
+ group_sequence BIGINT NOT NULL DEFAULT 0,
204
204
  PRIMARY KEY (namespace, queue)
205
205
  );
206
206
 
207
207
  ALTER TABLE bunqueue_queue_state
208
208
  ADD COLUMN IF NOT EXISTS rate_expires_at BIGINT;
209
+ ALTER TABLE bunqueue_queue_state
210
+ ADD COLUMN IF NOT EXISTS group_sequence BIGINT NOT NULL DEFAULT 0;
211
+
212
+ ${POSTGRES_GROUP_SCHEMA}
209
213
 
210
214
  INSERT INTO bunqueue_queue_state (namespace, queue)
211
215
  SELECT DISTINCT namespace, queue FROM bunqueue_jobs
@@ -2,6 +2,7 @@ import { postgresAdvisoryLockName } from './advisoryLocks';
2
2
  import { databaseNow } from './context';
3
3
  import { POSTGRES_ASSIGN_EVENT_COMMIT_BODY, POSTGRES_REGISTER_EVENT_ROWS_BODY, POSTGRES_REGISTER_WATERMARK_ROWS_BODY, } from './eventJournalSchema';
4
4
  import { POSTGRES_SCHEMA, POSTGRES_SCHEMA_VERSION } from './schema';
5
+ import { hasCurrentPostgresGroupSchema } from './groupSchemaFingerprint';
5
6
  const EXPECTED_FUNCTION_BODIES = new Map([
6
7
  ['bunqueue_register_event_rows', POSTGRES_REGISTER_EVENT_ROWS_BODY],
7
8
  ['bunqueue_register_watermark_rows', POSTGRES_REGISTER_WATERMARK_ROWS_BODY],
@@ -41,10 +42,12 @@ async function hasCurrentSchema(tx) {
41
42
  ('bunqueue_event_prune_watermarks', 'transaction_id', 'bigint', TRUE, 'transaction'),
42
43
  ('bunqueue_event_prune_watermarks', 'commit_seq', 'bigint', FALSE, 'none'),
43
44
  ('bunqueue_event_prune_watermarks', 'pruned_commit_seq', 'bigint', TRUE, 'zero'),
45
+ ('bunqueue_event_prune_watermarks', 'self_pruned_commit_seq', 'bigint', TRUE, 'zero'),
44
46
  ('bunqueue_event_prune_watermarks', 'prunes_current_transaction', 'boolean', TRUE, 'false'),
45
47
  ('bunqueue_brokers', 'session_id', 'text', FALSE, 'none'),
46
48
  ('bunqueue_jobs', 'lease_broker_session_id', 'text', FALSE, 'none'),
47
- ('bunqueue_workers', 'broker_session_id', 'text', FALSE, 'none')
49
+ ('bunqueue_workers', 'broker_session_id', 'text', FALSE, 'none'),
50
+ ('bunqueue_queue_state', 'group_sequence', 'bigint', TRUE, 'zero')
48
51
  ), expected_indexes(
49
52
  index_name, table_name, columns, descending, predicate, is_unique
50
53
  ) AS (
@@ -179,6 +182,8 @@ async function hasCurrentSchema(tx) {
179
182
  `;
180
183
  if (!objects.valid)
181
184
  return false;
185
+ if (!(await hasCurrentPostgresGroupSchema(tx)))
186
+ return false;
182
187
  const functions = await tx `
183
188
  SELECT expected.name, functions.prosrc AS body,
184
189
  functions.prorettype = to_regtype('trigger')
@@ -3,6 +3,7 @@ import { setPostgresDlqConfig, setPostgresStallConfig } from './control';
3
3
  import { type ListPostgresJobsOptions } from './queries';
4
4
  import { PostgresAdmissionStore } from './admissionStore';
5
5
  import type { CronJobInput } from '../../../domain/types/cron';
6
+ import { type GroupPullOptions } from '../../../domain/types/group';
6
7
  import type { Worker } from '../../../domain/types/worker';
7
8
  import type { QueueMetricType } from '../../../domain/types/metrics';
8
9
  import type { ClaimedPostgresJob, PostgresCompletionInput, PostgresFailureInput } from './types';
@@ -10,8 +11,17 @@ import { finalizePostgresLifetimeMetrics } from './lifetimeMetricsFinalizer';
10
11
  import { loadPostgresJobProjections } from './readModels';
11
12
  /** Database-authoritative PostgreSQL queue store safe for concurrent brokers. */
12
13
  export declare class PostgresQueueStore extends PostgresAdmissionStore {
14
+ getGroupJobsCount: (queue: string, groupId?: string) => Promise<number>;
15
+ getGroupActiveCount: (queue: string, groupId: string) => Promise<number>;
16
+ setGroupRateLimit: (queue: string, groupId: string, max: number, duration: number) => Promise<void>;
17
+ getGroupRateLimit: (queue: string, groupId: string) => Promise<import("../../../domain/types/group").GroupRateLimitOverride | null>;
18
+ removeGroupRateLimit: (queue: string, groupId: string) => Promise<number>;
19
+ getGroupRateLimitTtl: (queue: string, groupId: string, maxJobs?: number) => Promise<number>;
20
+ setGroupConcurrency: (queue: string, groupId: string, concurrency: number) => Promise<void>;
21
+ getGroupConcurrency: (queue: string, groupId: string) => Promise<number | null>;
22
+ removeGroupConcurrency: (queue: string, groupId: string) => Promise<number>;
13
23
  now(): Promise<number>;
14
- claim(queue: string, count: number, owner?: string, leaseDurationMs?: number): Promise<ClaimedPostgresJob[]>;
24
+ claim(queue: string, count: number, owner?: string, leaseDurationMs?: number, groupOptions?: GroupPullOptions): Promise<ClaimedPostgresJob[]>;
15
25
  complete(id: JobId, token: string, result?: unknown, remove?: boolean): Promise<import("./types").PostgresTransitionResult>;
16
26
  completeMany(items: readonly PostgresCompletionInput[]): Promise<import("./types").PostgresBatchCompletionResult[]>;
17
27
  fail(input: PostgresFailureInput): Promise<import("./types").PostgresTransitionResult>;