@goodandready/dsh-key-rotation 0.8.0 → 0.8.1

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.
Files changed (2) hide show
  1. package/lib/client.js +53 -4
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -240,6 +240,33 @@ window.__ModuleLoader__.load({
240
240
  // отдельными отмеченными галочками, чтобы правило нельзя было потерять.
241
241
  const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
242
242
 
243
+ // #273: keep the settings list item mounted if the section throws.
244
+ class KeyRotationErrorBoundary extends React.Component {
245
+ constructor(props) {
246
+ super(props);
247
+ this.state = { error: null };
248
+ }
249
+ static getDerivedStateFromError(error) {
250
+ return { error };
251
+ }
252
+ componentDidCatch(error, info) {
253
+ try { console.error('[dsh-key-rotation] settings card crashed', error, info); } catch (_) {}
254
+ }
255
+ render() {
256
+ if (this.state.error) {
257
+ return React.createElement('div', { className: 'krot krot-err', style: { padding: 12 } },
258
+ React.createElement('p', null, 'Key Rotation settings failed to render.'),
259
+ React.createElement('p', { style: { fontSize: 11, opacity: 0.8 } }, String(this.state.error?.message || this.state.error)),
260
+ React.createElement('button', {
261
+ type: 'button',
262
+ className: 'krot-btn',
263
+ onClick: () => this.setState({ error: null }),
264
+ }, 'Retry'));
265
+ }
266
+ return this.props.children;
267
+ }
268
+ }
269
+
243
270
  /** Опрос статуса ротации, пока раздел настроек открыт (smart polling). */
244
271
  function useRotationStatus() {
245
272
  const [byProvider, setByProvider] = React.useState({});
@@ -414,9 +441,31 @@ window.__ModuleLoader__.load({
414
441
  }
415
442
  return null;
416
443
  }, [props.ctx]);
444
+ // #273: getSnapshot must be referentially stable or React 18 unmounts the tree
445
+ const scopeCacheRef = React.useRef({ has: false, value: null });
446
+ const getScopeSnapshot = React.useCallback(() => {
447
+ if (!settingsScope || typeof settingsScope.getSnapshot !== 'function') return null;
448
+ let next = null;
449
+ try {
450
+ next = settingsScope.getSnapshot();
451
+ } catch (_) {
452
+ return null;
453
+ }
454
+ const prev = scopeCacheRef.current;
455
+ if (prev.has && Object.is(prev.value, next)) return prev.value;
456
+ // shallow-compare common snapshot shape to avoid identity thrash
457
+ if (prev.has && prev.value && next
458
+ && prev.value.status === next.status
459
+ && prev.value.revision === next.revision
460
+ && prev.value.value === next.value) {
461
+ return prev.value;
462
+ }
463
+ scopeCacheRef.current = { has: true, value: next };
464
+ return next;
465
+ }, [settingsScope]);
417
466
  const scopeSnapshot = React.useSyncExternalStore(
418
- React.useMemo(() => (cb) => (settingsScope ? settingsScope.subscribe(cb) : () => {}), [settingsScope]),
419
- React.useCallback(() => (settingsScope ? settingsScope.getSnapshot() : null), [settingsScope])
467
+ React.useMemo(() => (cb) => (settingsScope && typeof settingsScope.subscribe === 'function' ? settingsScope.subscribe(cb) : () => {}), [settingsScope]),
468
+ getScopeSnapshot
420
469
  );
421
470
  const [state, setState] = React.useState({ status: 'loading', value: null, revision: 0, error: '', providers: [] });
422
471
  const [draft, setDraft] = React.useState(null);
@@ -1185,7 +1234,7 @@ window.__ModuleLoader__.load({
1185
1234
  h('span', { className: 'krot-card-chevron' + (open ? ' krot-card-chevron-open' : ''), 'aria-hidden': 'true' },
1186
1235
  h('svg', { width: 14, height: 14, viewBox: '0 0 14 14', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, style: { display: 'block' } },
1187
1236
  h('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' })))),
1188
- open ? h('div', { className: 'krot-card-body' }, h(KeyRotationSection, { ...props, locale })) : null);
1237
+ open ? h('div', { className: 'krot-card-body' }, h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale }))) : null);
1189
1238
  }
1190
1239
  const tryPluginItem = () => {
1191
1240
  try {
@@ -1209,7 +1258,7 @@ window.__ModuleLoader__.load({
1209
1258
  label: () => (t ? t('title') : 'Key Rotation') || 'Key Rotation',
1210
1259
  inject: () => ({ ctx }),
1211
1260
  },
1212
- (props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
1261
+ (props) => h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale: useLocale() })),
1213
1262
  ));
1214
1263
  }
1215
1264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-key-rotation",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "packageManager": "pnpm@10.33.2",
5
5
  "description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
6
6
  "keywords": [