s3db.js 9.3.0 → 10.0.1
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.
- package/README.md +72 -13
- package/dist/s3db.cjs.js +2342 -540
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +2341 -541
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/client.class.js +8 -7
- package/src/concerns/high-performance-inserter.js +285 -0
- package/src/concerns/partition-queue.js +171 -0
- package/src/errors.js +10 -2
- package/src/partition-drivers/base-partition-driver.js +96 -0
- package/src/partition-drivers/index.js +60 -0
- package/src/partition-drivers/memory-partition-driver.js +274 -0
- package/src/partition-drivers/sqs-partition-driver.js +332 -0
- package/src/partition-drivers/sync-partition-driver.js +38 -0
- package/src/plugins/audit.plugin.js +4 -4
- package/src/plugins/backup.plugin.js +380 -105
- package/src/plugins/backup.plugin.js.backup +1 -1
- package/src/plugins/cache.plugin.js +203 -150
- package/src/plugins/eventual-consistency.plugin.js +1012 -0
- package/src/plugins/fulltext.plugin.js +6 -6
- package/src/plugins/index.js +2 -0
- package/src/plugins/metrics.plugin.js +13 -13
- package/src/plugins/replicator.plugin.js +108 -70
- package/src/plugins/replicators/s3db-replicator.class.js +7 -3
- package/src/plugins/replicators/sqs-replicator.class.js +11 -3
- package/src/plugins/s3-queue.plugin.js +776 -0
- package/src/plugins/scheduler.plugin.js +226 -164
- package/src/plugins/state-machine.plugin.js +109 -81
- package/src/resource.class.js +205 -0
- package/PLUGINS.md +0 -5036
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Plugin from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
|
+
import { idGenerator } from "../concerns/id.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* SchedulerPlugin - Cron-based Task Scheduling System
|
|
@@ -14,7 +15,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
14
15
|
* - Error handling and retry logic
|
|
15
16
|
* - Job persistence and recovery
|
|
16
17
|
* - Timezone support
|
|
17
|
-
* -
|
|
18
|
+
* - Distributed locking for multi-instance deployments
|
|
18
19
|
* - Resource cleanup and maintenance tasks
|
|
19
20
|
*
|
|
20
21
|
* === Configuration Example ===
|
|
@@ -38,7 +39,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
38
39
|
* return { deleted: expired.length };
|
|
39
40
|
* },
|
|
40
41
|
* enabled: true,
|
|
41
|
-
* retries: 3,
|
|
42
|
+
* retries: 3, // Number of retry attempts after initial failure (total: 4 attempts)
|
|
42
43
|
* timeout: 300000 // 5 minutes
|
|
43
44
|
* },
|
|
44
45
|
*
|
|
@@ -80,7 +81,6 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
80
81
|
* }
|
|
81
82
|
* throw new Error('BackupPlugin not available');
|
|
82
83
|
* },
|
|
83
|
-
* dependencies: ['backup_full'], // Run only after full backup exists
|
|
84
84
|
* retries: 2
|
|
85
85
|
* },
|
|
86
86
|
*
|
|
@@ -153,7 +153,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
153
153
|
jobs: options.jobs || {},
|
|
154
154
|
defaultTimeout: options.defaultTimeout || 300000, // 5 minutes
|
|
155
155
|
defaultRetries: options.defaultRetries || 1,
|
|
156
|
-
jobHistoryResource: options.jobHistoryResource || '
|
|
156
|
+
jobHistoryResource: options.jobHistoryResource || 'plg_job_executions',
|
|
157
157
|
persistJobs: options.persistJobs !== false,
|
|
158
158
|
verbose: options.verbose || false,
|
|
159
159
|
onJobStart: options.onJobStart || null,
|
|
@@ -163,14 +163,25 @@ export class SchedulerPlugin extends Plugin {
|
|
|
163
163
|
};
|
|
164
164
|
|
|
165
165
|
this.database = null;
|
|
166
|
+
this.lockResource = null;
|
|
166
167
|
this.jobs = new Map();
|
|
167
168
|
this.activeJobs = new Map();
|
|
168
169
|
this.timers = new Map();
|
|
169
170
|
this.statistics = new Map();
|
|
170
|
-
|
|
171
|
+
|
|
171
172
|
this._validateConfiguration();
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Helper to detect test environment
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
_isTestEnvironment() {
|
|
180
|
+
return process.env.NODE_ENV === 'test' ||
|
|
181
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
182
|
+
global.expect !== undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
174
185
|
_validateConfiguration() {
|
|
175
186
|
if (Object.keys(this.config.jobs).length === 0) {
|
|
176
187
|
throw new Error('SchedulerPlugin: At least one job must be defined');
|
|
@@ -208,7 +219,10 @@ export class SchedulerPlugin extends Plugin {
|
|
|
208
219
|
|
|
209
220
|
async setup(database) {
|
|
210
221
|
this.database = database;
|
|
211
|
-
|
|
222
|
+
|
|
223
|
+
// Create lock resource for distributed locking
|
|
224
|
+
await this._createLockResource();
|
|
225
|
+
|
|
212
226
|
// Create job execution history resource
|
|
213
227
|
if (this.config.persistJobs) {
|
|
214
228
|
await this._createJobHistoryResource();
|
|
@@ -245,6 +259,28 @@ export class SchedulerPlugin extends Plugin {
|
|
|
245
259
|
this.emit('initialized', { jobs: this.jobs.size });
|
|
246
260
|
}
|
|
247
261
|
|
|
262
|
+
async _createLockResource() {
|
|
263
|
+
const [ok, err, lockResource] = await tryFn(() =>
|
|
264
|
+
this.database.createResource({
|
|
265
|
+
name: 'plg_scheduler_job_locks',
|
|
266
|
+
attributes: {
|
|
267
|
+
id: 'string|required',
|
|
268
|
+
jobName: 'string|required',
|
|
269
|
+
lockedAt: 'number|required',
|
|
270
|
+
instanceId: 'string|optional'
|
|
271
|
+
},
|
|
272
|
+
behavior: 'body-only',
|
|
273
|
+
timestamps: false
|
|
274
|
+
})
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (!ok && !this.database.resources.plg_scheduler_job_locks) {
|
|
278
|
+
throw new Error(`Failed to create lock resource: ${err?.message}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
this.lockResource = ok ? lockResource : this.database.resources.plg_scheduler_job_locks;
|
|
282
|
+
}
|
|
283
|
+
|
|
248
284
|
async _createJobHistoryResource() {
|
|
249
285
|
const [ok] = await tryFn(() => this.database.createResource({
|
|
250
286
|
name: this.config.jobHistoryResource,
|
|
@@ -359,10 +395,7 @@ export class SchedulerPlugin extends Plugin {
|
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
// For tests, ensure we always schedule in the future
|
|
362
|
-
|
|
363
|
-
process.env.JEST_WORKER_ID !== undefined ||
|
|
364
|
-
global.expect !== undefined;
|
|
365
|
-
if (isTestEnvironment) {
|
|
398
|
+
if (this._isTestEnvironment()) {
|
|
366
399
|
// Add 1 second to ensure it's in the future for tests
|
|
367
400
|
next.setTime(next.getTime() + 1000);
|
|
368
401
|
}
|
|
@@ -372,147 +405,180 @@ export class SchedulerPlugin extends Plugin {
|
|
|
372
405
|
|
|
373
406
|
async _executeJob(jobName) {
|
|
374
407
|
const job = this.jobs.get(jobName);
|
|
375
|
-
if (!job
|
|
408
|
+
if (!job) {
|
|
376
409
|
return;
|
|
377
410
|
}
|
|
378
|
-
|
|
379
|
-
|
|
411
|
+
|
|
412
|
+
// Check and mark as active atomically to prevent race conditions
|
|
413
|
+
if (this.activeJobs.has(jobName)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Mark as active immediately (will be updated with executionId later)
|
|
418
|
+
this.activeJobs.set(jobName, 'acquiring-lock');
|
|
419
|
+
|
|
420
|
+
// Acquire distributed lock to prevent concurrent execution across instances
|
|
421
|
+
const lockId = `lock-${jobName}`;
|
|
422
|
+
const [lockAcquired, lockErr] = await tryFn(() =>
|
|
423
|
+
this.lockResource.insert({
|
|
424
|
+
id: lockId,
|
|
425
|
+
jobName,
|
|
426
|
+
lockedAt: Date.now(),
|
|
427
|
+
instanceId: process.pid ? String(process.pid) : 'unknown'
|
|
428
|
+
})
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// If lock couldn't be acquired, another instance is executing this job
|
|
432
|
+
if (!lockAcquired) {
|
|
433
|
+
if (this.config.verbose) {
|
|
434
|
+
console.log(`[SchedulerPlugin] Job '${jobName}' already running on another instance`);
|
|
435
|
+
}
|
|
436
|
+
// Remove from activeJobs since we didn't acquire the lock
|
|
437
|
+
this.activeJobs.delete(jobName);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const executionId = `${jobName}_${idGenerator()}`;
|
|
380
442
|
const startTime = Date.now();
|
|
381
|
-
|
|
443
|
+
|
|
382
444
|
const context = {
|
|
383
445
|
jobName,
|
|
384
446
|
executionId,
|
|
385
447
|
scheduledTime: new Date(startTime),
|
|
386
448
|
database: this.database
|
|
387
449
|
};
|
|
388
|
-
|
|
450
|
+
|
|
451
|
+
// Update with actual executionId
|
|
389
452
|
this.activeJobs.set(jobName, executionId);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
while (attempt <= job.retries) { // attempt 0 = initial, attempt 1+ = retries
|
|
409
|
-
try {
|
|
410
|
-
// Set timeout for job execution (reduce timeout in test environment)
|
|
411
|
-
const actualTimeout = isTestEnvironment ? Math.min(job.timeout, 1000) : job.timeout; // Max 1000ms in tests
|
|
412
|
-
|
|
413
|
-
let timeoutId;
|
|
414
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
415
|
-
timeoutId = setTimeout(() => reject(new Error('Job execution timeout')), actualTimeout);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Execute job with timeout
|
|
419
|
-
const jobPromise = job.action(this.database, context, this);
|
|
420
|
-
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
// Execute onJobStart hook
|
|
456
|
+
if (this.config.onJobStart) {
|
|
457
|
+
await this._executeHook(this.config.onJobStart, jobName, context);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
this.emit('job_start', { jobName, executionId, startTime });
|
|
461
|
+
|
|
462
|
+
let attempt = 0;
|
|
463
|
+
let lastError = null;
|
|
464
|
+
let result = null;
|
|
465
|
+
let status = 'success';
|
|
466
|
+
|
|
467
|
+
// Detect test environment once
|
|
468
|
+
const isTestEnvironment = this._isTestEnvironment();
|
|
469
|
+
|
|
470
|
+
while (attempt <= job.retries) { // attempt 0 = initial, attempt 1+ = retries
|
|
421
471
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
472
|
+
// Set timeout for job execution (reduce timeout in test environment)
|
|
473
|
+
const actualTimeout = isTestEnvironment ? Math.min(job.timeout, 1000) : job.timeout; // Max 1000ms in tests
|
|
474
|
+
|
|
475
|
+
let timeoutId;
|
|
476
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
477
|
+
timeoutId = setTimeout(() => reject(new Error('Job execution timeout')), actualTimeout);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Execute job with timeout
|
|
481
|
+
const jobPromise = job.action(this.database, context, this);
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
result = await Promise.race([jobPromise, timeoutPromise]);
|
|
485
|
+
// Clear timeout if job completes successfully
|
|
486
|
+
clearTimeout(timeoutId);
|
|
487
|
+
} catch (raceError) {
|
|
488
|
+
// Ensure timeout is cleared even on error
|
|
489
|
+
clearTimeout(timeoutId);
|
|
490
|
+
throw raceError;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
status = 'success';
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
} catch (error) {
|
|
497
|
+
lastError = error;
|
|
498
|
+
attempt++;
|
|
499
|
+
|
|
500
|
+
if (attempt <= job.retries) {
|
|
501
|
+
if (this.config.verbose) {
|
|
502
|
+
console.warn(`[SchedulerPlugin] Job '${jobName}' failed (attempt ${attempt + 1}):`, error.message);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Wait before retry (exponential backoff with max delay, shorter in tests)
|
|
506
|
+
const baseDelay = Math.min(Math.pow(2, attempt) * 1000, 5000); // Max 5 seconds
|
|
507
|
+
const delay = isTestEnvironment ? 1 : baseDelay; // Just 1ms in tests
|
|
508
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
441
509
|
}
|
|
442
|
-
|
|
443
|
-
// Wait before retry (exponential backoff with max delay, shorter in tests)
|
|
444
|
-
const baseDelay = Math.min(Math.pow(2, attempt) * 1000, 5000); // Max 5 seconds
|
|
445
|
-
const delay = isTestEnvironment ? 1 : baseDelay; // Just 1ms in tests
|
|
446
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
447
510
|
}
|
|
448
511
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Remove from active jobs
|
|
506
|
-
this.activeJobs.delete(jobName);
|
|
507
|
-
|
|
508
|
-
// Schedule next execution if job is still enabled
|
|
509
|
-
if (job.enabled) {
|
|
510
|
-
this._scheduleNextExecution(jobName);
|
|
511
|
-
}
|
|
512
|
+
|
|
513
|
+
const endTime = Date.now();
|
|
514
|
+
const duration = Math.max(1, endTime - startTime); // Ensure minimum 1ms duration
|
|
515
|
+
|
|
516
|
+
if (lastError && attempt > job.retries) {
|
|
517
|
+
status = lastError.message.includes('timeout') ? 'timeout' : 'error';
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Update job statistics
|
|
521
|
+
job.lastRun = new Date(endTime);
|
|
522
|
+
job.runCount++;
|
|
523
|
+
|
|
524
|
+
if (status === 'success') {
|
|
525
|
+
job.successCount++;
|
|
526
|
+
} else {
|
|
527
|
+
job.errorCount++;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Update plugin statistics
|
|
531
|
+
const stats = this.statistics.get(jobName);
|
|
532
|
+
stats.totalRuns++;
|
|
533
|
+
stats.lastRun = new Date(endTime);
|
|
534
|
+
|
|
535
|
+
if (status === 'success') {
|
|
536
|
+
stats.totalSuccesses++;
|
|
537
|
+
stats.lastSuccess = new Date(endTime);
|
|
538
|
+
} else {
|
|
539
|
+
stats.totalErrors++;
|
|
540
|
+
stats.lastError = { time: new Date(endTime), message: lastError?.message };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
stats.avgDuration = ((stats.avgDuration * (stats.totalRuns - 1)) + duration) / stats.totalRuns;
|
|
544
|
+
|
|
545
|
+
// Persist execution history
|
|
546
|
+
if (this.config.persistJobs) {
|
|
547
|
+
await this._persistJobExecution(jobName, executionId, startTime, endTime, duration, status, result, lastError, attempt);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Execute completion hooks
|
|
551
|
+
if (status === 'success' && this.config.onJobComplete) {
|
|
552
|
+
await this._executeHook(this.config.onJobComplete, jobName, result, duration);
|
|
553
|
+
} else if (status !== 'success' && this.config.onJobError) {
|
|
554
|
+
await this._executeHook(this.config.onJobError, jobName, lastError, attempt);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
this.emit('job_complete', {
|
|
558
|
+
jobName,
|
|
559
|
+
executionId,
|
|
560
|
+
status,
|
|
561
|
+
duration,
|
|
562
|
+
result,
|
|
563
|
+
error: lastError?.message,
|
|
564
|
+
retryCount: attempt
|
|
565
|
+
});
|
|
512
566
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
567
|
+
// Remove from active jobs
|
|
568
|
+
this.activeJobs.delete(jobName);
|
|
569
|
+
|
|
570
|
+
// Schedule next execution if job is still enabled
|
|
571
|
+
if (job.enabled) {
|
|
572
|
+
this._scheduleNextExecution(jobName);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Throw error if all retries failed
|
|
576
|
+
if (lastError && status !== 'success') {
|
|
577
|
+
throw lastError;
|
|
578
|
+
}
|
|
579
|
+
} finally {
|
|
580
|
+
// Always release the distributed lock
|
|
581
|
+
await tryFn(() => this.lockResource.delete(lockId));
|
|
516
582
|
}
|
|
517
583
|
}
|
|
518
584
|
|
|
@@ -548,17 +614,18 @@ export class SchedulerPlugin extends Plugin {
|
|
|
548
614
|
|
|
549
615
|
/**
|
|
550
616
|
* Manually trigger a job execution
|
|
617
|
+
* Note: Race conditions are prevented by distributed locking in _executeJob()
|
|
551
618
|
*/
|
|
552
619
|
async runJob(jobName, context = {}) {
|
|
553
620
|
const job = this.jobs.get(jobName);
|
|
554
621
|
if (!job) {
|
|
555
622
|
throw new Error(`Job '${jobName}' not found`);
|
|
556
623
|
}
|
|
557
|
-
|
|
624
|
+
|
|
558
625
|
if (this.activeJobs.has(jobName)) {
|
|
559
626
|
throw new Error(`Job '${jobName}' is already running`);
|
|
560
627
|
}
|
|
561
|
-
|
|
628
|
+
|
|
562
629
|
await this._executeJob(jobName);
|
|
563
630
|
}
|
|
564
631
|
|
|
@@ -647,33 +714,32 @@ export class SchedulerPlugin extends Plugin {
|
|
|
647
714
|
if (!this.config.persistJobs) {
|
|
648
715
|
return [];
|
|
649
716
|
}
|
|
650
|
-
|
|
717
|
+
|
|
651
718
|
const { limit = 50, status = null } = options;
|
|
652
|
-
|
|
653
|
-
//
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
719
|
+
|
|
720
|
+
// Build query to use partition (byJob)
|
|
721
|
+
const queryParams = {
|
|
722
|
+
jobName // Uses byJob partition for efficient lookup
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
if (status) {
|
|
726
|
+
queryParams.status = status;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// Use query() to leverage partitions instead of list() + filter
|
|
730
|
+
const [ok, err, history] = await tryFn(() =>
|
|
731
|
+
this.database.resource(this.config.jobHistoryResource).query(queryParams)
|
|
659
732
|
);
|
|
660
|
-
|
|
733
|
+
|
|
661
734
|
if (!ok) {
|
|
662
735
|
if (this.config.verbose) {
|
|
663
736
|
console.warn(`[SchedulerPlugin] Failed to get job history:`, err.message);
|
|
664
737
|
}
|
|
665
738
|
return [];
|
|
666
739
|
}
|
|
667
|
-
|
|
668
|
-
// Filter client-side
|
|
669
|
-
let filtered = allHistory.filter(h => h.jobName === jobName);
|
|
670
|
-
|
|
671
|
-
if (status) {
|
|
672
|
-
filtered = filtered.filter(h => h.status === status);
|
|
673
|
-
}
|
|
674
|
-
|
|
740
|
+
|
|
675
741
|
// Sort by startTime descending and limit
|
|
676
|
-
filtered =
|
|
742
|
+
let filtered = history.sort((a, b) => b.startTime - a.startTime).slice(0, limit);
|
|
677
743
|
|
|
678
744
|
return filtered.map(h => {
|
|
679
745
|
let result = null;
|
|
@@ -791,13 +857,9 @@ export class SchedulerPlugin extends Plugin {
|
|
|
791
857
|
clearTimeout(timer);
|
|
792
858
|
}
|
|
793
859
|
this.timers.clear();
|
|
794
|
-
|
|
860
|
+
|
|
795
861
|
// For tests, don't wait for active jobs - they may be mocked
|
|
796
|
-
|
|
797
|
-
process.env.JEST_WORKER_ID !== undefined ||
|
|
798
|
-
global.expect !== undefined;
|
|
799
|
-
|
|
800
|
-
if (!isTestEnvironment && this.activeJobs.size > 0) {
|
|
862
|
+
if (!this._isTestEnvironment() && this.activeJobs.size > 0) {
|
|
801
863
|
if (this.config.verbose) {
|
|
802
864
|
console.log(`[SchedulerPlugin] Waiting for ${this.activeJobs.size} active jobs to complete...`);
|
|
803
865
|
}
|
|
@@ -814,9 +876,9 @@ export class SchedulerPlugin extends Plugin {
|
|
|
814
876
|
console.warn(`[SchedulerPlugin] ${this.activeJobs.size} jobs still running after timeout`);
|
|
815
877
|
}
|
|
816
878
|
}
|
|
817
|
-
|
|
879
|
+
|
|
818
880
|
// Clear active jobs in test environment
|
|
819
|
-
if (
|
|
881
|
+
if (this._isTestEnvironment()) {
|
|
820
882
|
this.activeJobs.clear();
|
|
821
883
|
}
|
|
822
884
|
}
|