bro-framework 2.4.3 → 2.4.5
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/bin/bro.js +15 -7
- package/package.json +1 -1
- package/src/locale.js +1 -1
- package/src/next.d.ts +10 -1
- package/src/next.js +181 -65
- package/src/router.js +0 -1
- package/src/server.js +7 -2
package/bin/bro.js
CHANGED
|
@@ -207,7 +207,8 @@ async function bootstrap() {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
const localeDir = globalConfig.locale?.directory || path.join(cwd, 'locale');
|
|
210
|
-
const
|
|
210
|
+
const tasksDir = path.join(cwd, 'tasks');
|
|
211
|
+
const { app, server, routes: initialRoutes, reload, reloadLocale, reloadTasks, io, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
211
212
|
const port = globalConfig.port;
|
|
212
213
|
|
|
213
214
|
let currentRoutes = initialRoutes;
|
|
@@ -234,28 +235,35 @@ async function bootstrap() {
|
|
|
234
235
|
|
|
235
236
|
printCurrentRoutes(currentRoutes);
|
|
236
237
|
|
|
237
|
-
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts}';
|
|
238
|
-
const watcher = chokidar.watch([routesDir, localeGlob], { ignoreInitial: true });
|
|
238
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts,json}';
|
|
239
|
+
const watcher = chokidar.watch([routesDir, localeGlob, tasksDir], { ignoreInitial: true });
|
|
239
240
|
|
|
240
241
|
watcher.on('all', async (event, filepath) => {
|
|
241
|
-
const
|
|
242
|
-
if (!
|
|
242
|
+
const isValidFile = filepath.match(/\.(js|ts|mjs|json)$/);
|
|
243
|
+
if (!isValidFile) return;
|
|
243
244
|
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
244
245
|
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
246
|
+
|
|
247
|
+
const relTask = path.relative(path.resolve(tasksDir), filepath);
|
|
248
|
+
const isTaskFile = !relTask.startsWith('..') && !path.isAbsolute(relTask);
|
|
245
249
|
|
|
246
250
|
try {
|
|
247
251
|
const reloadStartTime = performance.now();
|
|
248
252
|
if (isLocaleFile) {
|
|
249
253
|
await reloadLocale();
|
|
254
|
+
} else if (isTaskFile) {
|
|
255
|
+
await reloadTasks();
|
|
250
256
|
} else {
|
|
251
257
|
currentRoutes = await reload();
|
|
252
258
|
}
|
|
253
259
|
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
254
260
|
|
|
255
|
-
|
|
261
|
+
const fileType = isTaskFile ? 'Task' : (isLocaleFile ? 'Locale' : 'Route');
|
|
262
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, fileType);
|
|
256
263
|
printCurrentRoutes(currentRoutes);
|
|
257
264
|
} catch (err) {
|
|
258
|
-
|
|
265
|
+
const fileType = isTaskFile ? 'tasks' : (isLocaleFile ? 'locale' : 'routes');
|
|
266
|
+
console.error(`\n ✗ Error hot-reloading ${fileType}:`, err);
|
|
259
267
|
}
|
|
260
268
|
});
|
|
261
269
|
}
|
package/package.json
CHANGED
package/src/locale.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { pathToFileURL } from 'url';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
|
|
6
|
-
const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts']);
|
|
6
|
+
const LOCALE_EXTENSIONS = new Set(['.js', '.mjs', '.ts', '.json']);
|
|
7
7
|
|
|
8
8
|
function localeFromFilename(fileName) {
|
|
9
9
|
return path.basename(fileName, path.extname(fileName));
|
package/src/next.d.ts
CHANGED
|
@@ -12,11 +12,15 @@ export interface UploadedFile {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface NextBroGlobalConfig<TDb = any> {
|
|
15
|
-
|
|
15
|
+
env?: ZodTypeAny;
|
|
16
|
+
locales?: Record<string, any>;
|
|
17
|
+
defaultLocale?: string;
|
|
16
18
|
redisUrl?: string;
|
|
19
|
+
rateLimit?: { windowMs: number; max: number; };
|
|
17
20
|
auth?: {
|
|
18
21
|
jwtSecret?: string;
|
|
19
22
|
apiKey?: string | string[];
|
|
23
|
+
expiresIn?: string | number;
|
|
20
24
|
};
|
|
21
25
|
db?: TDb | Promise<TDb> | (() => TDb | Promise<TDb>) | { init: () => TDb | Promise<TDb> };
|
|
22
26
|
}
|
|
@@ -44,6 +48,11 @@ export interface NextRouteConfig<TBody = any, TQuery = any, TParams = any, TDb =
|
|
|
44
48
|
body?: TBody;
|
|
45
49
|
query?: TQuery;
|
|
46
50
|
params?: TParams;
|
|
51
|
+
cache?: number;
|
|
52
|
+
rateLimit?: { windowMs: number; max: number; } | false;
|
|
53
|
+
response?: ZodTypeAny;
|
|
54
|
+
summary?: string;
|
|
55
|
+
upload?: any;
|
|
47
56
|
handler: (ctx: NextRouteContext<TBody, TQuery, TParams, TDb>) => Promise<any> | any;
|
|
48
57
|
}
|
|
49
58
|
|
package/src/next.js
CHANGED
|
@@ -3,13 +3,19 @@ import { z } from 'zod';
|
|
|
3
3
|
import { verifyJwt, signJwt } from './auth.js';
|
|
4
4
|
import { createClient } from 'redis';
|
|
5
5
|
|
|
6
|
+
export { z };
|
|
7
|
+
|
|
6
8
|
export function createBro(globalConfig = {}) {
|
|
7
9
|
let isInitialized = false;
|
|
8
10
|
let initPromise = null;
|
|
9
11
|
|
|
10
12
|
let globalDb = null;
|
|
11
|
-
let globalRedis = null;
|
|
12
13
|
let globalLocale = null;
|
|
14
|
+
|
|
15
|
+
const globalStore = globalThis;
|
|
16
|
+
globalStore.__broRedis = globalStore.__broRedis || null;
|
|
17
|
+
globalStore.__broMemoryCache = globalStore.__broMemoryCache || new Map();
|
|
18
|
+
globalStore.__broMemoryRateLimit = globalStore.__broMemoryRateLimit || new Map();
|
|
13
19
|
|
|
14
20
|
async function ensureInitialized() {
|
|
15
21
|
if (isInitialized) return;
|
|
@@ -17,17 +23,15 @@ export function createBro(globalConfig = {}) {
|
|
|
17
23
|
|
|
18
24
|
initPromise = (async () => {
|
|
19
25
|
try {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
};
|
|
26
|
+
if (globalConfig.env) {
|
|
27
|
+
try {
|
|
28
|
+
globalConfig.env.parse(process.env);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error('[bro.js/next] Environment Validation Error:', err);
|
|
31
|
+
throw err;
|
|
32
|
+
}
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
// DB
|
|
31
35
|
if (typeof globalConfig.db === 'function') {
|
|
32
36
|
globalDb = await globalConfig.db();
|
|
33
37
|
} else if (globalConfig.db && typeof globalConfig.db.init === 'function') {
|
|
@@ -37,12 +41,11 @@ export function createBro(globalConfig = {}) {
|
|
|
37
41
|
if (globalDb instanceof Promise) globalDb = await globalDb;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
|
-
// Redis
|
|
41
44
|
if (globalConfig.redisUrl) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
await
|
|
45
|
+
globalStore.__broRedis = createClient({ url: globalConfig.redisUrl });
|
|
46
|
+
globalStore.__broRedis.on('error', (err) => console.error('[bro.js/next] Redis Error:', err));
|
|
47
|
+
if (!globalStore.__broRedis.isOpen) {
|
|
48
|
+
await globalStore.__broRedis.connect().catch(err => {
|
|
46
49
|
if (!err.message.includes('already connecting') && !err.message.includes('already connected')) {
|
|
47
50
|
throw err;
|
|
48
51
|
}
|
|
@@ -60,6 +63,45 @@ export function createBro(globalConfig = {}) {
|
|
|
60
63
|
return initPromise;
|
|
61
64
|
}
|
|
62
65
|
|
|
66
|
+
const resolveLocale = (headers) => {
|
|
67
|
+
const acceptLanguage = headers['accept-language'] || '';
|
|
68
|
+
const preferredLanguages = acceptLanguage
|
|
69
|
+
.split(',')
|
|
70
|
+
.map(lang => lang.split(';')[0].trim().toLowerCase())
|
|
71
|
+
.filter(lang => lang);
|
|
72
|
+
|
|
73
|
+
const configuredLocales = Object.keys(globalConfig.locales || {});
|
|
74
|
+
|
|
75
|
+
for (const lang of preferredLanguages) {
|
|
76
|
+
if (configuredLocales.includes(lang)) {
|
|
77
|
+
return lang;
|
|
78
|
+
}
|
|
79
|
+
const baseLang = lang.split('-')[0];
|
|
80
|
+
if (configuredLocales.includes(baseLang)) {
|
|
81
|
+
return baseLang;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return globalConfig.defaultLocale || 'en';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const translate = (locale, key, values = {}) => {
|
|
89
|
+
let messages = globalConfig.locales?.[locale] || globalConfig.locales?.[globalConfig.defaultLocale || 'en'];
|
|
90
|
+
if (!messages) return key;
|
|
91
|
+
|
|
92
|
+
// Handle Webpack / ES module JSON interop where the object is under .default
|
|
93
|
+
if (messages.default && typeof messages.default === 'object') {
|
|
94
|
+
messages = messages.default;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const message = key.split('.').reduce((acc, part) => acc && acc[part], messages);
|
|
98
|
+
if (!message || typeof message !== 'string') return key;
|
|
99
|
+
|
|
100
|
+
return message.replace(/\{(\w+)\}/g, (_, name) => {
|
|
101
|
+
return values[name] !== undefined ? String(values[name]) : `{${name}}`;
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
|
|
63
105
|
const errorHelper = (status, message) => {
|
|
64
106
|
const err = new Error(message);
|
|
65
107
|
err.status = status;
|
|
@@ -71,54 +113,11 @@ export function createBro(globalConfig = {}) {
|
|
|
71
113
|
try {
|
|
72
114
|
await ensureInitialized();
|
|
73
115
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
const url = new URL(req.url);
|
|
77
|
-
const rawQuery = Object.fromEntries(url.searchParams.entries());
|
|
78
|
-
|
|
79
|
-
let rawBody = {};
|
|
80
|
-
const parsedFiles = {};
|
|
81
|
-
let totalFiles = 0;
|
|
82
|
-
|
|
83
|
-
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
84
|
-
const contentType = req.headers.get('content-type') || '';
|
|
85
|
-
|
|
86
|
-
if (contentType.includes('multipart/form-data')) {
|
|
87
|
-
try {
|
|
88
|
-
const formData = await req.formData();
|
|
89
|
-
for (const [key, value] of formData.entries()) {
|
|
90
|
-
if (value instanceof File || value instanceof Blob) {
|
|
91
|
-
if (!parsedFiles[key]) parsedFiles[key] = [];
|
|
92
|
-
parsedFiles[key].push(value);
|
|
93
|
-
totalFiles++;
|
|
94
|
-
} else {
|
|
95
|
-
rawBody[key] = value;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
} catch (err) {}
|
|
99
|
-
} else {
|
|
100
|
-
try {
|
|
101
|
-
rawBody = await req.json();
|
|
102
|
-
} catch (err) {}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let body, params, query;
|
|
107
|
-
try {
|
|
108
|
-
if (config.body) body = await config.body.parseAsync(rawBody);
|
|
109
|
-
if (config.params) params = await config.params.parseAsync(rawParams);
|
|
110
|
-
if (config.query) query = await config.query.parseAsync(rawQuery);
|
|
111
|
-
} catch (err) {
|
|
112
|
-
if (err instanceof z.ZodError) {
|
|
113
|
-
return NextResponse.json(
|
|
114
|
-
{ error: 'Validation Error', issues: err.issues },
|
|
115
|
-
{ status: 400 }
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
throw err;
|
|
119
|
-
}
|
|
116
|
+
const resolvedLocale = resolveLocale(Object.fromEntries(req.headers.entries()));
|
|
120
117
|
|
|
118
|
+
// Auth extraction early for Identity caching
|
|
121
119
|
let user = null;
|
|
120
|
+
let apiKeyUsed = null;
|
|
122
121
|
if (config.auth) {
|
|
123
122
|
if (config.auth === 'api-key') {
|
|
124
123
|
const apiKey = req.headers.get('x-api-key') || req.headers.get('authorization')?.replace('Bearer ', '');
|
|
@@ -133,6 +132,7 @@ export function createBro(globalConfig = {}) {
|
|
|
133
132
|
if (!isValid) {
|
|
134
133
|
return NextResponse.json({ error: 'Unauthorized', details: 'Missing or invalid API key' }, { status: 401 });
|
|
135
134
|
}
|
|
135
|
+
apiKeyUsed = apiKey;
|
|
136
136
|
} else {
|
|
137
137
|
const authHeader = req.headers.get('authorization');
|
|
138
138
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
@@ -161,8 +161,113 @@ export function createBro(globalConfig = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
// Rate Limiting
|
|
165
|
+
const activeRateLimit = config.rateLimit === false ? null : (config.rateLimit || globalConfig.rateLimit);
|
|
166
|
+
if (activeRateLimit) {
|
|
167
|
+
const ip = req.headers.get('x-forwarded-for') || 'ip';
|
|
168
|
+
const urlObj = new URL(req.url);
|
|
169
|
+
const rlKey = `rate-limit:${urlObj.pathname}:${ip}`;
|
|
170
|
+
|
|
171
|
+
if (globalStore.__broRedis) {
|
|
172
|
+
const currentCount = await globalStore.__broRedis.incr(rlKey);
|
|
173
|
+
if (currentCount === 1) {
|
|
174
|
+
await globalStore.__broRedis.expire(rlKey, Math.ceil(activeRateLimit.windowMs / 1000));
|
|
175
|
+
}
|
|
176
|
+
if (currentCount > activeRateLimit.max) {
|
|
177
|
+
return NextResponse.json({ error: 'Too Many Requests' }, { status: 429 });
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
const now = Date.now();
|
|
181
|
+
let record = globalStore.__broMemoryRateLimit.get(rlKey);
|
|
182
|
+
|
|
183
|
+
if (!record || now > record.expires) {
|
|
184
|
+
record = { count: 0, expires: now + activeRateLimit.windowMs };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
record.count++;
|
|
188
|
+
globalStore.__broMemoryRateLimit.set(rlKey, record);
|
|
189
|
+
|
|
190
|
+
if (record.count > activeRateLimit.max) {
|
|
191
|
+
return NextResponse.json({ error: 'Too Many Requests' }, { status: 429 });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Caching
|
|
197
|
+
let cacheKey = null;
|
|
198
|
+
if (config.cache && req.method === 'GET') {
|
|
199
|
+
const urlObj = new URL(req.url);
|
|
200
|
+
const identity = user ? (user.id || user.role || 'user') : (apiKeyUsed || 'anon');
|
|
201
|
+
cacheKey = `cache:${urlObj.pathname}${urlObj.search}:${resolvedLocale}:${identity}`;
|
|
202
|
+
|
|
203
|
+
if (globalStore.__broRedis) {
|
|
204
|
+
const cachedData = await globalStore.__broRedis.get(cacheKey);
|
|
205
|
+
if (cachedData) {
|
|
206
|
+
return NextResponse.json(JSON.parse(cachedData), { status: 200 });
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
const cached = globalStore.__broMemoryCache.get(cacheKey);
|
|
210
|
+
if (cached) {
|
|
211
|
+
if (Date.now() < cached.expires) {
|
|
212
|
+
return NextResponse.json(cached.data, { status: 200 });
|
|
213
|
+
} else {
|
|
214
|
+
globalStore.__broMemoryCache.delete(cacheKey);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const rawParams = context?.params ? await context.params : {};
|
|
221
|
+
const url = new URL(req.url);
|
|
222
|
+
const rawQuery = Object.fromEntries(url.searchParams.entries());
|
|
165
223
|
|
|
224
|
+
let rawBody = {};
|
|
225
|
+
const parsedFiles = {};
|
|
226
|
+
let totalFiles = 0;
|
|
227
|
+
|
|
228
|
+
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
229
|
+
const contentType = req.headers.get('content-type') || '';
|
|
230
|
+
|
|
231
|
+
if (contentType.includes('multipart/form-data')) {
|
|
232
|
+
try {
|
|
233
|
+
const formData = await req.formData();
|
|
234
|
+
for (const [key, value] of formData.entries()) {
|
|
235
|
+
if (value instanceof File || value instanceof Blob) {
|
|
236
|
+
if (!parsedFiles[key]) parsedFiles[key] = [];
|
|
237
|
+
parsedFiles[key].push(value);
|
|
238
|
+
totalFiles++;
|
|
239
|
+
} else {
|
|
240
|
+
rawBody[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {}
|
|
244
|
+
} else {
|
|
245
|
+
try {
|
|
246
|
+
rawBody = await req.json();
|
|
247
|
+
} catch (err) {}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let body, params, query;
|
|
252
|
+
try {
|
|
253
|
+
if (config.body) body = await config.body.parseAsync(rawBody);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid Request Body', details: err.issues }, { status: 400 });
|
|
256
|
+
throw err;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
if (config.params) params = await config.params.parseAsync(rawParams);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid URL Parameters', details: err.issues }, { status: 400 });
|
|
262
|
+
throw err;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
if (config.query) query = await config.query.parseAsync(rawQuery);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof z.ZodError) return NextResponse.json({ error: 'Invalid Query Parameters', details: err.issues }, { status: 400 });
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
|
|
166
271
|
let ctxFile = undefined;
|
|
167
272
|
let ctxFiles = undefined;
|
|
168
273
|
if (totalFiles === 1) {
|
|
@@ -177,7 +282,7 @@ export function createBro(globalConfig = {}) {
|
|
|
177
282
|
req,
|
|
178
283
|
env: process.env,
|
|
179
284
|
db: globalDb,
|
|
180
|
-
redis:
|
|
285
|
+
redis: globalStore.__broRedis,
|
|
181
286
|
io: { emit: () => console.warn('[bro.js/next] WebSockets require standard bro.js server.') },
|
|
182
287
|
body,
|
|
183
288
|
params,
|
|
@@ -185,13 +290,24 @@ export function createBro(globalConfig = {}) {
|
|
|
185
290
|
file: ctxFile,
|
|
186
291
|
files: ctxFiles,
|
|
187
292
|
locale: resolvedLocale,
|
|
188
|
-
t: (key, values) =>
|
|
293
|
+
t: (key, values) => translate(resolvedLocale, key, values),
|
|
189
294
|
user,
|
|
190
|
-
jwt: {
|
|
295
|
+
jwt: {
|
|
296
|
+
sign: (payload, opts) => signJwt(payload, globalConfig?.auth?.jwtSecret || process.env.JWT_SECRET, Object.assign({ expiresIn: globalConfig?.auth?.expiresIn || '1d' }, opts || {}))
|
|
297
|
+
},
|
|
191
298
|
error: errorHelper
|
|
192
299
|
};
|
|
193
300
|
|
|
194
301
|
const result = await config.handler(ctx);
|
|
302
|
+
|
|
303
|
+
if (cacheKey) {
|
|
304
|
+
if (globalStore.__broRedis) {
|
|
305
|
+
await globalStore.__broRedis.set(cacheKey, JSON.stringify(result), { EX: config.cache });
|
|
306
|
+
} else {
|
|
307
|
+
globalStore.__broMemoryCache.set(cacheKey, { data: result, expires: Date.now() + (config.cache * 1000) });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
195
311
|
return NextResponse.json(result, { status: 200 });
|
|
196
312
|
|
|
197
313
|
} catch (error) {
|
package/src/router.js
CHANGED
|
@@ -157,7 +157,6 @@ export async function loadRoutes(app, routesDir, createHandler, openApiSpec) {
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
// Auto-inject security definition if auth is true
|
|
161
160
|
if (config.auth === 'api-key') {
|
|
162
161
|
operation.security = [{ apiKeyAuth: [] }];
|
|
163
162
|
} else if (config.auth) {
|
package/src/server.js
CHANGED
|
@@ -407,7 +407,12 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
407
407
|
|
|
408
408
|
const initialRoutes = await reload();
|
|
409
409
|
|
|
410
|
-
|
|
410
|
+
let taskManager = await scanTasks({ db, io });
|
|
411
|
+
|
|
412
|
+
const reloadTasks = async () => {
|
|
413
|
+
if (taskManager) taskManager.stopAll();
|
|
414
|
+
taskManager = await scanTasks({ db, io });
|
|
415
|
+
};
|
|
411
416
|
|
|
412
417
|
let isShuttingDown = false;
|
|
413
418
|
const shutdown = async () => {
|
|
@@ -434,5 +439,5 @@ export async function createServer(globalConfig, routesDir, db) {
|
|
|
434
439
|
});
|
|
435
440
|
};
|
|
436
441
|
|
|
437
|
-
return { app, server, routes: initialRoutes, reload, reloadLocale, io, shutdown };
|
|
442
|
+
return { app, server, routes: initialRoutes, reload, reloadLocale, reloadTasks, io, shutdown };
|
|
438
443
|
}
|