@goodandready/dsh-cron 0.2.43 → 0.2.45
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/lib/client.js +27 -6
- package/lib/index.js +26 -3
- package/lib/settings.js +11 -18
- package/lib/store.js +10 -3
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -1441,14 +1441,23 @@ window.__ModuleLoader__.load({
|
|
|
1441
1441
|
|
|
1442
1442
|
let timerId = null;
|
|
1443
1443
|
let isDestroyed = false;
|
|
1444
|
+
let pollController = null;
|
|
1445
|
+
const currentGen = fetchGenRef.current;
|
|
1444
1446
|
|
|
1445
1447
|
const doPoll = () => {
|
|
1446
|
-
if (isDestroyed) return;
|
|
1448
|
+
if (isDestroyed || currentGen !== fetchGenRef.current) return;
|
|
1447
1449
|
const headers = {};
|
|
1448
1450
|
if (lastEtagRef.current) headers['If-None-Match'] = lastEtagRef.current;
|
|
1449
1451
|
|
|
1450
|
-
|
|
1452
|
+
if (pollController) pollController.abort();
|
|
1453
|
+
pollController = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
|
1454
|
+
|
|
1455
|
+
const fetchOpts = { headers };
|
|
1456
|
+
if (pollController) fetchOpts.signal = pollController.signal;
|
|
1457
|
+
|
|
1458
|
+
fetch('/dsh-cron/tasks?status=' + encodeURIComponent(tab) + '&query=' + encodeURIComponent(query), fetchOpts)
|
|
1451
1459
|
.then(res => {
|
|
1460
|
+
if (isDestroyed || currentGen !== fetchGenRef.current) return null;
|
|
1452
1461
|
if (res.status === 304) {
|
|
1453
1462
|
// Not modified: skip parsing JSON
|
|
1454
1463
|
return null;
|
|
@@ -1458,15 +1467,18 @@ window.__ModuleLoader__.load({
|
|
|
1458
1467
|
return res.json();
|
|
1459
1468
|
})
|
|
1460
1469
|
.then(data => {
|
|
1470
|
+
if (isDestroyed || currentGen !== fetchGenRef.current) return;
|
|
1461
1471
|
if (data && data.ok) {
|
|
1462
1472
|
setTasks(data.tasks || []);
|
|
1463
1473
|
setRecs(data.recommendations || []);
|
|
1464
1474
|
setStats(data.stats || null);
|
|
1465
1475
|
}
|
|
1466
1476
|
})
|
|
1467
|
-
.catch(
|
|
1477
|
+
.catch(err => {
|
|
1478
|
+
if (err && err.name === 'AbortError') return;
|
|
1479
|
+
})
|
|
1468
1480
|
.finally(() => {
|
|
1469
|
-
if (isDestroyed) return;
|
|
1481
|
+
if (isDestroyed || currentGen !== fetchGenRef.current) return;
|
|
1470
1482
|
// Adaptive interval: 30s when tab is hidden, 8s when active (#134)
|
|
1471
1483
|
const intervalMs = (typeof document !== 'undefined' && document.hidden) ? 30000 : 8000;
|
|
1472
1484
|
timerId = setTimeout(doPoll, intervalMs);
|
|
@@ -1490,6 +1502,7 @@ window.__ModuleLoader__.load({
|
|
|
1490
1502
|
return () => {
|
|
1491
1503
|
isDestroyed = true;
|
|
1492
1504
|
if (timerId) clearTimeout(timerId);
|
|
1505
|
+
if (pollController) pollController.abort();
|
|
1493
1506
|
if (typeof document !== 'undefined') {
|
|
1494
1507
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
1495
1508
|
}
|
|
@@ -1507,10 +1520,18 @@ window.__ModuleLoader__.load({
|
|
|
1507
1520
|
...options,
|
|
1508
1521
|
signal: controller ? controller.signal : options.signal,
|
|
1509
1522
|
});
|
|
1510
|
-
|
|
1523
|
+
let data = {};
|
|
1524
|
+
try {
|
|
1525
|
+
data = await res.json();
|
|
1526
|
+
} catch (jsonErr) {
|
|
1527
|
+
// Re-throw if body reading aborted/timed out (#242)
|
|
1528
|
+
if (jsonErr && (jsonErr.name === 'AbortError' || controller?.signal.aborted)) {
|
|
1529
|
+
throw jsonErr;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1511
1532
|
return { res, data };
|
|
1512
1533
|
} catch (err) {
|
|
1513
|
-
if (err && err.name === 'AbortError') {
|
|
1534
|
+
if (err && (err.name === 'AbortError' || controller?.signal.aborted)) {
|
|
1514
1535
|
const timeoutErr = new Error(`Request timed out after ${Math.round(timeoutMs / 1000)}s`);
|
|
1515
1536
|
timeoutErr.isTimeout = true;
|
|
1516
1537
|
throw timeoutErr;
|
package/lib/index.js
CHANGED
|
@@ -80,6 +80,7 @@ import {
|
|
|
80
80
|
sanitizeSettingsPayload,
|
|
81
81
|
unwrapConfig,
|
|
82
82
|
unwrapConfigValue,
|
|
83
|
+
plainConfig,
|
|
83
84
|
} from './settings.js';
|
|
84
85
|
import { registerRoutes } from './routes.js';
|
|
85
86
|
|
|
@@ -90,6 +91,7 @@ export {
|
|
|
90
91
|
sanitizeSettingsPayload,
|
|
91
92
|
unwrapConfig,
|
|
92
93
|
unwrapConfigValue,
|
|
94
|
+
plainConfig,
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
export const name = '@goodandready/dsh-cron';
|
|
@@ -115,11 +117,31 @@ export {
|
|
|
115
117
|
LEGACY_ACTION_MAP,
|
|
116
118
|
};
|
|
117
119
|
const pkgUrl = new URL('../package.json', import.meta.url);
|
|
120
|
+
// Helper to access configuration safely in native Cordis Context without throwing missing-inject error (#235)
|
|
121
|
+
function getContextConfig(ctx, rawConfig) {
|
|
122
|
+
try {
|
|
123
|
+
if (ctx && ctx.fiber && ctx.fiber.config) return ctx.fiber.config;
|
|
124
|
+
if (ctx && ctx.scope && ctx.scope.config) return ctx.scope.config;
|
|
125
|
+
if (ctx && ctx.runtime && ctx.runtime.config) return ctx.runtime.config;
|
|
126
|
+
if (ctx && typeof ctx === 'object' && 'config' in ctx) {
|
|
127
|
+
try {
|
|
128
|
+
const c = ctx.config;
|
|
129
|
+
if (c) return c;
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Ignore missing inject error on native Cordis Context (#235)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
// Ignore context access error and fall back to rawConfig (#235)
|
|
136
|
+
}
|
|
137
|
+
return rawConfig || {};
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgUrl, 'utf8')).version || '0.2.29';
|
|
119
141
|
|
|
120
142
|
export function apply(ctx, rawConfig) {
|
|
121
143
|
if (ctx && ctx.logger) setLogger(ctx.logger);
|
|
122
|
-
const config =
|
|
144
|
+
const config = plainConfig(rawConfig);
|
|
123
145
|
const storePath = (config && (config.cronDir || config.storePath)) ? (config.storePath || path.join(config.cronDir, 'tasks.json')) : undefined;
|
|
124
146
|
const store = new TaskStore(storePath);
|
|
125
147
|
|
|
@@ -213,7 +235,8 @@ export function apply(ctx, rawConfig) {
|
|
|
213
235
|
function syncVolatileConfig(updatedConfig) {
|
|
214
236
|
try {
|
|
215
237
|
const isPlainConfigObject = updatedConfig && typeof updatedConfig === 'object' && !Array.isArray(updatedConfig);
|
|
216
|
-
const
|
|
238
|
+
const liveConfig = getContextConfig(ctx, rawConfig);
|
|
239
|
+
const cfg = isPlainConfigObject ? { ...liveConfig, ...updatedConfig } : liveConfig;
|
|
217
240
|
const unwrapped = unwrapConfig(cfg);
|
|
218
241
|
activeVolatileConfig = { ...activeVolatileConfig, ...unwrapped };
|
|
219
242
|
if (typeof unwrapped.maxConcurrent === 'number') {
|
|
@@ -284,7 +307,7 @@ export function apply(ctx, rawConfig) {
|
|
|
284
307
|
try {
|
|
285
308
|
if (req.method === 'GET') {
|
|
286
309
|
const unwrapped = {
|
|
287
|
-
...unwrapConfig(ctx
|
|
310
|
+
...unwrapConfig(getContextConfig(ctx, rawConfig)),
|
|
288
311
|
...activeVolatileConfig,
|
|
289
312
|
};
|
|
290
313
|
const clientSettings = store.getClientSettings();
|
package/lib/settings.js
CHANGED
|
@@ -53,28 +53,21 @@ export const Config = z.object({
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
*
|
|
56
|
+
* Deeply unwrap Cosmokit/Schemastery Volatile boxes into plain primitives/objects/arrays (#282).
|
|
57
57
|
*/
|
|
58
|
-
export function
|
|
59
|
-
if (
|
|
60
|
-
|
|
58
|
+
export function plainConfig(value) {
|
|
59
|
+
if (value === null || value === undefined) return value;
|
|
60
|
+
if (typeof value === 'function') return plainConfig(value());
|
|
61
|
+
if (typeof value.get === 'function') return plainConfig(value.get());
|
|
62
|
+
if (Array.isArray(value)) return value.map(plainConfig);
|
|
63
|
+
if (typeof value === 'object') {
|
|
64
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plainConfig(v)]));
|
|
61
65
|
}
|
|
62
|
-
return
|
|
66
|
+
return value;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
if (typeof cfg.get === 'function') return unwrapConfig(cfg.get());
|
|
68
|
-
return new Proxy(cfg, {
|
|
69
|
-
get(target, prop, receiver) {
|
|
70
|
-
const val = Reflect.get(target, prop, receiver);
|
|
71
|
-
if (val && typeof val === 'object' && typeof val.get === 'function') {
|
|
72
|
-
return val.get();
|
|
73
|
-
}
|
|
74
|
-
return val;
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
69
|
+
export const unwrapConfigValue = plainConfig;
|
|
70
|
+
export const unwrapConfig = plainConfig;
|
|
78
71
|
|
|
79
72
|
export const SETTINGS_SYNC_KEYS = [
|
|
80
73
|
'botToken', 'botTokenRef', 'telegramWebhookSecret', 'chatId', 'notifyTelegram', 'onlyOnFailure', 'kanbanBaseUrl',
|
package/lib/store.js
CHANGED
|
@@ -190,8 +190,12 @@ export class TaskStore {
|
|
|
190
190
|
const tmp = `${this.filePath}.tmp.${process.pid}_${Date.now()}_${rand}`;
|
|
191
191
|
this._currentAsyncTmp = tmp;
|
|
192
192
|
|
|
193
|
-
//
|
|
193
|
+
// Yield to the event loop before CPU serialization so pending timers/I/O are serviced (#244)
|
|
194
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
195
|
+
// Compact serialization reduces payload size; disk write and rename are offloaded asynchronously via fs.promises (#244)
|
|
194
196
|
const payload = JSON.stringify(data);
|
|
197
|
+
// Yield after serialization before starting disk write I/O
|
|
198
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
195
199
|
await fs.promises.writeFile(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
|
|
196
200
|
bestEffort('chmod-tmp', () => fs.chmodSync(tmp, 0o600));
|
|
197
201
|
|
|
@@ -209,8 +213,11 @@ export class TaskStore {
|
|
|
209
213
|
this._lastWrittenVersion = version;
|
|
210
214
|
} catch (err) {
|
|
211
215
|
this._currentAsyncTmp = null;
|
|
212
|
-
// If tmp was unlinked by newer sync save, rename will fail with ENOENT - safely ignore
|
|
213
|
-
if (err.code
|
|
216
|
+
// If tmp was unlinked by newer sync save, rename will fail with ENOENT - safely ignore only in that case (#244)
|
|
217
|
+
if (err.code === 'ENOENT' && (this._lastSyncVersion || 0) >= version) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
214
221
|
}
|
|
215
222
|
|
|
216
223
|
const now = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-cron",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.45",
|
|
4
4
|
"description": "Background automation runner for DSH: isolated agent runs, script/HTTP/SSH/Docker runtimes, cost guard, notifications, heartbeats.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|