@zintrust/core 0.1.41 → 0.1.42

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 (193) hide show
  1. package/package.json +17 -1
  2. package/src/boot/bootstrap.js +27 -11
  3. package/src/boot/registry/runtime.d.ts.map +1 -1
  4. package/src/boot/registry/runtime.js +11 -0
  5. package/src/cli/CLI.d.ts.map +1 -1
  6. package/src/cli/CLI.js +12 -0
  7. package/src/cli/commands/ConfigCommand.d.ts.map +1 -1
  8. package/src/cli/commands/ConfigCommand.js +3 -5
  9. package/src/cli/commands/D1LearnCommand.d.ts +9 -0
  10. package/src/cli/commands/D1LearnCommand.d.ts.map +1 -0
  11. package/src/cli/commands/D1LearnCommand.js +143 -0
  12. package/src/cli/commands/D1MigrateCommand.d.ts.map +1 -1
  13. package/src/cli/commands/D1MigrateCommand.js +55 -16
  14. package/src/cli/commands/InitContainerCommand.d.ts.map +1 -1
  15. package/src/cli/commands/InitContainerCommand.js +21 -6
  16. package/src/cli/commands/InitEcosystemCommand.d.ts +6 -0
  17. package/src/cli/commands/InitEcosystemCommand.d.ts.map +1 -0
  18. package/src/cli/commands/InitEcosystemCommand.js +51 -0
  19. package/src/cli/commands/MigrateCommand.d.ts.map +1 -1
  20. package/src/cli/commands/MigrateCommand.js +78 -36
  21. package/src/cli/commands/MigrateWorkerCommand.d.ts.map +1 -1
  22. package/src/cli/commands/MigrateWorkerCommand.js +36 -2
  23. package/src/cli/commands/PutCommand.d.ts +6 -0
  24. package/src/cli/commands/PutCommand.d.ts.map +1 -0
  25. package/src/cli/commands/PutCommand.js +173 -0
  26. package/src/cli/commands/QueueRecoveryCommand.d.ts.map +1 -1
  27. package/src/cli/commands/QueueRecoveryCommand.js +113 -14
  28. package/src/cli/commands/ScheduleListCommand.d.ts +6 -0
  29. package/src/cli/commands/ScheduleListCommand.d.ts.map +1 -0
  30. package/src/cli/commands/ScheduleListCommand.js +62 -0
  31. package/src/cli/commands/ScheduleRunCommand.d.ts +6 -0
  32. package/src/cli/commands/ScheduleRunCommand.d.ts.map +1 -0
  33. package/src/cli/commands/ScheduleRunCommand.js +32 -0
  34. package/src/cli/commands/ScheduleStartCommand.d.ts +6 -0
  35. package/src/cli/commands/ScheduleStartCommand.d.ts.map +1 -0
  36. package/src/cli/commands/ScheduleStartCommand.js +40 -0
  37. package/src/cli/commands/SecretsCommand.d.ts.map +1 -1
  38. package/src/cli/commands/SecretsCommand.js +2 -2
  39. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts +6 -0
  40. package/src/cli/commands/schedule/ScheduleCliSupport.d.ts.map +1 -0
  41. package/src/cli/commands/schedule/ScheduleCliSupport.js +55 -0
  42. package/src/cli/config/ConfigManager.d.ts.map +1 -1
  43. package/src/cli/config/ConfigManager.js +8 -1
  44. package/src/cli/d1/D1SqlMigrations.d.ts.map +1 -1
  45. package/src/cli/d1/D1SqlMigrations.js +11 -1
  46. package/src/cli/d1/WranglerConfig.d.ts.map +1 -1
  47. package/src/cli/d1/WranglerConfig.js +34 -2
  48. package/src/cli/services/VersionChecker.d.ts.map +1 -1
  49. package/src/cli/services/VersionChecker.js +5 -1
  50. package/src/cli/utils/DatabaseCliUtils.d.ts.map +1 -1
  51. package/src/cli/utils/DatabaseCliUtils.js +6 -1
  52. package/src/cli/utils/EnvFileLoader.d.ts.map +1 -1
  53. package/src/cli/utils/EnvFileLoader.js +33 -14
  54. package/src/cli.d.ts +5 -0
  55. package/src/cli.d.ts.map +1 -0
  56. package/src/cli.js +4 -0
  57. package/src/collections/index.d.ts +2 -2
  58. package/src/collections/index.d.ts.map +1 -1
  59. package/src/collections/index.js +1 -1
  60. package/src/common/RemoteSignedJson.d.ts.map +1 -1
  61. package/src/common/RemoteSignedJson.js +49 -23
  62. package/src/common/utility.d.ts.map +1 -1
  63. package/src/common/utility.js +2 -6
  64. package/src/config/cloudflare.d.ts.map +1 -1
  65. package/src/config/cloudflare.js +19 -8
  66. package/src/config/env.js +2 -2
  67. package/src/helper/index.d.ts +225 -0
  68. package/src/helper/index.d.ts.map +1 -0
  69. package/src/helper/index.js +347 -0
  70. package/src/index.d.ts +3 -6
  71. package/src/index.d.ts.map +1 -1
  72. package/src/index.js +7 -9
  73. package/src/migrations/MigrationDiscovery.d.ts.map +1 -1
  74. package/src/migrations/MigrationDiscovery.js +2 -1
  75. package/src/orm/DatabaseAdapter.d.ts +1 -0
  76. package/src/orm/DatabaseAdapter.d.ts.map +1 -1
  77. package/src/orm/SchemaStatemenWriter.d.ts +15 -0
  78. package/src/orm/SchemaStatemenWriter.d.ts.map +1 -0
  79. package/src/orm/SchemaStatemenWriter.js +78 -0
  80. package/src/orm/adapters/D1Adapter.d.ts.map +1 -1
  81. package/src/orm/adapters/D1Adapter.js +52 -2
  82. package/src/orm/adapters/D1RemoteAdapter.d.ts.map +1 -1
  83. package/src/orm/adapters/D1RemoteAdapter.js +137 -89
  84. package/src/orm/adapters/MySQLProxyAdapter.d.ts.map +1 -1
  85. package/src/orm/adapters/MySQLProxyAdapter.js +100 -81
  86. package/src/orm/adapters/PostgreSQLProxyAdapter.d.ts.map +1 -1
  87. package/src/orm/adapters/PostgreSQLProxyAdapter.js +26 -10
  88. package/src/orm/adapters/SqlProxyAdapterUtils.d.ts.map +1 -1
  89. package/src/orm/adapters/SqlProxyAdapterUtils.js +2 -1
  90. package/src/orm/adapters/SqlProxyRegistryMode.d.ts +12 -0
  91. package/src/orm/adapters/SqlProxyRegistryMode.d.ts.map +1 -0
  92. package/src/orm/adapters/SqlProxyRegistryMode.js +24 -0
  93. package/src/orm/adapters/SqlServerProxyAdapter.d.ts +3 -0
  94. package/src/orm/adapters/SqlServerProxyAdapter.d.ts.map +1 -1
  95. package/src/orm/adapters/SqlServerProxyAdapter.js +125 -117
  96. package/src/orm/migrations/MigrationStore.js +1 -1
  97. package/src/proxy/ProxyRequestParsing.d.ts +9 -0
  98. package/src/proxy/ProxyRequestParsing.d.ts.map +1 -0
  99. package/src/proxy/ProxyRequestParsing.js +16 -0
  100. package/src/proxy/RequestValidator.d.ts.map +1 -1
  101. package/src/proxy/RequestValidator.js +2 -1
  102. package/src/proxy/SigningService.js +2 -2
  103. package/src/proxy/SqlProxyDbOverrides.d.ts +17 -0
  104. package/src/proxy/SqlProxyDbOverrides.d.ts.map +1 -0
  105. package/src/proxy/SqlProxyDbOverrides.js +1 -0
  106. package/src/proxy/SqlProxyServerDeps.d.ts +12 -0
  107. package/src/proxy/SqlProxyServerDeps.d.ts.map +1 -0
  108. package/src/proxy/SqlProxyServerDeps.js +9 -0
  109. package/src/proxy/StatementPayloadValidator.d.ts +13 -0
  110. package/src/proxy/StatementPayloadValidator.d.ts.map +1 -0
  111. package/src/proxy/StatementPayloadValidator.js +18 -0
  112. package/src/proxy/StatementRegistryLoader.d.ts +2 -0
  113. package/src/proxy/StatementRegistryLoader.d.ts.map +1 -0
  114. package/src/proxy/StatementRegistryLoader.js +36 -0
  115. package/src/proxy/StatementRegistryResolver.d.ts +15 -0
  116. package/src/proxy/StatementRegistryResolver.d.ts.map +1 -0
  117. package/src/proxy/StatementRegistryResolver.js +34 -0
  118. package/src/proxy/d1/ZintrustD1Proxy.d.ts +2 -1
  119. package/src/proxy/d1/ZintrustD1Proxy.d.ts.map +1 -1
  120. package/src/proxy/d1/ZintrustD1Proxy.js +2 -1
  121. package/src/proxy/isMutatingSql.d.ts +2 -0
  122. package/src/proxy/isMutatingSql.d.ts.map +1 -0
  123. package/src/proxy/isMutatingSql.js +12 -0
  124. package/src/proxy/kv/ZintrustKvProxy.d.ts +2 -1
  125. package/src/proxy/kv/ZintrustKvProxy.d.ts.map +1 -1
  126. package/src/proxy/kv/ZintrustKvProxy.js +2 -1
  127. package/src/proxy/mysql/MySqlProxyServer.d.ts +2 -8
  128. package/src/proxy/mysql/MySqlProxyServer.d.ts.map +1 -1
  129. package/src/proxy/mysql/MySqlProxyServer.js +84 -51
  130. package/src/proxy/postgres/PostgresProxyServer.d.ts +2 -8
  131. package/src/proxy/postgres/PostgresProxyServer.d.ts.map +1 -1
  132. package/src/proxy/postgres/PostgresProxyServer.js +86 -48
  133. package/src/proxy/smtp/SmtpProxyServer.d.ts.map +1 -1
  134. package/src/proxy/smtp/SmtpProxyServer.js +6 -5
  135. package/src/proxy/sqlserver/SqlServerProxyServer.d.ts +2 -8
  136. package/src/proxy/sqlserver/SqlServerProxyServer.d.ts.map +1 -1
  137. package/src/proxy/sqlserver/SqlServerProxyServer.js +84 -49
  138. package/src/proxy.d.ts +4 -0
  139. package/src/proxy.d.ts.map +1 -0
  140. package/src/proxy.js +3 -0
  141. package/src/scheduler/Schedule.d.ts +36 -0
  142. package/src/scheduler/Schedule.d.ts.map +1 -0
  143. package/src/scheduler/Schedule.js +197 -0
  144. package/src/scheduler/ScheduleHttpGateway.d.ts +8 -0
  145. package/src/scheduler/ScheduleHttpGateway.d.ts.map +1 -0
  146. package/src/scheduler/ScheduleHttpGateway.js +196 -0
  147. package/src/scheduler/ScheduleRunner.d.ts +6 -0
  148. package/src/scheduler/ScheduleRunner.d.ts.map +1 -1
  149. package/src/scheduler/ScheduleRunner.js +166 -29
  150. package/src/scheduler/SchedulerRuntime.d.ts +15 -0
  151. package/src/scheduler/SchedulerRuntime.d.ts.map +1 -0
  152. package/src/scheduler/SchedulerRuntime.js +79 -0
  153. package/src/scheduler/cron/Cron.d.ts +19 -0
  154. package/src/scheduler/cron/Cron.d.ts.map +1 -0
  155. package/src/scheduler/cron/Cron.js +200 -0
  156. package/src/scheduler/leader/SchedulerLeader.d.ts +14 -0
  157. package/src/scheduler/leader/SchedulerLeader.d.ts.map +1 -0
  158. package/src/scheduler/leader/SchedulerLeader.js +187 -0
  159. package/src/scheduler/state/ScheduleStateStore.d.ts +27 -0
  160. package/src/scheduler/state/ScheduleStateStore.d.ts.map +1 -0
  161. package/src/scheduler/state/ScheduleStateStore.js +27 -0
  162. package/src/scheduler/types.d.ts +10 -0
  163. package/src/scheduler/types.d.ts.map +1 -1
  164. package/src/schedules/index.d.ts +1 -0
  165. package/src/schedules/index.d.ts.map +1 -1
  166. package/src/schedules/index.js +1 -0
  167. package/src/schedules/job-tracking-cleanup.d.ts +4 -0
  168. package/src/schedules/job-tracking-cleanup.d.ts.map +1 -0
  169. package/src/schedules/job-tracking-cleanup.js +116 -0
  170. package/src/schedules/log-cleanup.d.ts +1 -2
  171. package/src/schedules/log-cleanup.d.ts.map +1 -1
  172. package/src/schedules/log-cleanup.js +12 -15
  173. package/src/security/Sanitizer.d.ts.map +1 -1
  174. package/src/security/Sanitizer.js +1 -9
  175. package/src/security/SignedRequest.d.ts.map +1 -1
  176. package/src/security/SignedRequest.js +2 -2
  177. package/src/templates/docker/docker-compose.ecosystem.yml.tpl +301 -0
  178. package/src/templates/docker/docker-compose.schedules.yml.tpl +84 -0
  179. package/src/templates/project/basic/app/Schedules/index.ts.tpl +0 -0
  180. package/src/templates/project/basic/config/database.ts.tpl +1 -1
  181. package/src/toolkit/Secrets/Manifest.d.ts.map +1 -1
  182. package/src/toolkit/Secrets/Manifest.js +5 -7
  183. package/src/tools/mail/drivers/Smtp.d.ts.map +1 -1
  184. package/src/tools/mail/drivers/Smtp.js +7 -1
  185. package/src/tools/queue/JobReconciliationRunner.d.ts.map +1 -1
  186. package/src/tools/queue/JobReconciliationRunner.js +7 -39
  187. package/src/tools/queue/JobRecoveryDaemon.d.ts.map +1 -1
  188. package/src/tools/queue/JobRecoveryDaemon.js +116 -18
  189. package/src/tools/queue/JobStateTracker.d.ts +10 -1
  190. package/src/tools/queue/JobStateTracker.d.ts.map +1 -1
  191. package/src/tools/queue/JobStateTracker.js +24 -2
  192. package/src/tools/queue/JobStateTrackerDbPersistence.d.ts.map +1 -1
  193. package/src/tools/queue/JobStateTrackerDbPersistence.js +93 -2
@@ -1,6 +1,7 @@
1
1
  import { Env } from '../../config/env.js';
2
2
  import { Logger } from '../../config/logger.js';
3
3
  import { ErrorFactory } from '../../exceptions/ZintrustError.js';
4
+ import { useDatabase } from '../../orm/Database.js';
4
5
  import { JobStateTracker } from './JobStateTracker.js';
5
6
  import { Queue } from './Queue.js';
6
7
  const parsePayload = (payload) => {
@@ -108,28 +109,78 @@ export const JobRecoveryDaemon = Object.freeze({
108
109
  return 'manual_review';
109
110
  }
110
111
  const payload = parsePayload(record.payload);
111
- const backoffMs = Math.min(300000, Math.max(1000, 1000 * 2 ** Math.max(0, record.attempts)));
112
- await Queue.enqueue(record.queueName, {
113
- ...payload,
114
- attempts: Math.max(0, record.attempts),
115
- timestamp: Date.now() + backoffMs,
116
- });
117
- await JobStateTracker.markedRecovered({
118
- queueName: record.queueName,
119
- jobId: record.jobId,
120
- reason: 'Recovered and re-queued by recovery daemon',
121
- retryAt: new Date(Date.now() + backoffMs).toISOString(),
122
- });
123
- return 'requeued';
112
+ // 30s, 1m, 3m backoff strategy
113
+ const getBackoffMs = (attempt) => {
114
+ if (attempt === 0)
115
+ return 30000;
116
+ if (attempt === 1)
117
+ return 60000;
118
+ return 180000;
119
+ };
120
+ const backoffMs = getBackoffMs(record.attempts);
121
+ try {
122
+ await Queue.enqueue(record.queueName, {
123
+ ...payload,
124
+ uniqueId: record.jobId, // Preserves job ID (prevents duplication)
125
+ attempts: maxAttempts,
126
+ _currentAttempts: record.attempts + 1,
127
+ timestamp: Date.now() + backoffMs,
128
+ });
129
+ // Once the job is in QUEUE_DRIVER (or already exists), we never re-enqueue from DB/recovery again.
130
+ await JobStateTracker.handedOffToQueue({
131
+ queueName: record.queueName,
132
+ jobId: record.jobId,
133
+ reason: 'Enqueue-fallback job handed off to QUEUE_DRIVER',
134
+ });
135
+ return 'requeued';
136
+ }
137
+ catch (error) {
138
+ const errorMessage = error instanceof Error ? error.message : String(error);
139
+ const message = errorMessage.toLowerCase();
140
+ if (message.includes('jobid') && message.includes('already exists')) {
141
+ await JobStateTracker.handedOffToQueue({
142
+ queueName: record.queueName,
143
+ jobId: record.jobId,
144
+ reason: 'Job already exists in queue driver',
145
+ });
146
+ return 'requeued';
147
+ }
148
+ await JobStateTracker.pendingRecovery({
149
+ queueName: record.queueName,
150
+ jobId: record.jobId,
151
+ reason: 'Enqueue-fallback retry failed during recovery daemon run',
152
+ attempts: record.attempts + 1,
153
+ maxAttempts,
154
+ retryAt: new Date(Date.now() + backoffMs).toISOString(),
155
+ error,
156
+ });
157
+ throw error;
158
+ }
124
159
  },
125
160
  async runOnce() {
126
161
  const minAgeMs = Math.max(0, Env.getInt('JOB_RECOVERY_MIN_AGE_MS', 5000));
127
162
  const candidates = JobStateTracker.listRecoverable(minAgeMs);
163
+ const persisted = await listRecoverableFromPersistence(minAgeMs);
164
+ // De-dupe jobs that exist in both in-memory tracker and persistence.
165
+ const seen = new Set();
166
+ const allCandidates = [...candidates, ...persisted].filter((row) => {
167
+ const key = `${row.queueName}:${row.jobId}`;
168
+ if (seen.has(key))
169
+ return false;
170
+ seen.add(key);
171
+ return true;
172
+ });
128
173
  let requeued = 0;
129
174
  let deadLetter = 0;
130
175
  let manualReview = 0;
131
- const results = await Promise.all(candidates.map(async (candidate) => this.recoverOne(candidate)));
132
- results.forEach((result) => {
176
+ const concurrency = 10;
177
+ const batches = [];
178
+ for (let offset = 0; offset < allCandidates.length; offset += concurrency) {
179
+ const slice = allCandidates.slice(offset, offset + concurrency);
180
+ batches.push(Promise.all(slice.map(async (candidate) => this.recoverOne(candidate))));
181
+ }
182
+ const batchResults = await Promise.all(batches);
183
+ batchResults.flat().forEach((result) => {
133
184
  if (result === 'requeued')
134
185
  requeued += 1;
135
186
  if (result === 'dead_letter')
@@ -137,16 +188,16 @@ export const JobRecoveryDaemon = Object.freeze({
137
188
  if (result === 'manual_review')
138
189
  manualReview += 1;
139
190
  });
140
- if (candidates.length > 0) {
191
+ if (allCandidates.length > 0) {
141
192
  Logger.info('Queue recovery daemon completed scan', {
142
- scanned: candidates.length,
193
+ scanned: allCandidates.length,
143
194
  requeued,
144
195
  deadLetter,
145
196
  manualReview,
146
197
  });
147
198
  }
148
199
  return {
149
- scanned: candidates.length,
200
+ scanned: allCandidates.length,
150
201
  requeued,
151
202
  deadLetter,
152
203
  manualReview,
@@ -202,4 +253,51 @@ export const JobRecoveryDaemon = Object.freeze({
202
253
  };
203
254
  },
204
255
  });
256
+ const toSqlDateTime = (value) => value.toISOString().slice(0, 19).replace('T', ' ');
257
+ const getPersistenceDb = () => useDatabase(undefined, Env.get('JOB_TRACKING_DB_CONNECTION', 'default'));
258
+ const listRecoverableFromPersistence = async (minAgeMs) => {
259
+ if (!Env.getBool('JOB_TRACKING_PERSISTENCE_ENABLED', false))
260
+ return [];
261
+ const db = getPersistenceDb();
262
+ const cutoff = new Date(Date.now() - Math.max(0, Math.floor(minAgeMs)));
263
+ const rows = await db
264
+ .table(Env.get('JOB_TRACKING_DB_TABLE', 'zintrust_jobs'))
265
+ .select('queue_name', 'job_id', 'attempts', 'max_attempts', 'payload_json', 'retry_at', 'updated_at')
266
+ .where('status', '=', 'pending_recovery')
267
+ .where('updated_at', '<=', toSqlDateTime(cutoff))
268
+ .limit(Math.max(1, Env.getInt('JOB_RECOVERY_DB_SCAN_LIMIT', 100)))
269
+ .get();
270
+ return (rows ?? []).map((row) => {
271
+ let payload = {};
272
+ try {
273
+ payload = JSON.parse(String(row.payload_json ?? '{}'));
274
+ }
275
+ catch {
276
+ payload = {};
277
+ }
278
+ const attempts = typeof row.attempts === 'number' && Number.isFinite(row.attempts)
279
+ ? Math.max(0, Math.floor(row.attempts))
280
+ : 0;
281
+ const maxAttempts = typeof row.max_attempts === 'number' && Number.isFinite(row.max_attempts)
282
+ ? Math.max(1, Math.floor(row.max_attempts))
283
+ : undefined;
284
+ const updatedAtRaw = typeof row.updated_at === 'string' ? row.updated_at : undefined;
285
+ const updatedAt = updatedAtRaw !== undefined && updatedAtRaw.trim().length > 0
286
+ ? updatedAtRaw
287
+ : new Date(0).toISOString();
288
+ const retryAtRaw = typeof row.retry_at === 'string' ? row.retry_at : undefined;
289
+ const retryAt = retryAtRaw !== undefined && retryAtRaw.trim().length > 0 ? retryAtRaw : undefined;
290
+ return {
291
+ queueName: String(row.queue_name),
292
+ jobId: String(row.job_id),
293
+ status: 'pending_recovery',
294
+ attempts,
295
+ maxAttempts,
296
+ createdAt: new Date().toISOString(),
297
+ updatedAt,
298
+ payload,
299
+ retryAt,
300
+ };
301
+ });
302
+ };
205
303
  export default JobRecoveryDaemon;
@@ -1,4 +1,4 @@
1
- export type JobTrackingStatus = 'pending' | 'active' | 'completed' | 'failed' | 'stalled' | 'timeout' | 'pending_recovery' | 'dead_letter' | 'manual_review' | 'delayed';
1
+ export type JobTrackingStatus = 'pending' | 'active' | 'enqueued' | 'completed' | 'failed' | 'stalled' | 'timeout' | 'pending_recovery' | 'dead_letter' | 'manual_review' | 'delayed';
2
2
  export type JobTrackingRecord = {
3
3
  jobId: string;
4
4
  queueName: string;
@@ -44,6 +44,7 @@ export declare const JobStateTracker: Readonly<{
44
44
  queueName: string;
45
45
  jobId: string;
46
46
  payload?: unknown;
47
+ attempts?: number;
47
48
  maxAttempts?: number;
48
49
  expectedCompletionAt?: string;
49
50
  idempotencyKey?: string;
@@ -91,6 +92,9 @@ export declare const JobStateTracker: Readonly<{
91
92
  pendingRecovery(input: {
92
93
  queueName: string;
93
94
  jobId: string;
95
+ attempts?: number;
96
+ maxAttempts?: number;
97
+ retryAt?: string;
94
98
  reason?: string;
95
99
  error?: unknown;
96
100
  }): Promise<void>;
@@ -100,6 +104,11 @@ export declare const JobStateTracker: Readonly<{
100
104
  reason?: string;
101
105
  retryAt?: string;
102
106
  }): Promise<void>;
107
+ handedOffToQueue(input: {
108
+ queueName: string;
109
+ jobId: string;
110
+ reason?: string;
111
+ }): Promise<void>;
103
112
  setTerminalStatus(input: {
104
113
  queueName: string;
105
114
  jobId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"JobStateTracker.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/JobStateTracker.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"JobStateTracker.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/JobStateTracker.ts"],"names":[],"mappings":"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"}
@@ -175,7 +175,7 @@ export const JobStateTracker = Object.freeze({
175
175
  if (isEnabled() === false)
176
176
  return;
177
177
  const transition = updateStatus(input.queueName, input.jobId, 'pending', 'Job enqueued', {
178
- attempts: 0,
178
+ attempts: typeof input.attempts === 'number' ? input.attempts : 0,
179
179
  maxAttempts: toFinitePositiveInt(input.maxAttempts),
180
180
  payload: QueueDataRedactor.sanitizePayload(input.payload),
181
181
  expectedCompletionAt: input.expectedCompletionAt,
@@ -259,6 +259,11 @@ export const JobStateTracker = Object.freeze({
259
259
  if (isEnabled() === false)
260
260
  return;
261
261
  const transition = updateStatus(input.queueName, input.jobId, 'pending_recovery', input.reason ?? 'Job pending recovery', {
262
+ attempts: typeof input.attempts === 'number' && Number.isFinite(input.attempts)
263
+ ? Math.max(0, Math.floor(input.attempts))
264
+ : undefined,
265
+ maxAttempts: toFinitePositiveInt(input.maxAttempts),
266
+ retryAt: input.retryAt,
262
267
  error: normalizeError(input.error) ?? input.reason,
263
268
  lastErrorCode: normalizeErrorCode(input.error),
264
269
  });
@@ -273,6 +278,16 @@ export const JobStateTracker = Object.freeze({
273
278
  });
274
279
  await persistLatest(input.queueName, input.jobId, transition);
275
280
  },
281
+ async handedOffToQueue(input) {
282
+ if (isEnabled() === false)
283
+ return;
284
+ const transition = updateStatus(input.queueName, input.jobId, 'enqueued', input.reason ?? 'Job handed off to queue driver', {
285
+ retryAt: undefined,
286
+ timeoutAt: undefined,
287
+ error: undefined,
288
+ });
289
+ await persistLatest(input.queueName, input.jobId, transition);
290
+ },
276
291
  async setTerminalStatus(input) {
277
292
  if (isEnabled() === false)
278
293
  return;
@@ -345,12 +360,19 @@ export const JobStateTracker = Object.freeze({
345
360
  },
346
361
  listRecoverable(maxAgeMs, queueName) {
347
362
  const threshold = Date.now() - Math.max(0, Math.floor(maxAgeMs));
348
- const recoverable = new Set(['pending_recovery', 'timeout', 'stalled']);
363
+ const recoverable = new Set(['pending_recovery']);
349
364
  return Array.from(trackedJobs.values()).filter((row) => {
350
365
  if (!recoverable.has(row.status))
351
366
  return false;
352
367
  if (queueName !== undefined && row.queueName !== queueName)
353
368
  return false;
369
+ // Respect scheduled retry window to avoid hot-loop recovery churn.
370
+ if (row.retryAt !== undefined && row.retryAt.trim().length > 0) {
371
+ const retryAtMs = new Date(row.retryAt).getTime();
372
+ if (!Number.isNaN(retryAtMs) && retryAtMs > Date.now()) {
373
+ return false;
374
+ }
375
+ }
354
376
  const reference = toEpochMs(row.updatedAt);
355
377
  if (reference === null)
356
378
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"JobStateTrackerDbPersistence.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/JobStateTrackerDbPersistence.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,iCAAiC,EAGlC,MAAM,wBAAwB,CAAC;AAEhC,KAAK,wBAAwB,GAAG;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AA+FF,eAAO,MAAM,kCAAkC,GAC7C,UAAS,wBAA6B,KACrC,iCAuCF,CAAC;AAEF,eAAO,MAAM,6CAA6C,QAAO,OAsBhE,CAAC;AAEF,eAAe,kCAAkC,CAAC"}
1
+ {"version":3,"file":"JobStateTrackerDbPersistence.d.ts","sourceRoot":"","sources":["../../../../src/tools/queue/JobStateTrackerDbPersistence.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,iCAAiC,EAGlC,MAAM,wBAAwB,CAAC;AAGhC,KAAK,wBAAwB,GAAG;IAC9B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAoMF,eAAO,MAAM,kCAAkC,GAC7C,UAAS,wBAA6B,KACrC,iCAwDF,CAAC;AAEF,eAAO,MAAM,6CAA6C,QAAO,OAsBhE,CAAC;AAEF,eAAe,kCAAkC,CAAC"}
@@ -13,6 +13,10 @@ const toJson = (value) => {
13
13
  return null;
14
14
  }
15
15
  };
16
+ const toNonNullJson = (value) => {
17
+ const json = toJson(value);
18
+ return json ?? null;
19
+ };
16
20
  const toSqlDateTime = (isoLike) => {
17
21
  if (typeof isoLike !== 'string' || isoLike.trim().length === 0)
18
22
  return null;
@@ -58,7 +62,7 @@ const serializeJobRecord = (record, options) => {
58
62
  status: record.status,
59
63
  attempts: record.attempts,
60
64
  max_attempts: record.maxAttempts ?? null,
61
- payload_json: options.persistPayload === false ? null : toJson(record.payload),
65
+ payload_json: toNonNullJson(record.payload),
62
66
  result_json: options.persistResult === false ? null : toJson(record.result),
63
67
  last_error: record.lastError ?? null,
64
68
  last_error_code: record.lastErrorCode ?? null,
@@ -78,6 +82,78 @@ const serializeJobRecord = (record, options) => {
78
82
  updated_at: toSqlDateTime(record.updatedAt),
79
83
  };
80
84
  };
85
+ const serializeJobRecordForInsert = (record, options) => {
86
+ const payload = serializeJobRecord(record, options);
87
+ // Ensure payload_json is always non-null for inserts (some schemas require NOT NULL).
88
+ if (payload['payload_json'] === null) {
89
+ payload['payload_json'] = '{}';
90
+ }
91
+ return payload;
92
+ };
93
+ const applyUpdateEntries = (update, entries) => {
94
+ entries.forEach((entry) => {
95
+ if (entry.enabled)
96
+ update[entry.key] = entry.value;
97
+ });
98
+ };
99
+ const resolveResultEntry = (record, options, base) => {
100
+ if (options.persistResult === false) {
101
+ return { key: 'result_json', enabled: true, value: null };
102
+ }
103
+ const enabled = record.result !== undefined && base['result_json'] !== null;
104
+ return { key: 'result_json', enabled, value: base['result_json'] };
105
+ };
106
+ const serializeJobRecordForUpdate = (record, options) => {
107
+ const base = serializeJobRecord(record, options);
108
+ const update = {
109
+ status: base['status'],
110
+ attempts: base['attempts'],
111
+ updated_at: base['updated_at'],
112
+ };
113
+ applyUpdateEntries(update, [
114
+ { key: 'max_attempts', enabled: record.maxAttempts !== undefined, value: base['max_attempts'] },
115
+ resolveResultEntry(record, options, base),
116
+ { key: 'last_error', enabled: record.lastError !== undefined, value: base['last_error'] },
117
+ {
118
+ key: 'last_error_code',
119
+ enabled: record.lastErrorCode !== undefined,
120
+ value: base['last_error_code'],
121
+ },
122
+ { key: 'retry_at', enabled: record.retryAt !== undefined, value: base['retry_at'] },
123
+ { key: 'timeout_at', enabled: record.timeoutAt !== undefined, value: base['timeout_at'] },
124
+ { key: 'heartbeat_at', enabled: record.heartbeatAt !== undefined, value: base['heartbeat_at'] },
125
+ {
126
+ key: 'expected_completion_at',
127
+ enabled: record.expectedCompletionAt !== undefined,
128
+ value: base['expected_completion_at'],
129
+ },
130
+ { key: 'worker_name', enabled: record.workerName !== undefined, value: base['worker_name'] },
131
+ {
132
+ key: 'worker_instance_id',
133
+ enabled: record.workerInstanceId !== undefined,
134
+ value: base['worker_instance_id'],
135
+ },
136
+ {
137
+ key: 'worker_region',
138
+ enabled: record.workerRegion !== undefined,
139
+ value: base['worker_region'],
140
+ },
141
+ {
142
+ key: 'worker_version',
143
+ enabled: record.workerVersion !== undefined,
144
+ value: base['worker_version'],
145
+ },
146
+ { key: 'recovered_at', enabled: record.recoveredAt !== undefined, value: base['recovered_at'] },
147
+ {
148
+ key: 'idempotency_key',
149
+ enabled: record.idempotencyKey !== undefined,
150
+ value: base['idempotency_key'],
151
+ },
152
+ { key: 'started_at', enabled: record.startedAt !== undefined, value: base['started_at'] },
153
+ { key: 'completed_at', enabled: record.completedAt !== undefined, value: base['completed_at'] },
154
+ ]);
155
+ return update;
156
+ };
81
157
  const serializeTransition = (transition) => {
82
158
  return {
83
159
  job_id: transition.jobId,
@@ -94,17 +170,27 @@ export const createJobStateTrackerDbPersistence = (options = {}) => {
94
170
  const connectionName = getConnectionName(options);
95
171
  const jobsTable = getJobsTable(options);
96
172
  const transitionsTable = getTransitionsTable(options);
173
+ const persistTransitions = () => Env.getBool('JOB_TRACKING_PERSIST_TRANSITIONS_ENABLED', false);
174
+ const shouldInsertNewRow = (record) => {
175
+ // zintrust_jobs is an enqueue-fallback buffer: only jobs that failed to enqueue
176
+ // should be inserted into persistence. Once the job is in QUEUE_DRIVER, we only
177
+ // update the existing row (e.g., status=enqueued) but never create new rows.
178
+ return record.status === 'pending_recovery';
179
+ };
97
180
  const upsertJob = async (record) => {
98
181
  const db = getDatabase(connectionName);
99
182
  if (db === null)
100
183
  return;
101
- const payload = serializeJobRecord(record, options);
102
184
  const existing = await db
103
185
  .table(jobsTable)
104
186
  .where('job_id', '=', record.jobId)
105
187
  .where('queue_name', '=', record.queueName)
106
188
  .first();
107
189
  if (existing) {
190
+ const existingStatus = typeof existing.status === 'string' ? existing.status.trim().toLowerCase() : '';
191
+ if (existingStatus === 'enqueued')
192
+ return;
193
+ const payload = serializeJobRecordForUpdate(record, options);
108
194
  await db
109
195
  .table(jobsTable)
110
196
  .where('job_id', '=', record.jobId)
@@ -112,9 +198,14 @@ export const createJobStateTrackerDbPersistence = (options = {}) => {
112
198
  .update(payload);
113
199
  return;
114
200
  }
201
+ if (!shouldInsertNewRow(record))
202
+ return;
203
+ const payload = serializeJobRecordForInsert(record, options);
115
204
  await db.table(jobsTable).insert(payload);
116
205
  };
117
206
  const insertTransition = async (transition) => {
207
+ if (persistTransitions() === false)
208
+ return;
118
209
  const db = getDatabase(connectionName);
119
210
  if (db === null)
120
211
  return;