@goodandready/dsh-model-sync 0.3.9 → 0.3.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 +12 -0
- package/README.ru.md +12 -0
- package/lib/client.js +12 -3
- package/lib/history.js +14 -3
- package/lib/scheduler.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,18 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
142
142
|
|
|
143
143
|
---
|
|
144
144
|
|
|
145
|
+
## 🚀 Enhancements in v0.3.10
|
|
146
|
+
|
|
147
|
+
* 📦 **Compacted History Storage**:
|
|
148
|
+
* Stripped non-essential metadata (`description`, `tags`, `aliases`) from catalog snapshots (`before`/`after`) in `settings.yaml`, retaining only essential properties (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
149
|
+
* Optimized default snapshot retention (`DEFAULT_SNAPSHOT_RETENTION = 3`) to keep configuration files lean while preserving full rollback functionality.
|
|
150
|
+
* ⚡ **Immediate Scheduler First Tick**:
|
|
151
|
+
* Scheduler now triggers an initial discovery run immediately upon startup when enabled without requiring an initial 60-minute wait.
|
|
152
|
+
* 🛡️ **Client-side Policy Regex Validation**:
|
|
153
|
+
* Added instant regex pattern verification in the Web UI before submitting `/policy` requests with localized error feedback.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
145
157
|
## 🚀 Enhancements in v0.3.9
|
|
146
158
|
|
|
147
159
|
* 🔒 **Serialized Settings Mutations (`saveConfig`)**:
|
package/README.ru.md
CHANGED
|
@@ -119,6 +119,18 @@ dsh plugin --profile web add @goodandready/dsh-model-sync
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
+
## 🚀 Новые возможности в v0.3.10
|
|
123
|
+
|
|
124
|
+
* 📦 **Компактизация хранения истории**:
|
|
125
|
+
* Очистка некритичных метаданных (`description`, `tags`, `aliases`) из снимков моделей (`before`/`after`) в `settings.yaml`, с сохранением ключевых свойств (`id`, `name`, `capabilities`, `pricing`, `contextWindow`, `maxTokens`).
|
|
126
|
+
* Оптимизирована глубина хранения снимков (`DEFAULT_SNAPSHOT_RETENTION = 3`) для предотвращения разрастания конфигурационного файла при сохранении возможности безопасного отката.
|
|
127
|
+
* ⚡ **Мгновенный первый запуск планировщика**:
|
|
128
|
+
* Планировщик автоматически запускает первичный синк сразу при старте, если расписание активно и предыдущие запуски отсутствуют.
|
|
129
|
+
* 🛡️ **Клиентская валидация regex-политик**:
|
|
130
|
+
* Добавлена проверка синтаксиса регулярных выражений в интерфейсе настроек перед отправкой запроса на `/policy` с локализованными подсказками.
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
122
134
|
## 🚀 Новые возможности в v0.3.9
|
|
123
135
|
|
|
124
136
|
* 🔒 **Атомарная очередь сохранения настроек (`saveConfig`)**:
|
package/lib/client.js
CHANGED
|
@@ -42,10 +42,10 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
document.head.appendChild(tag)
|
|
43
43
|
}
|
|
44
44
|
const en = {
|
|
45
|
-
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago', noHistory: 'No synchronization history.', searchPlaceholder: 'Search id/name/tag', sortName: 'Name A→Z', sortPrice: 'Price ↑', sortContext: 'Context ↓', sortDate: 'Newest ↓', schedulerSettings: 'Scheduler configuration', scheduleEnabled: 'Enable background schedule', intervalMinutes: 'Interval (minutes)', autoApplySchedule: 'Auto-apply changes', saveScheduler: 'Save scheduler', schedulerSaved: 'Saved', scopeUnavailable: 'Settings service unavailable.',
|
|
45
|
+
title: 'Model synchronization', showSettings: 'Show settings', hideSettings: 'Hide settings', desc: 'Refresh model catalogs for API-key providers. A dry-run is the default; applying changes is explicit.', loading: 'Loading…', refresh: 'Refresh status', refreshAll: 'Refresh all', run: 'Discover', apply: 'Apply changes', dryRun: 'Preview only (dry-run)', all: 'All API-key providers', ok: 'ready', dormant: 'not configured', running: 'running…', noProviders: 'No API-key providers found.', lastRun: 'Last run', previewTitle: 'Changes preview', previewNone: 'No changes detected.', previewAdded: 'Added', previewRemoved: 'Removed', previewChanged: 'Changed', previewApply: 'Apply preview', previewDismiss: 'Close preview', selectModels: 'Choose models', saveSelection: 'Save and apply selection', selectAtLeastOne: 'Select at least one model or use All models.', cancel: 'Cancel', selected: 'selected', allModels: 'All models', noModels: 'No models in the current catalog.', capabilityFilter: 'Filter by capabilities', capabilityCount: 'shown', noCapabilities: 'No capability metadata in this catalog.', pickerNote: 'The selection is applied to the DSH model picker immediately.', health: 'Check availability', healthOk: 'available', healthError: 'unavailable', healthDormant: 'not configured', policy: 'Manual model selection', savePolicy: 'Save policy', policyInclude: 'Include patterns (one per line)', policyExclude: 'Exclude patterns (one per line)', policyRequire: 'Required capabilities (comma-separated)', policyDeny: 'Denied capabilities (comma-separated)', policyNote: 'Patterns match model id, name, and tags. Apply changes to update the DSH picker.', scheduler: 'Scheduler', nextRun: 'next run', schedulerOff: 'off', schedulerOn: 'on', removeMissing: 'Confirm stale removal', stale: 'stale', deprecated: 'deprecated', history: 'Synchronization history', historyRun: 'Run', diffAdded: 'added', diffRemoved: 'removed', diffRenamed: 'renamed', diffChanged: 'metadata changed', rollback: 'Rollback catalog', rollbackConfirm: 'Rollback this provider catalog to the selected snapshot?', credentials: 'Credential diagnostics', checkCredentials: 'Check credentials', missingCredential: 'not configured', credentialReady: 'configured', report: 'Run report', success: 'success', partial: 'partial success', failure: 'failed', empty: 'no configured providers', notifications: 'Notifications', markRead: 'Mark read', acknowledge: 'Acknowledge', noNotifications: 'No notifications.', selectAllFiltered: 'Select all shown', deselectAllFiltered: 'Deselect all shown', invertFiltered: 'Invert selection', filterContext100k: 'Context ≥ 100k', filterCheap: 'Cheap ≤ $1/1M', showMore: 'Show more (%d)', showingCount: 'Showing %d of %d models', deprecatesIn: 'Deprecates in %dd', cacheHoursAgo: 'cache %dh ago', noHistory: 'No synchronization history.', searchPlaceholder: 'Search id/name/tag', sortName: 'Name A→Z', sortPrice: 'Price ↑', sortContext: 'Context ↓', sortDate: 'Newest ↓', schedulerSettings: 'Scheduler configuration', scheduleEnabled: 'Enable background schedule', intervalMinutes: 'Interval (minutes)', autoApplySchedule: 'Auto-apply changes', saveScheduler: 'Save scheduler', schedulerSaved: 'Saved', scopeUnavailable: 'Settings service unavailable.', invalidPattern: 'Invalid regex pattern: %s',
|
|
46
46
|
}
|
|
47
47
|
const ru = {
|
|
48
|
-
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад', noHistory: 'Истории синхронизаций нет.', searchPlaceholder: 'Поиск id/name/tag', sortName: 'Имя A→Z', sortPrice: 'Цена ↑', sortContext: 'Контекст ↓', sortDate: 'Новые ↓', schedulerSettings: 'Настройка планировщика', scheduleEnabled: 'Включить фоновое расписание', intervalMinutes: 'Интервал (минуты)', autoApplySchedule: 'Автоматически применять изменения', saveScheduler: 'Сохранить расписание', schedulerSaved: 'Сохранено', scopeUnavailable: 'Служба настроек недоступна.',
|
|
48
|
+
title: 'Синхронизация моделей', showSettings: 'Показать настройки', hideSettings: 'Скрыть настройки', desc: 'Обновляет каталоги моделей у провайдеров с API-ключом. По умолчанию только dry-run; применение запускается явно.', loading: 'Загрузка…', refresh: 'Обновить статус', refreshAll: 'Обновить все', run: 'Проверить', apply: 'Применить изменения', dryRun: 'Только просмотр (dry-run)', all: 'Все API-key провайдеры', ok: 'готов', dormant: 'не настроен', running: 'выполняется…', noProviders: 'API-key провайдеры не найдены.', lastRun: 'Последний запуск', previewTitle: 'Предпросмотр изменений', previewNone: 'Изменений не обнаружено.', previewAdded: 'Добавлено', previewRemoved: 'Удалено', previewChanged: 'Изменено', previewApply: 'Применить изменения', previewDismiss: 'Закрыть предпросмотр', selectModels: 'Выбрать модели', saveSelection: 'Сохранить и применить выбор', selectAtLeastOne: 'Выберите хотя бы одну модель или нажмите «Все модели».', cancel: 'Отмена', selected: 'выбрано', allModels: 'Все модели', noModels: 'В текущем каталоге нет моделей.', capabilityFilter: 'Фильтр по возможностям', capabilityCount: 'показано', noCapabilities: 'В этом каталоге нет данных о возможностях.', pickerNote: 'Выбор сразу применяется к пикеру моделей DSH.', health: 'Проверить доступность', healthOk: 'доступен', healthError: 'недоступен', healthDormant: 'не настроен', policy: 'Ручной выбор моделей', savePolicy: 'Сохранить политику', policyInclude: 'Включать (по одному шаблону в строке)', policyExclude: 'Исключать (по одному шаблону в строке)', policyRequire: 'Обязательные возможности (через запятую)', policyDeny: 'Запрещённые возможности (через запятую)', policyNote: 'Шаблоны ищутся в id, названии и тегах модели. Нажмите «Применить», чтобы обновить пикер DSH.', scheduler: 'Планировщик', nextRun: 'следующий запуск', schedulerOff: 'выключен', schedulerOn: 'включён', removeMissing: 'Подтвердить удаление устаревших', stale: 'устарела', deprecated: 'deprecated', history: 'История синхронизаций', historyRun: 'Запуск', diffAdded: 'добавлено', diffRemoved: 'удалено', diffRenamed: 'переименовано', diffChanged: 'изменена метаинформация', rollback: 'Откатить каталог', rollbackConfirm: 'Откатить каталог этого провайдера к выбранному снимку?', credentials: 'Диагностика ключей', checkCredentials: 'Проверить ключи', missingCredential: 'не настроен', credentialReady: 'настроен', report: 'Итог запуска', success: 'успех', partial: 'частичный успех', failure: 'ошибка', empty: 'нет настроенных провайдеров', notifications: 'Уведомления', markRead: 'Прочитано', acknowledge: 'Подтвердить', noNotifications: 'Новых уведомлений нет.', selectAllFiltered: 'Выбрать все показанные', deselectAllFiltered: 'Снять выбор с показанных', invertFiltered: 'Инвертировать выбор', filterContext100k: 'Контекст ≥ 100k', filterCheap: 'Дешевые ≤ $1/1M', showMore: 'Показать ещё (%d)', showingCount: 'Показано %d из %d моделей', deprecatesIn: 'Устареет через %d дн.', cacheHoursAgo: 'кеш %dч назад', noHistory: 'Истории синхронизаций нет.', searchPlaceholder: 'Поиск id/name/tag', sortName: 'Имя A→Z', sortPrice: 'Цена ↑', sortContext: 'Контекст ↓', sortDate: 'Новые ↓', schedulerSettings: 'Настройка планировщика', scheduleEnabled: 'Включить фоновое расписание', intervalMinutes: 'Интервал (минуты)', autoApplySchedule: 'Автоматически применять изменения', saveScheduler: 'Сохранить расписание', schedulerSaved: 'Сохранено', scopeUnavailable: 'Служба настроек недоступна.', invalidPattern: 'Некорректный regex-шаблон: %s',
|
|
49
49
|
}
|
|
50
50
|
const API = '/dsh-model-sync'
|
|
51
51
|
const CAPABILITY_LABELS = { vision: { en: 'Vision', ru: 'Зрение' }, tools: { en: 'Tools', ru: 'Инструменты' }, reasoning: { en: 'Reasoning', ru: 'Рассуждение' }, embeddings: { en: 'Embeddings', ru: 'Эмбеддинги' } }
|
|
@@ -97,7 +97,16 @@ window.__ModuleLoader__.load({
|
|
|
97
97
|
const closePolicy = () => { setPolicyProvider(''); setPolicyDraft({ include: '', exclude: '', requireCapabilities: '', denyCapabilities: '' }) }
|
|
98
98
|
const splitValues = (value) => [...new Set(String(value ?? '').split(/[,\n]+/).map((item) => item.trim()).filter(Boolean))]
|
|
99
99
|
const capabilityValues = (value) => Object.fromEntries(splitValues(value).map((key) => [key, true]))
|
|
100
|
-
const
|
|
100
|
+
const validatePatterns = (patterns) => { for (const p of patterns) { try { new RegExp(p, 'i') } catch { return p } } return null }
|
|
101
|
+
const savePolicy = () => {
|
|
102
|
+
const includeList = splitValues(policyDraft.include)
|
|
103
|
+
const excludeList = splitValues(policyDraft.exclude)
|
|
104
|
+
const badInclude = validatePatterns(includeList)
|
|
105
|
+
if (badInclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badInclude)); return }
|
|
106
|
+
const badExclude = validatePatterns(excludeList)
|
|
107
|
+
if (badExclude) { setError((t.invalidPattern || 'Invalid regex pattern: %s').replace('%s', badExclude)); return }
|
|
108
|
+
setBusy(true); setError(''); fetch(`${API}/policy`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: policyProvider, include: includeList, exclude: excludeList, requireCapabilities: capabilityValues(policyDraft.requireCapabilities), denyCapabilities: capabilityValues(policyDraft.denyCapabilities) }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => { closePolicy(); return load() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false))
|
|
109
|
+
}
|
|
101
110
|
const saveSelection = () => { const row = rows.find((item) => item.provider === selectionProvider); const available = row?.availableModels ?? row?.models ?? []; if (available.length > 0 && selectionDraft.length === 0) { setError(t.selectAtLeastOne); return } const models = selectionDraft.length === available.length ? [] : selectionDraft; setBusy(true); setError(''); fetch(`${API}/selection`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ provider: selectionProvider, models }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then((body) => { setStatus(body); closeSelection() }).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
102
111
|
const rollback = (entry, targetProvider) => { if (!window.confirm(t.rollbackConfirm)) return; setBusy(true); setError(''); fetch(`${API}/history/rollback`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ historyId: entry.id, provider: targetProvider }) }).then(async (r) => { const body = await r.json(); if (!r.ok) throw new Error(body?.error?.message ?? `HTTP ${r.status}`); return body }).then(() => load()).catch((e) => setError(String(e?.message ?? e))).finally(() => setBusy(false)) }
|
|
103
112
|
const isNewModel = (model, row) => { const added = status?.lastRun?.results?.find(r=>r.provider===row.provider)?.diff?.added||[]; const isAdded = added.some(m=>m.id===model.id); if(!isAdded) return false; const finishedAt = status?.lastRun?.finishedAt; if(!finishedAt) return false; return (Date.now()-new Date(finishedAt).getTime()) < 7*24*60*60*1000; };
|
package/lib/history.js
CHANGED
|
@@ -16,11 +16,22 @@ export function historyLimit(value) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function cloneModel(provider, model) {
|
|
19
|
-
|
|
19
|
+
const normalized = normalizeModels(provider, [model])[0]
|
|
20
|
+
if (!normalized) return null
|
|
21
|
+
const compact = {
|
|
22
|
+
provider: normalized.provider,
|
|
23
|
+
id: normalized.id,
|
|
24
|
+
name: normalized.name,
|
|
25
|
+
}
|
|
26
|
+
if (normalized.capabilities) compact.capabilities = normalized.capabilities
|
|
27
|
+
if (normalized.pricing) compact.pricing = normalized.pricing
|
|
28
|
+
if (normalized.contextWindow !== undefined) compact.contextWindow = normalized.contextWindow
|
|
29
|
+
if (normalized.maxTokens !== undefined) compact.maxTokens = normalized.maxTokens
|
|
30
|
+
return compact
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
function catalog(provider, models) {
|
|
23
|
-
return normalizeModels(provider, models).slice(0, MAX_HISTORY_MODELS)
|
|
34
|
+
return (normalizeModels(provider, models) ?? []).map((model) => cloneModel(provider, model)).filter(Boolean).slice(0, MAX_HISTORY_MODELS)
|
|
24
35
|
}
|
|
25
36
|
|
|
26
37
|
function message(value) {
|
|
@@ -80,7 +91,7 @@ export function createHistoryEntry(result, { version = 1, maxModels = MAX_HISTOR
|
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
83
|
-
export const DEFAULT_SNAPSHOT_RETENTION =
|
|
94
|
+
export const DEFAULT_SNAPSHOT_RETENTION = 3
|
|
84
95
|
|
|
85
96
|
export function compactHistoryEntry(entry) {
|
|
86
97
|
if (!entry || typeof entry !== 'object') return entry
|
package/lib/scheduler.js
CHANGED
|
@@ -117,10 +117,16 @@ export function createSyncScheduler(sync, {
|
|
|
117
117
|
if (config.enabled === false || config.scheduleEnabled === false) return false
|
|
118
118
|
const rows = scheduleRows(config)
|
|
119
119
|
if (inventory(config) !== null && rows.length === 0) return false
|
|
120
|
+
const isFirstRun = state.size === 0 || [...state.values()].every((row) => row.lastRunAt === null)
|
|
120
121
|
resetStates(config)
|
|
121
122
|
// One-minute polling is the lower safety bound. Per-provider intervals,
|
|
122
123
|
// TTLs, and jitter are enforced by tick without multiplying timers.
|
|
123
124
|
timer = setIntervalFn(() => tick(), 60_000)
|
|
125
|
+
if (isFirstRun) {
|
|
126
|
+
Promise.resolve().then(() => tick()).catch((err) => {
|
|
127
|
+
logger.warn?.("[dsh-model-sync] initial tick failed: " + errorMessage(err))
|
|
128
|
+
})
|
|
129
|
+
}
|
|
124
130
|
return true
|
|
125
131
|
}
|
|
126
132
|
|