@goodandready/dsh-model-sync 0.2.7 → 0.2.10
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 +27 -0
- package/lib/adapter-registry.js +147 -27
- package/lib/client.js +87 -14
- package/lib/credentials.js +86 -5
- package/lib/generic-adapter.js +37 -5
- package/lib/history.js +138 -0
- package/lib/http.js +121 -4
- package/lib/index.js +62 -1
- package/lib/lifecycle.js +130 -0
- package/lib/models.js +203 -11
- package/lib/policy.js +71 -0
- package/lib/reconcile.js +8 -2
- package/lib/reliability.js +112 -0
- package/lib/reporting.js +155 -0
- package/lib/scheduler.js +127 -21
- package/lib/synchronizer.js +427 -36
- package/package.json +1 -1
package/lib/scheduler.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
function minutes(value, fallback = 60) {
|
|
2
|
+
const number = Number(value)
|
|
3
|
+
if (!Number.isFinite(number) || number <= 0) return fallback
|
|
4
|
+
return Math.min(10080, Math.max(1, number))
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function optionalMinutes(value, fallback) {
|
|
8
|
+
const number = Number(value)
|
|
9
|
+
if (!Number.isFinite(number) || number <= 0) return fallback
|
|
10
|
+
return Math.min(10080, Math.max(1, number))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function errorMessage(error) {
|
|
14
|
+
return error instanceof Error ? error.message : String(error)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createSyncScheduler(sync, {
|
|
18
|
+
getConfig = () => ({}),
|
|
19
|
+
setIntervalFn = setInterval,
|
|
20
|
+
clearIntervalFn = clearInterval,
|
|
21
|
+
logger = console,
|
|
22
|
+
randomFn = Math.random,
|
|
23
|
+
nowFn = () => Date.now(),
|
|
24
|
+
} = {}) {
|
|
2
25
|
let timer = null
|
|
3
26
|
let running = false
|
|
27
|
+
const state = new Map()
|
|
4
28
|
|
|
5
29
|
function stop() {
|
|
6
30
|
if (timer !== null) clearIntervalFn(timer)
|
|
@@ -8,30 +32,112 @@ export function createSyncScheduler(sync, { getConfig = () => ({}), setIntervalF
|
|
|
8
32
|
running = false
|
|
9
33
|
}
|
|
10
34
|
|
|
35
|
+
function inventory(config) {
|
|
36
|
+
if (typeof sync.listProviders === 'function') {
|
|
37
|
+
const rows = sync.listProviders().filter((row) => row?.configured && row.provider)
|
|
38
|
+
const configured = Array.isArray(config.providers) ? new Map(config.providers.map((row) => [row.provider, row])) : null
|
|
39
|
+
return rows
|
|
40
|
+
.filter((row) => !configured || configured.size === 0 || configured.get(row.provider)?.enabled !== false)
|
|
41
|
+
.map((row) => ({ ...row, ...(configured?.get(row.provider) ?? {}) }))
|
|
42
|
+
}
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function scheduleRows(config) {
|
|
47
|
+
const rows = inventory(config)
|
|
48
|
+
if (rows === null) return [{ provider: undefined, enabled: true }]
|
|
49
|
+
return rows
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function timing(row, config) {
|
|
53
|
+
const interval = optionalMinutes(row.intervalMinutes, minutes(config.intervalMinutes, 60))
|
|
54
|
+
const ttl = optionalMinutes(row.ttlMinutes, optionalMinutes(config.ttlMinutes, 0))
|
|
55
|
+
const jitter = optionalMinutes(row.jitterMinutes, optionalMinutes(config.jitterMinutes, 0))
|
|
56
|
+
return { interval, ttl, jitter }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function delayMs(timingValue) {
|
|
60
|
+
const jitter = timingValue.jitter > 0 ? randomFn() * timingValue.jitter : 0
|
|
61
|
+
return Math.max(60_000, (Math.max(timingValue.interval, timingValue.ttl) + jitter) * 60_000)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function ensureState(row, config, now) {
|
|
65
|
+
const key = row.provider ?? '*'
|
|
66
|
+
const existing = state.get(key)
|
|
67
|
+
if (existing) return existing
|
|
68
|
+
const nextAt = now
|
|
69
|
+
const created = { provider: row.provider, nextAt, lastRunAt: null, lastStatus: null, lastError: null }
|
|
70
|
+
state.set(key, created)
|
|
71
|
+
return created
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resetStates(config) {
|
|
75
|
+
const rows = scheduleRows(config)
|
|
76
|
+
const active = new Set(rows.map((row) => row.provider ?? '*'))
|
|
77
|
+
for (const key of state.keys()) if (!active.has(key)) state.delete(key)
|
|
78
|
+
const now = nowFn()
|
|
79
|
+
for (const row of rows) ensureState(row, config, now)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function tick() {
|
|
83
|
+
const config = getConfig() ?? {}
|
|
84
|
+
if (config.enabled === false || config.scheduleEnabled === false || running) return
|
|
85
|
+
const rows = scheduleRows(config)
|
|
86
|
+
if (rows.length === 0) return
|
|
87
|
+
const now = nowFn()
|
|
88
|
+
const due = rows.filter((row) => now >= ensureState(row, config, now).nextAt)
|
|
89
|
+
if (due.length === 0) return
|
|
90
|
+
running = true
|
|
91
|
+
try {
|
|
92
|
+
for (const row of due) {
|
|
93
|
+
const item = ensureState(row, config, nowFn())
|
|
94
|
+
const options = { dryRun: config.autoApply === true ? false : true, reportSource: 'schedule' }
|
|
95
|
+
if (row.provider) options.provider = row.provider
|
|
96
|
+
item.lastRunAt = nowFn()
|
|
97
|
+
try {
|
|
98
|
+
const result = await sync.run(options)
|
|
99
|
+
item.lastStatus = result?.results?.find((entry) => entry.provider === row.provider)?.status ?? 'ok'
|
|
100
|
+
item.lastError = null
|
|
101
|
+
} catch (error) {
|
|
102
|
+
item.lastStatus = 'error'
|
|
103
|
+
item.lastError = errorMessage(error)
|
|
104
|
+
logger.warn?.(`[dsh-model-sync] scheduled run failed${row.provider ? ` for ${row.provider}` : ''}: ${item.lastError}`)
|
|
105
|
+
} finally {
|
|
106
|
+
item.nextAt = nowFn() + delayMs(timing(row, config))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} finally {
|
|
110
|
+
running = false
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
11
114
|
function start() {
|
|
12
115
|
stop()
|
|
13
116
|
const config = getConfig() ?? {}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
await sync.run({ dryRun: config.autoApply !== true })
|
|
22
|
-
} catch (error) {
|
|
23
|
-
logger.warn?.(`[dsh-model-sync] scheduled run failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
24
|
-
} finally {
|
|
25
|
-
running = false
|
|
26
|
-
}
|
|
27
|
-
}, period)
|
|
117
|
+
if (config.enabled === false || config.scheduleEnabled === false) return false
|
|
118
|
+
const rows = scheduleRows(config)
|
|
119
|
+
if (inventory(config) !== null && rows.length === 0) return false
|
|
120
|
+
resetStates(config)
|
|
121
|
+
// One-minute polling is the lower safety bound. Per-provider intervals,
|
|
122
|
+
// TTLs, and jitter are enforced by tick without multiplying timers.
|
|
123
|
+
timer = setIntervalFn(() => tick(), 60_000)
|
|
28
124
|
return true
|
|
29
125
|
}
|
|
30
126
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
127
|
+
function status() {
|
|
128
|
+
const config = getConfig() ?? {}
|
|
129
|
+
resetStates(config)
|
|
130
|
+
const values = [...state.values()]
|
|
131
|
+
const nextAt = values.length > 0 ? Math.min(...values.map((row) => row.nextAt)) : null
|
|
132
|
+
const lastRunAt = values.filter((row) => row.lastRunAt).reduce((max, row) => Math.max(max, row.lastRunAt), 0) || null
|
|
133
|
+
return {
|
|
134
|
+
active: timer !== null,
|
|
135
|
+
running,
|
|
136
|
+
lastRunAt,
|
|
137
|
+
nextRunAt: nextAt,
|
|
138
|
+
providers: Object.fromEntries(values.map((row) => [row.provider ?? '*', { lastRunAt: row.lastRunAt, nextRunAt: row.nextAt, lastStatus: row.lastStatus, ...(row.lastError ? { lastError: row.lastError } : {}) }])),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Object.freeze({ start, stop, reconfigure: start, tick, status })
|
|
37
143
|
}
|