@memberjunction/scheduling-engine-base 2.107.0
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/dist/ScheduledJobEntityExtended.d.ts +73 -0
- package/dist/ScheduledJobEntityExtended.d.ts.map +1 -0
- package/dist/ScheduledJobEntityExtended.js +153 -0
- package/dist/ScheduledJobEntityExtended.js.map +1 -0
- package/dist/SchedulingEngineBase.d.ts +76 -0
- package/dist/SchedulingEngineBase.d.ts.map +1 -0
- package/dist/SchedulingEngineBase.js +157 -0
- package/dist/SchedulingEngineBase.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Extended ScheduledJob entity with helper methods
|
|
3
|
+
* @module @memberjunction/scheduling-engine-base
|
|
4
|
+
*/
|
|
5
|
+
import { ScheduledJobEntity } from '@memberjunction/core-entities';
|
|
6
|
+
/**
|
|
7
|
+
* Extended ScheduledJob entity with scheduling helper methods
|
|
8
|
+
*
|
|
9
|
+
* Provides utilities for:
|
|
10
|
+
* - Calculating next run times
|
|
11
|
+
* - Determining minimum intervals
|
|
12
|
+
* - Validating cron expressions
|
|
13
|
+
*/
|
|
14
|
+
export declare class ScheduledJobEntityExtended extends ScheduledJobEntity {
|
|
15
|
+
/**
|
|
16
|
+
* Calculate time in milliseconds until next execution
|
|
17
|
+
* Note: Requires cron-parser, which is not available in base-engine
|
|
18
|
+
* This method is a placeholder for server-side implementation
|
|
19
|
+
*/
|
|
20
|
+
GetTimeUntilNextRun(): number;
|
|
21
|
+
/**
|
|
22
|
+
* Check if this job is currently locked by an execution
|
|
23
|
+
*/
|
|
24
|
+
get IsLocked(): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Check if lock is potentially stale
|
|
27
|
+
*/
|
|
28
|
+
get IsLockStale(): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Get the server instance that holds the current lock
|
|
31
|
+
*/
|
|
32
|
+
get CurrentLockHolder(): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Check if job allows concurrent runs
|
|
35
|
+
*/
|
|
36
|
+
get AllowsConcurrent(): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Check if job queues overlapping runs
|
|
39
|
+
*/
|
|
40
|
+
get QueuesOverlappingRuns(): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Check if job skips overlapping runs
|
|
43
|
+
*/
|
|
44
|
+
get SkipsOverlappingRuns(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Get success rate as percentage (0-100)
|
|
47
|
+
*/
|
|
48
|
+
get SuccessRate(): number;
|
|
49
|
+
/**
|
|
50
|
+
* Get failure rate as percentage (0-100)
|
|
51
|
+
*/
|
|
52
|
+
get FailureRate(): number;
|
|
53
|
+
/**
|
|
54
|
+
* Override Save to update polling interval cache after saving
|
|
55
|
+
*/
|
|
56
|
+
Save(options?: any): Promise<boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* Override Delete to update polling interval cache after deletion
|
|
59
|
+
*/
|
|
60
|
+
Delete(): Promise<boolean>;
|
|
61
|
+
/**
|
|
62
|
+
* Notify the scheduling engine about job changes
|
|
63
|
+
* This method reloads job metadata and restarts polling if needed
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
private notifyEngineOfChange;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Loader function to ensure this class is registered
|
|
70
|
+
* Prevents tree-shaking from removing the class
|
|
71
|
+
*/
|
|
72
|
+
export declare function LoadScheduledJobEntityExtended(): void;
|
|
73
|
+
//# sourceMappingURL=ScheduledJobEntityExtended.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduledJobEntityExtended.d.ts","sourceRoot":"","sources":["../src/ScheduledJobEntityExtended.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,+BAA+B,CAAC;AAInE;;;;;;;GAOG;AACH,qBACa,0BAA2B,SAAQ,kBAAkB;IAC9D;;;;OAIG;IACI,mBAAmB,IAAI,MAAM;IAOpC;;OAEG;IACH,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,OAAO,CAKhC;IAED;;OAEG;IACH,IAAW,iBAAiB,IAAI,MAAM,GAAG,IAAI,CAE5C;IAED;;OAEG;IACH,IAAW,gBAAgB,IAAI,OAAO,CAErC;IAED;;OAEG;IACH,IAAW,qBAAqB,IAAI,OAAO,CAE1C;IAED;;OAEG;IACH,IAAW,oBAAoB,IAAI,OAAO,CAEzC;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,MAAM,CAK/B;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,MAAM,CAK/B;IAED;;OAEG;IACmB,IAAI,CAAC,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC;IAgB3D;;OAEG;IACmB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBhD;;;;OAIG;YACW,oBAAoB;CASrC;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,IAAI,IAAI,CAErD"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Extended ScheduledJob entity with helper methods
|
|
4
|
+
* @module @memberjunction/scheduling-engine-base
|
|
5
|
+
*/
|
|
6
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
7
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
8
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
9
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
10
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
11
|
+
};
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.LoadScheduledJobEntityExtended = exports.ScheduledJobEntityExtended = void 0;
|
|
14
|
+
const core_entities_1 = require("@memberjunction/core-entities");
|
|
15
|
+
const global_1 = require("@memberjunction/global");
|
|
16
|
+
const SchedulingEngineBase_1 = require("./SchedulingEngineBase");
|
|
17
|
+
/**
|
|
18
|
+
* Extended ScheduledJob entity with scheduling helper methods
|
|
19
|
+
*
|
|
20
|
+
* Provides utilities for:
|
|
21
|
+
* - Calculating next run times
|
|
22
|
+
* - Determining minimum intervals
|
|
23
|
+
* - Validating cron expressions
|
|
24
|
+
*/
|
|
25
|
+
let ScheduledJobEntityExtended = class ScheduledJobEntityExtended extends core_entities_1.ScheduledJobEntity {
|
|
26
|
+
/**
|
|
27
|
+
* Calculate time in milliseconds until next execution
|
|
28
|
+
* Note: Requires cron-parser, which is not available in base-engine
|
|
29
|
+
* This method is a placeholder for server-side implementation
|
|
30
|
+
*/
|
|
31
|
+
GetTimeUntilNextRun() {
|
|
32
|
+
if (this.NextRunAt) {
|
|
33
|
+
return Math.max(0, this.NextRunAt.getTime() - Date.now());
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Check if this job is currently locked by an execution
|
|
39
|
+
*/
|
|
40
|
+
get IsLocked() {
|
|
41
|
+
return this.LockToken != null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if lock is potentially stale
|
|
45
|
+
*/
|
|
46
|
+
get IsLockStale() {
|
|
47
|
+
if (!this.IsLocked || !this.ExpectedCompletionAt) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return new Date() > this.ExpectedCompletionAt;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get the server instance that holds the current lock
|
|
54
|
+
*/
|
|
55
|
+
get CurrentLockHolder() {
|
|
56
|
+
return this.LockedByInstance;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if job allows concurrent runs
|
|
60
|
+
*/
|
|
61
|
+
get AllowsConcurrent() {
|
|
62
|
+
return this.ConcurrencyMode === 'Concurrent';
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if job queues overlapping runs
|
|
66
|
+
*/
|
|
67
|
+
get QueuesOverlappingRuns() {
|
|
68
|
+
return this.ConcurrencyMode === 'Queue';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if job skips overlapping runs
|
|
72
|
+
*/
|
|
73
|
+
get SkipsOverlappingRuns() {
|
|
74
|
+
return this.ConcurrencyMode === 'Skip';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get success rate as percentage (0-100)
|
|
78
|
+
*/
|
|
79
|
+
get SuccessRate() {
|
|
80
|
+
if (this.RunCount === 0) {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
return (this.SuccessCount / this.RunCount) * 100;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Get failure rate as percentage (0-100)
|
|
87
|
+
*/
|
|
88
|
+
get FailureRate() {
|
|
89
|
+
if (this.RunCount === 0) {
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
return (this.FailureCount / this.RunCount) * 100;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Override Save to update polling interval cache after saving
|
|
96
|
+
*/
|
|
97
|
+
async Save(options) {
|
|
98
|
+
const result = await super.Save(options);
|
|
99
|
+
if (result) {
|
|
100
|
+
// Notify the scheduling engine about the change
|
|
101
|
+
try {
|
|
102
|
+
await this.notifyEngineOfChange();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Log but don't fail the save operation
|
|
106
|
+
console.error('Failed to notify engine after save:', error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Override Delete to update polling interval cache after deletion
|
|
113
|
+
*/
|
|
114
|
+
async Delete() {
|
|
115
|
+
const result = await super.Delete();
|
|
116
|
+
if (result) {
|
|
117
|
+
// Notify the scheduling engine about the change
|
|
118
|
+
try {
|
|
119
|
+
await this.notifyEngineOfChange();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
// Log but don't fail the delete operation
|
|
123
|
+
console.error('Failed to notify engine after delete:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Notify the scheduling engine about job changes
|
|
130
|
+
* This method reloads job metadata and restarts polling if needed
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
async notifyEngineOfChange() {
|
|
134
|
+
const engine = SchedulingEngineBase_1.SchedulingEngineBase.Instance;
|
|
135
|
+
// Force reload of job metadata
|
|
136
|
+
await engine.Config(true, this.ContextCurrentUser);
|
|
137
|
+
// Recalculate polling interval (will set to null if no jobs)
|
|
138
|
+
engine.UpdatePollingInterval();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
exports.ScheduledJobEntityExtended = ScheduledJobEntityExtended;
|
|
142
|
+
exports.ScheduledJobEntityExtended = ScheduledJobEntityExtended = __decorate([
|
|
143
|
+
(0, global_1.RegisterClass)(core_entities_1.ScheduledJobEntity, 'ScheduledJobEntityExtended')
|
|
144
|
+
], ScheduledJobEntityExtended);
|
|
145
|
+
/**
|
|
146
|
+
* Loader function to ensure this class is registered
|
|
147
|
+
* Prevents tree-shaking from removing the class
|
|
148
|
+
*/
|
|
149
|
+
function LoadScheduledJobEntityExtended() {
|
|
150
|
+
// No-op function, just ensures class is loaded
|
|
151
|
+
}
|
|
152
|
+
exports.LoadScheduledJobEntityExtended = LoadScheduledJobEntityExtended;
|
|
153
|
+
//# sourceMappingURL=ScheduledJobEntityExtended.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScheduledJobEntityExtended.js","sourceRoot":"","sources":["../src/ScheduledJobEntityExtended.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Base scheduling engine with metadata caching
|
|
3
|
+
* @module @memberjunction/scheduling-engine-base
|
|
4
|
+
*/
|
|
5
|
+
import { BaseEngine, IMetadataProvider, UserInfo } from '@memberjunction/core';
|
|
6
|
+
import { ScheduledJobEntity, ScheduledJobTypeEntity, ScheduledJobRunEntity } from '@memberjunction/core-entities';
|
|
7
|
+
/**
|
|
8
|
+
* Base engine for scheduling system with metadata caching
|
|
9
|
+
*
|
|
10
|
+
* This engine loads and caches scheduling metadata including:
|
|
11
|
+
* - Scheduled job types (plugin registry)
|
|
12
|
+
* - Active scheduled jobs
|
|
13
|
+
* - Recent job runs (optional)
|
|
14
|
+
*
|
|
15
|
+
* Can be used anywhere (client or server) for accessing scheduling metadata.
|
|
16
|
+
* For actual job execution, use SchedulingEngine from @memberjunction/scheduling-engine.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SchedulingEngineBase extends BaseEngine<SchedulingEngineBase> {
|
|
19
|
+
private _scheduledJobTypes;
|
|
20
|
+
private _scheduledJobs;
|
|
21
|
+
private _scheduledJobRuns;
|
|
22
|
+
private _activePollingInterval;
|
|
23
|
+
/**
|
|
24
|
+
* Configure the engine by loading metadata
|
|
25
|
+
*
|
|
26
|
+
* @param forceRefresh - Whether to force reload from database
|
|
27
|
+
* @param contextUser - User context for data access
|
|
28
|
+
* @param provider - Optional metadata provider
|
|
29
|
+
* @param includeRuns - Whether to load recent job runs (default: false)
|
|
30
|
+
* @param includeAllJobs - Whether to load all jobs regardless of status (default: false, only loads Active)
|
|
31
|
+
*/
|
|
32
|
+
Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider, includeRuns?: boolean, includeAllJobs?: boolean): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Get all scheduled job types
|
|
35
|
+
*/
|
|
36
|
+
get ScheduledJobTypes(): ScheduledJobTypeEntity[];
|
|
37
|
+
/**
|
|
38
|
+
* Get scheduled jobs (active only by default, unless includeAllJobs was set in Config)
|
|
39
|
+
*/
|
|
40
|
+
get ScheduledJobs(): ScheduledJobEntity[];
|
|
41
|
+
/**
|
|
42
|
+
* Get recent scheduled job runs (only populated if includeRuns was set in Config)
|
|
43
|
+
*/
|
|
44
|
+
get ScheduledJobRuns(): ScheduledJobRunEntity[];
|
|
45
|
+
/**
|
|
46
|
+
* Find a job type by name
|
|
47
|
+
*/
|
|
48
|
+
GetJobTypeByName(name: string): ScheduledJobTypeEntity | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Find a job type by driver class
|
|
51
|
+
*/
|
|
52
|
+
GetJobTypeByDriverClass(driverClass: string): ScheduledJobTypeEntity | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Get all jobs of a specific type
|
|
55
|
+
*/
|
|
56
|
+
GetJobsByType(jobTypeId: string): ScheduledJobEntity[];
|
|
57
|
+
/**
|
|
58
|
+
* Get runs for a specific job
|
|
59
|
+
*/
|
|
60
|
+
GetRunsForJob(jobId: string): ScheduledJobRunEntity[];
|
|
61
|
+
/**
|
|
62
|
+
* Get the current active polling interval in milliseconds
|
|
63
|
+
* Returns null if no jobs are active (polling should be stopped)
|
|
64
|
+
*/
|
|
65
|
+
get ActivePollingInterval(): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* Calculate and update the active polling interval based on scheduled jobs
|
|
68
|
+
* This should be called whenever jobs are added, modified, or deleted
|
|
69
|
+
*/
|
|
70
|
+
UpdatePollingInterval(): void;
|
|
71
|
+
/**
|
|
72
|
+
* Get singleton instance
|
|
73
|
+
*/
|
|
74
|
+
static get Instance(): SchedulingEngineBase;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=SchedulingEngineBase.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchedulingEngineBase.d.ts","sourceRoot":"","sources":["../src/SchedulingEngineBase.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAA4B,iBAAiB,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACzG,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAElH;;;;;;;;;;GAUG;AACH,qBAAa,oBAAqB,SAAQ,UAAU,CAAC,oBAAoB,CAAC;IACtE,OAAO,CAAC,kBAAkB,CAAgC;IAC1D,OAAO,CAAC,cAAc,CAA4B;IAClD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,sBAAsB,CAAwB;IAEtD;;;;;;;;OAQG;IACU,MAAM,CACf,YAAY,CAAC,EAAE,OAAO,EACtB,WAAW,CAAC,EAAE,QAAQ,EACtB,QAAQ,CAAC,EAAE,iBAAiB,EAC5B,WAAW,GAAE,OAAe,EAC5B,cAAc,GAAE,OAAe,GAChC,OAAO,CAAC,OAAO,CAAC;IAqCnB;;OAEG;IACH,IAAW,iBAAiB,IAAI,sBAAsB,EAAE,CAEvD;IAED;;OAEG;IACH,IAAW,aAAa,IAAI,kBAAkB,EAAE,CAE/C;IAED;;OAEG;IACH,IAAW,gBAAgB,IAAI,qBAAqB,EAAE,CAErD;IAED;;OAEG;IACI,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAIzE;;OAEG;IACI,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,sBAAsB,GAAG,SAAS;IAIvF;;OAEG;IACI,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,kBAAkB,EAAE;IAI7D;;OAEG;IACI,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,qBAAqB,EAAE;IAI5D;;;OAGG;IACH,IAAW,qBAAqB,IAAI,MAAM,GAAG,IAAI,CAEhD;IAED;;;OAGG;IACI,qBAAqB,IAAI,IAAI;IAgCpC;;OAEG;IACH,WAAkB,QAAQ,IAAI,oBAAoB,CAEjD;CACJ"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Base scheduling engine with metadata caching
|
|
4
|
+
* @module @memberjunction/scheduling-engine-base
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SchedulingEngineBase = void 0;
|
|
8
|
+
const core_1 = require("@memberjunction/core");
|
|
9
|
+
/**
|
|
10
|
+
* Base engine for scheduling system with metadata caching
|
|
11
|
+
*
|
|
12
|
+
* This engine loads and caches scheduling metadata including:
|
|
13
|
+
* - Scheduled job types (plugin registry)
|
|
14
|
+
* - Active scheduled jobs
|
|
15
|
+
* - Recent job runs (optional)
|
|
16
|
+
*
|
|
17
|
+
* Can be used anywhere (client or server) for accessing scheduling metadata.
|
|
18
|
+
* For actual job execution, use SchedulingEngine from @memberjunction/scheduling-engine.
|
|
19
|
+
*/
|
|
20
|
+
class SchedulingEngineBase extends core_1.BaseEngine {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this._scheduledJobTypes = [];
|
|
24
|
+
this._scheduledJobs = [];
|
|
25
|
+
this._scheduledJobRuns = [];
|
|
26
|
+
this._activePollingInterval = 60000; // Default 1 minute, null when no jobs
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Configure the engine by loading metadata
|
|
30
|
+
*
|
|
31
|
+
* @param forceRefresh - Whether to force reload from database
|
|
32
|
+
* @param contextUser - User context for data access
|
|
33
|
+
* @param provider - Optional metadata provider
|
|
34
|
+
* @param includeRuns - Whether to load recent job runs (default: false)
|
|
35
|
+
* @param includeAllJobs - Whether to load all jobs regardless of status (default: false, only loads Active)
|
|
36
|
+
*/
|
|
37
|
+
async Config(forceRefresh, contextUser, provider, includeRuns = false, includeAllJobs = false) {
|
|
38
|
+
const configs = [
|
|
39
|
+
{
|
|
40
|
+
EntityName: 'MJ: Scheduled Job Types',
|
|
41
|
+
PropertyName: '_scheduledJobTypes'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
EntityName: 'MJ: Scheduled Jobs',
|
|
45
|
+
PropertyName: '_scheduledJobs'
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
// Optionally load recent runs
|
|
49
|
+
if (includeRuns) {
|
|
50
|
+
configs.push({
|
|
51
|
+
EntityName: 'MJ: Scheduled Job Runs',
|
|
52
|
+
PropertyName: '_scheduledJobRuns'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
await this.Load(configs, provider, forceRefresh, contextUser);
|
|
56
|
+
// Filter jobs to only active if requested
|
|
57
|
+
if (!includeAllJobs) {
|
|
58
|
+
this._scheduledJobs = this._scheduledJobs.filter(j => j.Status === 'Active');
|
|
59
|
+
}
|
|
60
|
+
// Filter runs to last 7 days if loaded
|
|
61
|
+
if (includeRuns) {
|
|
62
|
+
const sevenDaysAgo = new Date();
|
|
63
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
64
|
+
this._scheduledJobRuns = this._scheduledJobRuns.filter(r => r.StartedAt >= sevenDaysAgo);
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get all scheduled job types
|
|
70
|
+
*/
|
|
71
|
+
get ScheduledJobTypes() {
|
|
72
|
+
return this._scheduledJobTypes;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get scheduled jobs (active only by default, unless includeAllJobs was set in Config)
|
|
76
|
+
*/
|
|
77
|
+
get ScheduledJobs() {
|
|
78
|
+
return this._scheduledJobs;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get recent scheduled job runs (only populated if includeRuns was set in Config)
|
|
82
|
+
*/
|
|
83
|
+
get ScheduledJobRuns() {
|
|
84
|
+
return this._scheduledJobRuns;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Find a job type by name
|
|
88
|
+
*/
|
|
89
|
+
GetJobTypeByName(name) {
|
|
90
|
+
return this._scheduledJobTypes.find(jt => jt.Name === name);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find a job type by driver class
|
|
94
|
+
*/
|
|
95
|
+
GetJobTypeByDriverClass(driverClass) {
|
|
96
|
+
return this._scheduledJobTypes.find(jt => jt.DriverClass === driverClass);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get all jobs of a specific type
|
|
100
|
+
*/
|
|
101
|
+
GetJobsByType(jobTypeId) {
|
|
102
|
+
return this._scheduledJobs.filter(j => j.JobTypeID === jobTypeId);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get runs for a specific job
|
|
106
|
+
*/
|
|
107
|
+
GetRunsForJob(jobId) {
|
|
108
|
+
return this._scheduledJobRuns.filter(r => r.ScheduledJobID === jobId);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get the current active polling interval in milliseconds
|
|
112
|
+
* Returns null if no jobs are active (polling should be stopped)
|
|
113
|
+
*/
|
|
114
|
+
get ActivePollingInterval() {
|
|
115
|
+
return this._activePollingInterval;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Calculate and update the active polling interval based on scheduled jobs
|
|
119
|
+
* This should be called whenever jobs are added, modified, or deleted
|
|
120
|
+
*/
|
|
121
|
+
UpdatePollingInterval() {
|
|
122
|
+
if (this._scheduledJobs.length === 0) {
|
|
123
|
+
// No active jobs, stop polling
|
|
124
|
+
this._activePollingInterval = null;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Find the minimum time until next run across all active jobs
|
|
128
|
+
let minInterval = Number.MAX_SAFE_INTEGER;
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const job of this._scheduledJobs) {
|
|
131
|
+
if (job.Status !== 'Active' || !job.NextRunAt) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const timeUntilNext = Math.max(0, job.NextRunAt.getTime() - now);
|
|
135
|
+
if (timeUntilNext < minInterval) {
|
|
136
|
+
minInterval = timeUntilNext;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Set polling interval to half the minimum interval, with bounds
|
|
140
|
+
// Min: 1 minute (60000ms), Max: 1 week (604800000ms)
|
|
141
|
+
if (minInterval === Number.MAX_SAFE_INTEGER) {
|
|
142
|
+
this._activePollingInterval = 60000; // Default 1 minute
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const halfInterval = Math.floor(minInterval / 2);
|
|
146
|
+
this._activePollingInterval = Math.max(60000, Math.min(604800000, halfInterval));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get singleton instance
|
|
151
|
+
*/
|
|
152
|
+
static get Instance() {
|
|
153
|
+
return super.getInstance();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.SchedulingEngineBase = SchedulingEngineBase;
|
|
157
|
+
//# sourceMappingURL=SchedulingEngineBase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchedulingEngineBase.js","sourceRoot":"","sources":["../src/SchedulingEngineBase.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Main export for Scheduling Engine Base
|
|
3
|
+
* @module @memberjunction/scheduling-engine-base
|
|
4
|
+
*/
|
|
5
|
+
export * from './SchedulingEngineBase';
|
|
6
|
+
export * from './ScheduledJobEntityExtended';
|
|
7
|
+
/**
|
|
8
|
+
* Loader function to ensure all extended classes are registered
|
|
9
|
+
*/
|
|
10
|
+
export declare function LoadBaseSchedulingEngine(): void;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,cAAc,wBAAwB,CAAC;AACvC,cAAc,8BAA8B,CAAC;AAE7C;;GAEG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Main export for Scheduling Engine Base
|
|
4
|
+
* @module @memberjunction/scheduling-engine-base
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
|
+
};
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.LoadBaseSchedulingEngine = void 0;
|
|
22
|
+
const ScheduledJobEntityExtended_1 = require("./ScheduledJobEntityExtended");
|
|
23
|
+
__exportStar(require("./SchedulingEngineBase"), exports);
|
|
24
|
+
__exportStar(require("./ScheduledJobEntityExtended"), exports);
|
|
25
|
+
/**
|
|
26
|
+
* Loader function to ensure all extended classes are registered
|
|
27
|
+
*/
|
|
28
|
+
function LoadBaseSchedulingEngine() {
|
|
29
|
+
(0, ScheduledJobEntityExtended_1.LoadScheduledJobEntityExtended)();
|
|
30
|
+
}
|
|
31
|
+
exports.LoadBaseSchedulingEngine = LoadBaseSchedulingEngine;
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;;;;;;;;;;;;;;;AAEH,6EAA8E;AAE9E,yDAAuC;AACvC,+DAA6C;AAE7C;;GAEG;AACH,SAAgB,wBAAwB;IACpC,IAAA,2DAA8B,GAAE,CAAC;AACrC,CAAC;AAFD,4DAEC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@memberjunction/scheduling-engine-base",
|
|
3
|
+
"version": "2.107.0",
|
|
4
|
+
"description": "MemberJunction: Base Scheduling Engine Package - extended entity types and metadata caching, can be used anywhere.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"/dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"watch": "tsc --watch"
|
|
13
|
+
},
|
|
14
|
+
"author": "MemberJunction.com",
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@memberjunction/core": "2.100.3",
|
|
18
|
+
"@memberjunction/global": "2.100.3",
|
|
19
|
+
"@memberjunction/core-entities": "2.107.0",
|
|
20
|
+
"@memberjunction/scheduling-base-types": "2.107.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "20.14.2",
|
|
24
|
+
"typescript": "^5.4.5"
|
|
25
|
+
}
|
|
26
|
+
}
|