koatty_schedule 2.0.1 → 3.0.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/CHANGELOG.md +28 -0
- package/README.md +423 -31
- package/dist/README.md +423 -31
- package/dist/index.d.ts +75 -27
- package/dist/index.js +879 -265
- package/dist/index.mjs +881 -251
- package/dist/package.json +16 -12
- package/package.json +16 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [3.0.0](https://github.com/thinkkoa/koatty_schedule/compare/v2.1.0...v3.0.0) (2025-06-21)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* enhance distributed locking and scheduling system with global configuration management and improved validation ([cf3924c](https://github.com/thinkkoa/koatty_schedule/commit/cf3924cf6bccf951f070c68e33483ae935828382))
|
|
11
|
+
* improve RedLock singleton management with thread-safe initialization and lock renewal enhancements ([4e381cd](https://github.com/thinkkoa/koatty_schedule/commit/4e381cd8eec6aa366a6db813918f213f07b02921))
|
|
12
|
+
* refactor RedLock configuration and remove deprecated ScheduleConfig ([bb10ac7](https://github.com/thinkkoa/koatty_schedule/commit/bb10ac7dab67d32ca75a43db92c587a662bc1b9f))
|
|
13
|
+
|
|
14
|
+
## [2.1.0](https://github.com/thinkkoa/koatty_schedule/compare/v2.0.1...v2.1.0) (2025-06-09)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add schedule and redlock decorators with config management ([c1b5359](https://github.com/thinkkoa/koatty_schedule/commit/c1b535940df2b8a3403bf024137519246945870e))
|
|
20
|
+
* enhance ConfigManager with singleton pattern, environment config loading ([00db6eb](https://github.com/thinkkoa/koatty_schedule/commit/00db6eb97bdae226aaf433b23c770704b33d05e8))
|
|
21
|
+
* introduce DecoratorType enum, refactor decorator management system, ([e58e718](https://github.com/thinkkoa/koatty_schedule/commit/e58e718975e663820778352bedb6421e6852ba9f))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* simplify error handling in ConfigManager and RedLocker ([7be75fc](https://github.com/thinkkoa/koatty_schedule/commit/7be75fc7f4160094b57ca64905df4c81f77adb51))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Refactor
|
|
30
|
+
|
|
31
|
+
* use MethodDecoratorManager ([ff077c7](https://github.com/thinkkoa/koatty_schedule/commit/ff077c7211bb6cf258c6885e1d7dcbdacde90ef1))
|
|
32
|
+
|
|
5
33
|
### [2.0.1](https://github.com/thinkkoa/koatty_schedule/compare/v2.0.0...v2.0.1) (2024-01-17)
|
|
6
34
|
|
|
7
35
|
## [2.0.0](https://github.com/thinkkoa/koatty_schedule/compare/v1.6.0...v2.0.0) (2024-01-17)
|
package/README.md
CHANGED
|
@@ -1,48 +1,440 @@
|
|
|
1
1
|
# koatty_schedule
|
|
2
|
-
Schedule for koatty.
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
Powerful scheduled tasks and distributed locking solution for Koatty framework.
|
|
5
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/koatty_schedule)
|
|
6
|
+
[](https://github.com/koattyjs/koatty_schedule)
|
|
7
|
+
[](https://github.com/koattyjs/koatty_schedule/blob/main/LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
## Features
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
- 🕒 **Flexible Scheduling**: Support for cron expressions with timezone configuration
|
|
12
|
+
- 🔐 **Distributed Locking**: RedLock-based distributed locks with auto-extension
|
|
13
|
+
- 🏗️ **Plugin Architecture**: Native Koatty plugin integration
|
|
14
|
+
- ⚡ **Performance Optimized**: Singleton pattern, caching, and batch processing
|
|
15
|
+
- 🛡️ **Enhanced Safety**: Lock renewal logic with timeout protection
|
|
16
|
+
- 🌍 **Timezone Smart**: Three-tier priority system for timezone configuration
|
|
17
|
+
- 📊 **Health Monitoring**: Built-in health checks and detailed status reporting
|
|
18
|
+
- 🔧 **Easy Configuration**: Method-level and global configuration options
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install koatty_schedule
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### 1. Generate Plugin Template
|
|
29
|
+
|
|
30
|
+
Use Koatty CLI to generate the plugin template:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
kt plugin
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Create Scheduled Plugin
|
|
37
|
+
|
|
38
|
+
Create `src/plugin/Scheduled.ts`:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { Plugin, IPlugin, App } from "koatty";
|
|
42
|
+
import { KoattyScheduled } from "koatty_schedule";
|
|
43
|
+
|
|
44
|
+
@Plugin()
|
|
45
|
+
export class Scheduled implements IPlugin {
|
|
46
|
+
run(options: any, app: App) {
|
|
47
|
+
return KoattyScheduled(options, app);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Configure Plugin
|
|
53
|
+
|
|
54
|
+
Update `src/config/plugin.ts`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
export default {
|
|
58
|
+
list: ["Scheduled"], // Plugin loading order
|
|
59
|
+
config: {
|
|
60
|
+
Scheduled: {
|
|
61
|
+
timezone: "Asia/Shanghai",
|
|
62
|
+
lockTimeOut: 10000,
|
|
63
|
+
maxRetries: 3,
|
|
64
|
+
retryDelayMs: 200,
|
|
65
|
+
redisConfig: {
|
|
66
|
+
host: "127.0.0.1",
|
|
67
|
+
port: 6379,
|
|
68
|
+
db: 0,
|
|
69
|
+
keyPrefix: "koatty:schedule:"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
78
|
+
### Basic Scheduling
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { Scheduled, RedLock } from "koatty_schedule";
|
|
82
|
+
|
|
83
|
+
export class TaskService {
|
|
84
|
+
|
|
85
|
+
@Scheduled("0 */5 * * * *") // Every 5 minutes
|
|
86
|
+
async processData() {
|
|
87
|
+
console.log("Processing data...");
|
|
88
|
+
// Your business logic here
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@Scheduled("0 0 2 * * *", "UTC") // 2 AM UTC daily
|
|
92
|
+
async dailyCleanup() {
|
|
93
|
+
console.log("Running daily cleanup...");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Distributed Locking
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
export class CriticalTaskService {
|
|
102
|
+
|
|
103
|
+
@Scheduled("0 */10 * * * *")
|
|
104
|
+
@RedLock("critical-task") // Prevents concurrent execution
|
|
105
|
+
async criticalTask() {
|
|
106
|
+
console.log("Running critical task with lock protection...");
|
|
107
|
+
// Only one instance can execute this at a time
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@RedLock("user-sync", {
|
|
111
|
+
lockTimeOut: 30000, // 30 seconds
|
|
112
|
+
maxRetries: 5, // Retry 5 times
|
|
113
|
+
retryDelayMs: 500 // Wait 500ms between retries
|
|
114
|
+
})
|
|
115
|
+
async syncUsers() {
|
|
116
|
+
console.log("Syncing users with lock protection...");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Advanced Configuration
|
|
122
|
+
|
|
123
|
+
### Global Plugin Configuration
|
|
124
|
+
|
|
125
|
+
Configure global settings in `src/config/plugin.ts`:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
12
128
|
export default {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
129
|
+
list: ["Scheduled"],
|
|
130
|
+
config: {
|
|
131
|
+
Scheduled: {
|
|
132
|
+
// Global timezone (can be overridden per method)
|
|
133
|
+
timezone: "Asia/Shanghai",
|
|
134
|
+
|
|
135
|
+
// Default RedLock settings
|
|
136
|
+
lockTimeOut: 15000,
|
|
137
|
+
maxRetries: 3,
|
|
138
|
+
retryDelayMs: 200,
|
|
139
|
+
clockDriftFactor: 0.01,
|
|
140
|
+
|
|
141
|
+
// Redis configuration for distributed locks
|
|
142
|
+
redisConfig: {
|
|
143
|
+
host: "redis.example.com",
|
|
144
|
+
port: 6379,
|
|
145
|
+
password: "your-password",
|
|
146
|
+
db: 1,
|
|
147
|
+
keyPrefix: "myapp:locks:",
|
|
148
|
+
connectTimeout: 5000,
|
|
149
|
+
commandTimeout: 10000
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
30
153
|
};
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Method-Level Overrides
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
export class AdvancedTaskService {
|
|
160
|
+
|
|
161
|
+
// Custom timezone override
|
|
162
|
+
@Scheduled('0 0 8 * * 1-5', 'America/New_York') // 8 AM EST, weekdays only
|
|
163
|
+
async businessHoursTask() {
|
|
164
|
+
console.log("Running during business hours...");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Extended lock configuration for long-running tasks
|
|
168
|
+
@Scheduled('0 0 3 * * *')
|
|
169
|
+
@RedLock('heavy-processing', {
|
|
170
|
+
lockTimeOut: 300000, // 5 minutes
|
|
171
|
+
maxRetries: 1, // Don't retry if another instance is running
|
|
172
|
+
retryDelayMs: 1000
|
|
173
|
+
})
|
|
174
|
+
async heavyProcessing() {
|
|
175
|
+
console.log("Running heavy processing task...");
|
|
176
|
+
// Long-running task with extended timeout
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Custom lock name with timestamp
|
|
180
|
+
@RedLock() // Auto-generates unique lock name
|
|
181
|
+
async dynamicTask() {
|
|
182
|
+
console.log("Running with auto-generated lock name...");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Configuration Priority System
|
|
188
|
+
|
|
189
|
+
The library uses a three-tier priority system for configuration:
|
|
190
|
+
|
|
191
|
+
1. **Method-level** (highest priority)
|
|
192
|
+
2. **Global plugin config**
|
|
193
|
+
3. **Built-in defaults** (lowest priority)
|
|
194
|
+
|
|
195
|
+
### Timezone Resolution
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Priority: Method > Global > Default ('Asia/Beijing')
|
|
199
|
+
|
|
200
|
+
@Scheduled('0 0 12 * * *', 'UTC') // Uses UTC (method-level)
|
|
201
|
+
async task1() { ... }
|
|
202
|
+
|
|
203
|
+
@Scheduled('0 0 12 * * *') // Uses global timezone from plugin config
|
|
204
|
+
async task2() { ... }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### RedLock Options Resolution
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Global config in plugin.ts
|
|
211
|
+
Scheduled: {
|
|
212
|
+
lockTimeOut: 10000,
|
|
213
|
+
maxRetries: 3
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Method-level override
|
|
217
|
+
@RedLock('my-lock', {
|
|
218
|
+
lockTimeOut: 20000 // Overrides global, keeps maxRetries: 3
|
|
219
|
+
})
|
|
220
|
+
async task() { ... }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Monitoring and Health Checks
|
|
224
|
+
|
|
225
|
+
### Health Status Check
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { RedLocker } from "koatty_schedule";
|
|
229
|
+
|
|
230
|
+
export class MonitoringService {
|
|
231
|
+
|
|
232
|
+
@Scheduled('*/30 * * * * *') // Every 30 seconds
|
|
233
|
+
async checkSystemHealth() {
|
|
234
|
+
const redLocker = RedLocker.getInstance();
|
|
235
|
+
const health = await redLocker.healthCheck();
|
|
236
|
+
|
|
237
|
+
console.log('RedLock Status:', health.status);
|
|
238
|
+
console.log('Connection Details:', health.details);
|
|
239
|
+
|
|
240
|
+
if (health.status === 'unhealthy') {
|
|
241
|
+
// Send alert or take corrective action
|
|
242
|
+
console.error('RedLock is unhealthy!', health.details);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### Performance Monitoring
|
|
31
249
|
|
|
250
|
+
```typescript
|
|
251
|
+
export class PerformanceService {
|
|
252
|
+
|
|
253
|
+
@Scheduled('0 */15 * * * *')
|
|
254
|
+
async monitorPerformance() {
|
|
255
|
+
const redLocker = RedLocker.getInstance();
|
|
256
|
+
const config = redLocker.getConfig();
|
|
257
|
+
|
|
258
|
+
console.log('Current RedLock Configuration:', {
|
|
259
|
+
lockTimeOut: config.lockTimeOut,
|
|
260
|
+
retryCount: config.retryCount,
|
|
261
|
+
retryDelay: config.retryDelay
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
32
265
|
```
|
|
33
266
|
|
|
34
|
-
|
|
267
|
+
## Error Handling and Best Practices
|
|
35
268
|
|
|
36
|
-
|
|
37
|
-
import { Scheduled, SchedulerLock } from "koatty_schedule";
|
|
269
|
+
### Robust Error Handling
|
|
38
270
|
|
|
39
|
-
|
|
271
|
+
```typescript
|
|
272
|
+
export class RobustTaskService {
|
|
273
|
+
|
|
274
|
+
@Scheduled('0 */5 * * * *')
|
|
275
|
+
@RedLock('robust-task', { maxRetries: 2 })
|
|
276
|
+
async robustTask() {
|
|
277
|
+
try {
|
|
278
|
+
// Your business logic
|
|
279
|
+
await this.processData();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Handle business logic errors
|
|
282
|
+
console.error('Task failed:', error);
|
|
283
|
+
|
|
284
|
+
// Don't re-throw unless you want to stop the schedule
|
|
285
|
+
// The scheduler will continue with the next execution
|
|
286
|
+
}
|
|
287
|
+
}
|
|
40
288
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
289
|
+
private async processData() {
|
|
290
|
+
// Simulate work that might fail
|
|
291
|
+
if (Math.random() < 0.1) {
|
|
292
|
+
throw new Error('Random processing error');
|
|
45
293
|
}
|
|
294
|
+
console.log('Data processed successfully');
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Lock Extension for Long Tasks
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
export class LongRunningTaskService {
|
|
303
|
+
|
|
304
|
+
@Scheduled('0 0 1 * * *') // Daily at 1 AM
|
|
305
|
+
@RedLock('daily-backup', {
|
|
306
|
+
lockTimeOut: 60000, // 1 minute initial lock
|
|
307
|
+
maxRetries: 1 // Don't wait if another instance is running
|
|
308
|
+
})
|
|
309
|
+
async dailyBackup() {
|
|
310
|
+
console.log('Starting daily backup...');
|
|
311
|
+
|
|
312
|
+
// The lock will automatically extend up to 3 times (configurable)
|
|
313
|
+
// if the task takes longer than the initial timeout
|
|
314
|
+
await this.performLongBackup(); // May take several minutes
|
|
315
|
+
|
|
316
|
+
console.log('Daily backup completed');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private async performLongBackup() {
|
|
320
|
+
// Simulate long-running backup process
|
|
321
|
+
await new Promise(resolve => setTimeout(resolve, 150000)); // 2.5 minutes
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Cron Expression Examples
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
export class CronExamplesService {
|
|
330
|
+
|
|
331
|
+
@Scheduled('0 0 * * * *') // Every hour
|
|
332
|
+
async hourlyTask() { }
|
|
333
|
+
|
|
334
|
+
@Scheduled('0 */30 * * * *') // Every 30 minutes
|
|
335
|
+
async halfHourlyTask() { }
|
|
336
|
+
|
|
337
|
+
@Scheduled('0 0 9 * * 1-5') // 9 AM, Monday to Friday
|
|
338
|
+
async weekdayMorningTask() { }
|
|
339
|
+
|
|
340
|
+
@Scheduled('0 0 0 1 * *') // First day of every month
|
|
341
|
+
async monthlyTask() { }
|
|
342
|
+
|
|
343
|
+
@Scheduled('0 0 0 * * 0') // Every Sunday
|
|
344
|
+
async weeklyTask() { }
|
|
345
|
+
|
|
346
|
+
@Scheduled('*/10 * * * * *') // Every 10 seconds
|
|
347
|
+
async frequentTask() { }
|
|
46
348
|
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Troubleshooting
|
|
352
|
+
|
|
353
|
+
### Common Issues
|
|
354
|
+
|
|
355
|
+
1. **Redis Connection Failed**
|
|
356
|
+
```typescript
|
|
357
|
+
// Check your CacheStore configuration
|
|
358
|
+
"CacheStore": {
|
|
359
|
+
type: "redis", // Must be "redis" for distributed locking
|
|
360
|
+
host: '127.0.0.1',
|
|
361
|
+
port: 6379,
|
|
362
|
+
// ... other settings
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
2. **Lock Acquisition Timeout**
|
|
367
|
+
```typescript
|
|
368
|
+
// Increase timeout or reduce retries
|
|
369
|
+
@RedLock('my-lock', {
|
|
370
|
+
lockTimeOut: 30000, // Increase timeout
|
|
371
|
+
maxRetries: 1 // Reduce retries to fail fast
|
|
372
|
+
})
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
3. **Timezone Issues**
|
|
376
|
+
```typescript
|
|
377
|
+
// Always specify timezone explicitly for critical tasks
|
|
378
|
+
@Scheduled('0 0 9 * * *', 'America/New_York')
|
|
379
|
+
async criticalMorningTask() { }
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Debug Mode
|
|
383
|
+
|
|
384
|
+
Enable debug logging by setting environment variable:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
DEBUG=koatty_schedule* npm start
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
## API Reference
|
|
391
|
+
|
|
392
|
+
### Decorators
|
|
393
|
+
|
|
394
|
+
#### `@Scheduled(cron: string, timezone?: string)`
|
|
395
|
+
- `cron`: Cron expression (6-part format with seconds)
|
|
396
|
+
- `timezone`: Optional timezone override
|
|
397
|
+
|
|
398
|
+
#### `@RedLock(lockName?: string, options?: RedLockMethodOptions)`
|
|
399
|
+
- `lockName`: Unique lock identifier (auto-generated if not provided)
|
|
400
|
+
- `options`: Method-level lock configuration
|
|
401
|
+
|
|
402
|
+
### Configuration Types
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
interface ScheduledOptions {
|
|
406
|
+
timezone?: string;
|
|
407
|
+
lockTimeOut?: number;
|
|
408
|
+
maxRetries?: number;
|
|
409
|
+
retryDelayMs?: number;
|
|
410
|
+
clockDriftFactor?: number;
|
|
411
|
+
redisConfig?: RedisConfig;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface RedLockMethodOptions {
|
|
415
|
+
lockTimeOut?: number;
|
|
416
|
+
maxRetries?: number;
|
|
417
|
+
retryDelayMs?: number;
|
|
418
|
+
clockDriftFactor?: number;
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
## Version Compatibility
|
|
423
|
+
|
|
424
|
+
- **Koatty**: >= 2.0.0
|
|
425
|
+
- **Node.js**: >= 14.0.0
|
|
426
|
+
- **Redis**: >= 3.0.0
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
[BSD-3-Clause](LICENSE)
|
|
431
|
+
|
|
432
|
+
## Contributing
|
|
433
|
+
|
|
434
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
435
|
+
|
|
436
|
+
## Support
|
|
47
437
|
|
|
48
|
-
|
|
438
|
+
- 📚 [Documentation](https://koatty.js.org/)
|
|
439
|
+
- 🐛 [Issues](https://github.com/koattyjs/koatty_schedule/issues)
|
|
440
|
+
- 💬 [Discussions](https://github.com/koattyjs/koatty_schedule/discussions)
|