@namnguyen.repl.it/nn-scheduler 1.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/Readme.md ADDED
@@ -0,0 +1,58 @@
1
+
2
+ ## Example usage with timeout handling in actions
3
+ class MyAction {
4
+ static longRunningAction() {
5
+ return async () => {
6
+ try {
7
+ //console.log('Long running action started');
8
+ await new Promise((resolve, reject) => {
9
+ const timeout = setTimeout(() => reject(new Error('Action timed out')), 90 * 1000);
10
+ setTimeout(() => {
11
+ clearTimeout(timeout);
12
+ resolve();
13
+ }, 120 * 1000);
14
+ });
15
+ //console.log('Long running action completed');
16
+ } catch (error) {
17
+ console.error(`Long running action failed: ${error}`);
18
+ throw error;
19
+ }
20
+ };
21
+ }
22
+
23
+ static failingAction() {
24
+ return async () => {
25
+ try {
26
+ //console.log('Failing action started');
27
+ await new Promise((_, reject) => setTimeout(() => reject(new Error('Action failed')), 1000));
28
+ //console.log('Failing action completed');
29
+ } catch (error) {
30
+ console.error(`Failing action failed: ${error}`);
31
+ throw error;
32
+ }
33
+ };
34
+ }
35
+ }
36
+
37
+ const scheduler = new Scheduler({
38
+ logHandler: (msg) => //console.log(msg)
39
+ });
40
+
41
+ scheduler.schedule([
42
+ { time: '07:15', action: MyAction.longRunningAction(), weekDays: ['*'] },
43
+ { time: '09:15', action: MyAction.failingAction(), weekDays: [6, 1, 2, 3] },
44
+ ]);
45
+
46
+ scheduler.interval([
47
+ { interval: 60 * 1000, action: MyAction.longRunningAction(), weekDays: ['*'] },
48
+ { interval: 60 * 1000, action: MyAction.failingAction(), weekDays: [2, 3, 4] },
49
+ ]);
50
+
51
+
52
+ Using PM2 to manage tasks:
53
+ pm2 start scheduler.js --name scheduler
54
+ pm2 list
55
+ pm2 logs scheduler
56
+ pm2 stop scheduler
57
+ pm2 restart scheduler
58
+ pm2 delete scheduler
@@ -0,0 +1,2 @@
1
+ export * from './types/SchedulerType';
2
+ export * from './scheduler';
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types/SchedulerType"), exports);
18
+ __exportStar(require("./scheduler"), exports);
@@ -0,0 +1,55 @@
1
+ import { IntervalTask, ScheduledTask, SchedulerConfig } from './types/SchedulerType';
2
+ /**
3
+ * Scheduler class for managing background tasks in Node.js, designed to run under PM2.
4
+ * Supports two types of tasks:
5
+ * 1. Scheduled tasks that run at specific times on specified weekdays or matchPattern.
6
+ * 2. Interval tasks that run every n milliseconds if weekdays or matchPattern conditions are met.
7
+ * Runs indefinitely until stopped via PM2 commands (e.g., `pm2 stop scheduler`).
8
+ * Long-running or failing tasks are handled by the actions themselves.
9
+ */
10
+ export declare class Scheduler {
11
+ private logHandler;
12
+ private cronJobs;
13
+ private intervalIds;
14
+ /**
15
+ * Creates a new Scheduler instance
16
+ * @param config Configuration object with optional logHandler
17
+ */
18
+ constructor(config?: SchedulerConfig);
19
+ /**
20
+ * Converts a time string and weekdays into a cron expression
21
+ * @param time Time in "HH:MM" format
22
+ * @param weekDays Array of weekdays (0-6 or '*')
23
+ * @returns Cron expression string (e.g., "15 7 * * *")
24
+ * @throws Error if time format is invalid
25
+ */
26
+ private timeToCron;
27
+ private patternToCron;
28
+ /**
29
+ * Checks if the current day matches the specified weekdays
30
+ * @param weekDays Array of weekdays (0-6 or '*')
31
+ * @returns True if today is in the weekday list or '*' is included
32
+ */
33
+ private isMatchingWeekDay;
34
+ private isMatchingPattern;
35
+ /**
36
+ * Schedules tasks to run at specific times on specified weekdays or matchPattern
37
+ * @param tasks Array of scheduled tasks to execute
38
+ */
39
+ schedule(tasks: ScheduledTask[]): void;
40
+ /**
41
+ * Schedules tasks to run every n milliseconds if weekdays or matchPattern conditions are met
42
+ * @param tasks Array of interval tasks to execute
43
+ */
44
+ interval(tasks: IntervalTask[]): void;
45
+ /**
46
+ * Stops all scheduled and interval tasks
47
+ * Called when PM2 sends a shutdown signal
48
+ */
49
+ stop(): void;
50
+ /**
51
+ * Sets up integration with PM2 by handling shutdown signals
52
+ * PM2 sends SIGINT when stopping the process
53
+ */
54
+ private setupPM2;
55
+ }
@@ -0,0 +1,318 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.Scheduler = void 0;
46
+ const cron = __importStar(require("node-cron"));
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ /** Writes log messages to a file named by date (e.g., "2025-03-24.log") in the current directory */
50
+ const defaultLogHandler = (message) => {
51
+ const date = new Date().toISOString().split('T')[0];
52
+ const logFile = path.join(process.cwd(), `${date}.log`);
53
+ const logMessage = `${new Date().toISOString()} - ${message}\n`;
54
+ fs.appendFileSync(logFile, logMessage, { encoding: 'utf8' });
55
+ };
56
+ /**
57
+ * Scheduler class for managing background tasks in Node.js, designed to run under PM2.
58
+ * Supports two types of tasks:
59
+ * 1. Scheduled tasks that run at specific times on specified weekdays or matchPattern.
60
+ * 2. Interval tasks that run every n milliseconds if weekdays or matchPattern conditions are met.
61
+ * Runs indefinitely until stopped via PM2 commands (e.g., `pm2 stop scheduler`).
62
+ * Long-running or failing tasks are handled by the actions themselves.
63
+ */
64
+ class Scheduler {
65
+ /**
66
+ * Creates a new Scheduler instance
67
+ * @param config Configuration object with optional logHandler
68
+ */
69
+ constructor(config = {}) {
70
+ this.cronJobs = [];
71
+ this.intervalIds = [];
72
+ this.logHandler = config.logHandler || defaultLogHandler;
73
+ this.setupPM2();
74
+ }
75
+ /**
76
+ * Converts a time string and weekdays into a cron expression
77
+ * @param time Time in "HH:MM" format
78
+ * @param weekDays Array of weekdays (0-6 or '*')
79
+ * @returns Cron expression string (e.g., "15 7 * * *")
80
+ * @throws Error if time format is invalid
81
+ */
82
+ timeToCron(time, weekDays) {
83
+ const [hours, minutes] = time.split(':').map(Number);
84
+ if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
85
+ throw new Error(`Invalid time format: ${time}`);
86
+ }
87
+ const weekDayStr = weekDays.includes('*') ? '*' : weekDays.join(',');
88
+ return `${minutes} ${hours} * * ${weekDayStr}`;
89
+ }
90
+ // Converts a time string and matchPattern into a cron expression
91
+ // @param time Time in "HH:MM" format
92
+ // @param matchPattern Pattern (e.g., '2025/**/**', '2025/06/01')
93
+ // @returns Cron expression string
94
+ // @throws Error if time or pattern is invalid
95
+ patternToCron(time, matchPattern) {
96
+ const [hours, minutes] = time.split(':').map(Number);
97
+ if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
98
+ throw new Error(`Invalid time format: ${time}`);
99
+ }
100
+ // Validate pattern format (yyyy/mm/dd with ** wildcards)
101
+ if (!matchPattern.match(/^\d{4}\/(\d{2}|\*\*)\/(\d{2}|\*\*)$/)) {
102
+ throw new Error(`Invalid pattern format: ${matchPattern}`);
103
+ }
104
+ const [year, month, day] = matchPattern.split('/');
105
+ const yearNum = parseInt(year, 10);
106
+ const monthNum = month === '**' ? '*' : parseInt(month, 10);
107
+ const dayNum = day === '**' ? '*' : parseInt(day, 10);
108
+ // Validate year (basic range check, assuming 1970-2099 for cron compatibility)
109
+ if (isNaN(yearNum) || yearNum < new Date().getFullYear() || yearNum > 3000) {
110
+ throw new Error(`Invalid year in pattern: ${matchPattern}`);
111
+ }
112
+ // Validate month//
113
+ //@ts-ignore
114
+ if (month !== '**' && (isNaN(monthNum) || monthNum < 1 || monthNum > 12)) {
115
+ throw new Error(`Invalid month in pattern: ${matchPattern}`);
116
+ }
117
+ // Validate day
118
+ //@ts-ignore
119
+ if (day !== '**' && (isNaN(dayNum) || dayNum < 1 || dayNum > 31)) {
120
+ throw new Error(`Invalid day in pattern: ${matchPattern}`);
121
+ }
122
+ // Note: Cron doesn't support year directly, so we rely on pattern matching in schedule
123
+ // For simplicity, we generate a cron expression for day/month, assuming year is handled externally
124
+ return `${minutes} ${hours} ${dayNum} ${monthNum} *`;
125
+ }
126
+ /**
127
+ * Checks if the current day matches the specified weekdays
128
+ * @param weekDays Array of weekdays (0-6 or '*')
129
+ * @returns True if today is in the weekday list or '*' is included
130
+ */
131
+ isMatchingWeekDay(weekDays) {
132
+ const currentDay = new Date().getDay();
133
+ return weekDays.includes('*') || weekDays.includes(currentDay);
134
+ }
135
+ //Checks if the current date matches the specified match signalling
136
+ //@param matchPattern Pattern (e.g., '2025/**/** ', '2025 /06 /01')
137
+ //@returns True if today matches the pattern
138
+ isMatchingPattern(matchPattern) {
139
+ const today = new Date();
140
+ const day = today.getDate();
141
+ const month = today.getMonth() + 1; // getMonth() is 0-based
142
+ const year = today.getFullYear();
143
+ // Validate pattern format (yyyy/mm/dd with ** wildcards)
144
+ if (!matchPattern.match(/^\d{4}\/(\d{2}|\*\*)\/(\d{2}|\*\*)$/)) {
145
+ return false;
146
+ }
147
+ const [patternYear, patternMonth, patternDay] = matchPattern.split('/');
148
+ const yearNum = parseInt(patternYear, 10);
149
+ const monthNum = patternMonth === '**' ? month : parseInt(patternMonth, 10);
150
+ const dayNum = patternDay === '**' ? day : parseInt(patternDay, 10);
151
+ // Validate parsed values
152
+ if (isNaN(yearNum) || (patternMonth !== '**' && isNaN(monthNum)) || (patternDay !== '**' && isNaN(dayNum))) {
153
+ return false;
154
+ }
155
+ // Check year match
156
+ if (yearNum !== year) {
157
+ return false;
158
+ }
159
+ // Check month match
160
+ if (patternMonth !== '**' && monthNum !== month) {
161
+ return false;
162
+ }
163
+ // Check day match
164
+ if (patternDay !== '**' && dayNum !== day) {
165
+ return false;
166
+ }
167
+ // Validate date (e.g., avoid Feb 30)
168
+ if (patternMonth !== '**' && patternDay !== '**') {
169
+ const date = new Date(yearNum, monthNum - 1, dayNum);
170
+ if (date.getFullYear() !== yearNum || date.getMonth() + 1 !== monthNum || date.getDate() !== dayNum) {
171
+ return false;
172
+ }
173
+ }
174
+ return true;
175
+ }
176
+ /**
177
+ * Schedules tasks to run at specific times on specified weekdays or matchPattern
178
+ * @param tasks Array of scheduled tasks to execute
179
+ */
180
+ schedule(tasks) {
181
+ for (const task of tasks) {
182
+ if (!task.time) {
183
+ this.logHandler('No valid time provided for scheduled task, skipping');
184
+ continue;
185
+ }
186
+ // Weekdays trigger
187
+ try {
188
+ const cronExpression = this.timeToCron(task.time, task.weekDays);
189
+ if (!cron.validate(cronExpression)) {
190
+ this.logHandler(`Invalid cron expression for task at ${task.time}: ${cronExpression}`);
191
+ continue;
192
+ }
193
+ const cronJob = cron.schedule(cronExpression, () => __awaiter(this, void 0, void 0, function* () {
194
+ try {
195
+ this.logHandler(`Executing scheduled task at ${task.time} (weekdays trigger)`);
196
+ yield task.action();
197
+ this.logHandler(`Scheduled task at ${task.time} (weekdays trigger) completed`);
198
+ }
199
+ catch (error) {
200
+ this.logHandler(`Error in scheduled task at ${task.time} (weekdays trigger): ${error}`);
201
+ }
202
+ }));
203
+ this.cronJobs.push(cronJob);
204
+ this.logHandler(`Scheduled task at ${task.time} with weekdays ${task.weekDays.join(',')}`);
205
+ }
206
+ catch (error) {
207
+ this.logHandler(`Error processing task at ${task.time} for weekdays: ${error}`);
208
+ }
209
+ // MatchPattern trigger
210
+ if (task.matchPattern) {
211
+ for (const pattern of task.matchPattern) {
212
+ try {
213
+ const cronExpression = this.patternToCron(task.time, pattern);
214
+ if (!cron.validate(cronExpression)) {
215
+ this.logHandler(`Invalid cron expression for task at ${task.time}, pattern ${pattern}: ${cronExpression}`);
216
+ continue;
217
+ }
218
+ const cronJob = cron.schedule(cronExpression, () => __awaiter(this, void 0, void 0, function* () {
219
+ try {
220
+ this.logHandler(`Executing scheduled task at ${task.time} (pattern ${pattern} trigger)`);
221
+ yield task.action();
222
+ this.logHandler(`Scheduled task at ${task.time} (pattern ${pattern} trigger) completed`);
223
+ }
224
+ catch (error) {
225
+ this.logHandler(`Error in scheduled task at ${task.time} (pattern ${pattern} trigger): ${error}`);
226
+ }
227
+ }));
228
+ this.cronJobs.push(cronJob);
229
+ this.logHandler(`Scheduled task at ${task.time} with pattern ${pattern}`);
230
+ }
231
+ catch (error) {
232
+ this.logHandler(`Error processing task at ${task.time}, pattern ${pattern}: ${error}`);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ /**
239
+ * Schedules tasks to run every n milliseconds if weekdays or matchPattern conditions are met
240
+ * @param tasks Array of interval tasks to execute
241
+ */
242
+ interval(tasks) {
243
+ for (const task of tasks) {
244
+ if (task.interval <= 0 || !Number.isInteger(task.interval)) {
245
+ this.logHandler(`Invalid interval: ${task.interval}`);
246
+ continue;
247
+ }
248
+ const intervalId = setInterval(() => __awaiter(this, void 0, void 0, function* () {
249
+ let triggered = false;
250
+ // Check weekdays
251
+ if (this.isMatchingWeekDay(task.weekDays)) {
252
+ triggered = true;
253
+ try {
254
+ this.logHandler(`Executing interval task every ${task.interval / 1000} seconds (weekdays trigger)`);
255
+ yield task.action();
256
+ this.logHandler(`Interval task every ${task.interval / 1000} seconds (weekdays trigger) completed`);
257
+ }
258
+ catch (error) {
259
+ this.logHandler(`Error in interval task every ${task.interval / 1000} seconds (weekdays trigger): ${error}`);
260
+ }
261
+ }
262
+ // Check matchPattern
263
+ if (task.matchPattern) {
264
+ for (const pattern of task.matchPattern) {
265
+ if (this.isMatchingPattern(pattern)) {
266
+ triggered = true;
267
+ try {
268
+ this.logHandler(`Executing interval task every ${task.interval / 1000} seconds (pattern ${pattern} trigger)`);
269
+ yield task.action();
270
+ this.logHandler(`Interval task every ${task.interval / 1000} seconds (pattern ${pattern} trigger) completed`);
271
+ }
272
+ catch (error) {
273
+ this.logHandler(`Error in interval task every ${task.interval / 1000} seconds (pattern ${pattern} trigger): ${error}`);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ if (!triggered) {
279
+ this.logHandler(`Interval task every ${task.interval / 1000} seconds skipped (no matching weekdays or patterns)`);
280
+ }
281
+ }), task.interval);
282
+ this.intervalIds.push(intervalId);
283
+ this.logHandler(`Started interval task every ${task.interval / 1000} seconds with weekdays ${task.weekDays.join(',')} and patterns ${task.matchPattern ? task.matchPattern.join(',') : 'none'}`);
284
+ }
285
+ }
286
+ /**
287
+ * Stops all scheduled and interval tasks
288
+ * Called when PM2 sends a shutdown signal
289
+ */
290
+ stop() {
291
+ this.cronJobs.forEach(job => job.stop());
292
+ this.cronJobs = [];
293
+ this.logHandler('All scheduled tasks stopped');
294
+ this.intervalIds.forEach(id => clearInterval(id));
295
+ this.intervalIds = [];
296
+ this.logHandler('All interval tasks stopped');
297
+ }
298
+ /**
299
+ * Sets up integration with PM2 by handling shutdown signals
300
+ * PM2 sends SIGINT when stopping the process
301
+ */
302
+ setupPM2() {
303
+ process.on('SIGINT', () => {
304
+ this.logHandler('Received SIGINT from PM2, stopping scheduler...');
305
+ this.stop();
306
+ process.exit(0);
307
+ });
308
+ process.on('message', (msg) => {
309
+ if (msg === 'shutdown') {
310
+ this.logHandler('Received shutdown message from PM2, stopping scheduler...');
311
+ this.stop();
312
+ process.exit(0);
313
+ }
314
+ });
315
+ this.logHandler(`Scheduler started with PID ${process.pid}. Manage with PM2 commands (e.g., 'pm2 stop scheduler').`);
316
+ }
317
+ }
318
+ exports.Scheduler = Scheduler;
@@ -0,0 +1,34 @@
1
+ /** A task is a function that can be executed synchronously or asynchronously */
2
+ export type Task = () => void | Promise<void>;
3
+ /** A log handler is a function that processes log messages */
4
+ export type LogHandler = (message: string) => void;
5
+ /**
6
+ * Configuration options for the Scheduler
7
+ */
8
+ export interface SchedulerConfig {
9
+ /** Optional custom log handler. If not provided, logs are written to daily files (e.g., "2025-03-24.log") */
10
+ logHandler?: LogHandler;
11
+ }
12
+ /**
13
+ * Definition of a scheduled task with time, matchPattern, and weekday constraints
14
+ */
15
+ export interface ScheduledTask {
16
+ /** Time to run the task in "HH:MM" format (24-hour, e.g., "07:15") */
17
+ time: string;
18
+ matchPattern?: string[];
19
+ /** The task to execute */
20
+ action: Task;
21
+ /** Array of weekdays to run on (0-6 for Sun-Sat, or '*' for all days) */
22
+ weekDays: Array<number | '*'>;
23
+ }
24
+ /**
25
+ * Definition of an interval task with time, matchPattern, and weekday constraints
26
+ */
27
+ export interface IntervalTask {
28
+ interval: number;
29
+ matchPattern?: string[];
30
+ /** The task to execute */
31
+ action: Task;
32
+ /** Array of weekdays to run on (0-6 for Sun-Sat, or '*' for all days) */
33
+ weekDays: Array<number | '*'>;
34
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@namnguyen.repl.it/nn-scheduler",
3
+ "version": "1.0.0",
4
+ "description": "A scheduler module for NodeJS",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build",
10
+ "test": "jest"
11
+ },
12
+ "keywords": [],
13
+ "dependencies": {
14
+ "node-cron": "^3.0.3"
15
+ },
16
+ "devDependencies": {
17
+ "@babel/core": "^7.26.10",
18
+ "@babel/preset-env": "^7.26.9",
19
+ "@babel/preset-typescript": "^7.26.0",
20
+ "@types/jest": "^29.5.14",
21
+ "@types/node": "^20.17.30",
22
+ "@types/node-cron": "^3.0.11",
23
+ "babel-jest": "^29.7.0",
24
+ "jest": "^29.7.0",
25
+ "ts-jest": "^29.2.6",
26
+ "ts-node": "^10.9.2",
27
+ "typescript": "^5.8.2"
28
+ },
29
+ "files": [
30
+ "dist/**/*"
31
+ ],
32
+ "author": "namnguyen.repl.it@gmail.com",
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/NamReplIT/nn-scheduler/issues"
36
+ },
37
+ "homepage": "https://github.com/NamReplIT/nn-scheduler#readme",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }