@iasiv5/dsh-skins 1.5.14 → 1.5.16

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 +181 -186
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -2191,6 +2191,7 @@ ${references.join("\n")}`,
2191
2191
  var CHANNEL = "dsh-skins";
2192
2192
  var FETCH_TIMEOUT_MS = 3e3;
2193
2193
  var UPLOAD_TIMEOUT_MS = 12e4;
2194
+ var RETRY = /* @__PURE__ */ Symbol("dsh-skins:write-dialect-retry");
2194
2195
  function cloneSkins(skins) {
2195
2196
  return skins === void 0 || skins === null ? {} : structuredClone(skins);
2196
2197
  }
@@ -2320,31 +2321,25 @@ ${references.join("\n")}`,
2320
2321
  void flushNow();
2321
2322
  }, FLUSH_DEBOUNCE_MS);
2322
2323
  }
2323
- function flushNow() {
2324
- if (flushTimer !== null) {
2325
- clearTimeout(flushTimer);
2326
- flushTimer = null;
2327
- }
2328
- if (previews.size === 0) return Promise.resolve({ flushed: 0 });
2329
- const intent = [...previews.entries()];
2330
- const buildOperations = () => intent.map(([composite, value]) => {
2331
- const [skinId, key] = composite.split(" ");
2332
- return value === null ? { op: "delete", skinId, key } : { op: "set", skinId, key, value };
2333
- });
2334
- const count = buildOperations().length;
2324
+ function runWriteOp(operations, policy) {
2335
2325
  writeChain = writeChain.then(async () => {
2336
2326
  try {
2337
- if (disposed) return { flushed: 0, blocked: "disposed" };
2327
+ if (disposed) return policy.disposed();
2338
2328
  const blocked = writeGate();
2339
- if (blocked !== null) return { flushed: 0, blocked };
2329
+ if (blocked !== null) return policy.gated(blocked);
2340
2330
  const attemptedRevision = snapshot.revision;
2341
2331
  for (let attempt = 0; attempt < 2; attempt += 1) {
2342
- const response = await request("/config", {
2343
- method: "PATCH",
2344
- headers: { "content-type": "application/json" },
2345
- body: JSON.stringify({ baseRevision: snapshot.revision, operations: buildOperations() })
2346
- });
2347
- if (disposed) return { flushed: 0 };
2332
+ let response;
2333
+ try {
2334
+ response = await request("/config", {
2335
+ method: "PATCH",
2336
+ headers: { "content-type": "application/json" },
2337
+ body: JSON.stringify({ baseRevision: snapshot.revision, operations: operations() })
2338
+ });
2339
+ } catch (error) {
2340
+ return policy.offline(error);
2341
+ }
2342
+ if (disposed) return policy.disposedMidflight();
2348
2343
  if (response.status === 409) {
2349
2344
  const conflictBody = await response.json().catch(() => null);
2350
2345
  const code = conflictBody?.code;
@@ -2352,96 +2347,146 @@ ${references.join("\n")}`,
2352
2347
  if (code === "STORE_READONLY") {
2353
2348
  setFlushFailure(null, null);
2354
2349
  setStatus("unsupported-readonly");
2355
- return { flushed: 0, blocked: "conflict", errorMessage: conflictBody?.error ?? "" };
2350
+ return policy.readOnly(conflictBody);
2356
2351
  }
2357
2352
  if (code === "REVISION_CONFLICT") {
2358
- if (attempt === 1) {
2359
- setFlushFailure("REVISION_CONFLICT", null);
2360
- emit();
2361
- return { flushed: 0, blocked: "conflict" };
2362
- }
2363
- const refreshed = await refetch();
2364
- const mayRetry = refreshed.status === "synced" && refreshed.mode === "normal" && refreshed.revision !== attemptedRevision;
2365
- if (mayRetry) continue;
2366
- if (refreshed.mode === "recovery") {
2367
- setFlushFailure(null, null);
2368
- emit();
2369
- return { flushed: 0, blocked: "recovery" };
2370
- }
2371
- if (refreshed.status === "offline-failed") {
2372
- setFlushFailure(null, null);
2373
- emit();
2374
- return { flushed: 0, blocked: "offline" };
2375
- }
2376
- if (refreshed.status === "unsupported-readonly") {
2377
- setFlushFailure(null, null);
2378
- emit();
2379
- return { flushed: 0, blocked: "unsupported" };
2380
- }
2381
- setFlushFailure("REVISION_CONFLICT", null);
2382
- emit();
2383
- return { flushed: 0, blocked: "conflict" };
2353
+ const decision2 = await policy.onConflict({ attempt, attemptedRevision, errorMessage });
2354
+ if (decision2 === RETRY) continue;
2355
+ return decision2;
2384
2356
  }
2385
2357
  if (code === "STORE_RECOVERY_REQUIRED") {
2386
- const refreshed = await refetch();
2387
- if (refreshed.mode === "recovery") {
2388
- setFlushFailure(null, null);
2389
- emit();
2390
- return { flushed: 0, blocked: "recovery" };
2358
+ const decision2 = await policy.onRecoveryRequired(errorMessage);
2359
+ if (decision2 === RETRY) {
2360
+ throw new Error("[dsh-skins] onRecoveryRequired must resolve, never RETRY");
2391
2361
  }
2392
- setFlushFailure(null, errorMessage);
2393
- emit();
2394
- return { flushed: 0, blocked: "error", errorMessage };
2362
+ return decision2;
2395
2363
  }
2396
- setFlushFailure(null, errorMessage);
2397
- emit();
2398
- return { flushed: 0, blocked: "error", errorMessage };
2399
- }
2400
- if (!response.ok) {
2401
- const errorBody = await response.json().catch(() => null);
2402
- const errorMessage = errorBody?.error ?? `HTTP ${response.status}`;
2403
- const issues = Array.isArray(errorBody?.params?.issues) ? errorBody.params.issues : null;
2404
- let excised = false;
2405
- let onlyDangling = issues !== null && issues.length > 0;
2406
- if (issues !== null) {
2407
- for (const issue of issues) {
2408
- const dangling = issue.code === "MISSING_ASSET" || issue.code === "BAD_ASSET";
2409
- if (!dangling) onlyDangling = false;
2410
- if (dangling && typeof issue.skinId === "string" && typeof issue.key === "string") {
2411
- const composite = `${issue.skinId} ${issue.key}`;
2412
- if (previews.has(composite)) {
2413
- previews.delete(composite);
2414
- excised = true;
2415
- noteReverted({ skinId: issue.skinId, key: issue.key });
2416
- }
2417
- }
2418
- }
2419
- }
2420
- const healed = onlyDangling && excised;
2421
- setFlushFailure(null, healed ? null : errorMessage);
2422
- emit();
2423
- if (excised && previews.size > 0) void flushNow();
2424
- if (healed) return { flushed: 0, blocked: null, selfHealed: true };
2425
- return { flushed: 0, blocked: "error", errorMessage };
2426
- }
2427
- await response.json().catch(() => null);
2428
- for (const [composite, value] of intent) {
2429
- if (previews.get(composite) === value) previews.delete(composite);
2364
+ const decision = await policy.onUnknown409({ attempt, errorMessage });
2365
+ if (decision === RETRY) continue;
2366
+ return decision;
2430
2367
  }
2431
- setFlushFailure(null, null);
2432
- notices = [];
2368
+ if (!response.ok) return await policy.onError(response);
2369
+ const result = await policy.onSuccess(response);
2433
2370
  await refetch();
2434
2371
  announce();
2435
- return { flushed: count };
2372
+ return result;
2436
2373
  }
2374
+ return policy.exhausted();
2375
+ } catch (error) {
2376
+ return policy.chainError(error);
2377
+ }
2378
+ });
2379
+ return writeChain;
2380
+ }
2381
+ function flushNow() {
2382
+ if (flushTimer !== null) {
2383
+ clearTimeout(flushTimer);
2384
+ flushTimer = null;
2385
+ }
2386
+ if (previews.size === 0) return Promise.resolve({ flushed: 0 });
2387
+ const intent = [...previews.entries()];
2388
+ const buildOperations = () => intent.map(([composite, value]) => {
2389
+ const [skinId, key] = composite.split(" ");
2390
+ return value === null ? { op: "delete", skinId, key } : { op: "set", skinId, key, value };
2391
+ });
2392
+ const count = buildOperations().length;
2393
+ return runWriteOp(buildOperations, {
2394
+ disposed: () => ({ flushed: 0, blocked: "disposed" }),
2395
+ disposedMidflight: () => ({ flushed: 0 }),
2396
+ gated: (blocked) => ({ flushed: 0, blocked }),
2397
+ offline: (error) => {
2398
+ throw error;
2399
+ },
2400
+ readOnly: (conflictBody) => ({ flushed: 0, blocked: "conflict", errorMessage: conflictBody?.error ?? "" }),
2401
+ // ADR-0003: retry once only after a conflict refetch has actually
2402
+ // applied a newer normal-mode snapshot.
2403
+ onConflict: async ({ attempt, attemptedRevision }) => {
2404
+ if (attempt === 1) {
2405
+ setFlushFailure("REVISION_CONFLICT", null);
2406
+ emit();
2407
+ return { flushed: 0, blocked: "conflict" };
2408
+ }
2409
+ const refreshed = await refetch();
2410
+ const mayRetry = refreshed.status === "synced" && refreshed.mode === "normal" && refreshed.revision !== attemptedRevision;
2411
+ if (mayRetry) return RETRY;
2412
+ if (refreshed.mode === "recovery") {
2413
+ setFlushFailure(null, null);
2414
+ emit();
2415
+ return { flushed: 0, blocked: "recovery" };
2416
+ }
2417
+ if (refreshed.status === "offline-failed") {
2418
+ setFlushFailure(null, null);
2419
+ emit();
2420
+ return { flushed: 0, blocked: "offline" };
2421
+ }
2422
+ if (refreshed.status === "unsupported-readonly") {
2423
+ setFlushFailure(null, null);
2424
+ emit();
2425
+ return { flushed: 0, blocked: "unsupported" };
2426
+ }
2427
+ setFlushFailure("REVISION_CONFLICT", null);
2428
+ emit();
2437
2429
  return { flushed: 0, blocked: "conflict" };
2438
- } catch {
2430
+ },
2431
+ onRecoveryRequired: async (errorMessage) => {
2432
+ const refreshed = await refetch();
2433
+ if (refreshed.mode === "recovery") {
2434
+ setFlushFailure(null, null);
2435
+ emit();
2436
+ return { flushed: 0, blocked: "recovery" };
2437
+ }
2438
+ setFlushFailure(null, errorMessage);
2439
+ emit();
2440
+ return { flushed: 0, blocked: "error", errorMessage };
2441
+ },
2442
+ onUnknown409: ({ errorMessage }) => {
2443
+ setFlushFailure(null, errorMessage);
2444
+ emit();
2445
+ return { flushed: 0, blocked: "error", errorMessage };
2446
+ },
2447
+ onError: async (response) => {
2448
+ const errorBody = await response.json().catch(() => null);
2449
+ const errorMessage = errorBody?.error ?? `HTTP ${response.status}`;
2450
+ const issues = Array.isArray(errorBody?.params?.issues) ? errorBody.params.issues : null;
2451
+ let excised = false;
2452
+ let onlyDangling = issues !== null && issues.length > 0;
2453
+ if (issues !== null) {
2454
+ for (const issue of issues) {
2455
+ const dangling = issue.code === "MISSING_ASSET" || issue.code === "BAD_ASSET";
2456
+ if (!dangling) onlyDangling = false;
2457
+ if (dangling && typeof issue.skinId === "string" && typeof issue.key === "string") {
2458
+ const composite = `${issue.skinId} ${issue.key}`;
2459
+ if (previews.has(composite)) {
2460
+ previews.delete(composite);
2461
+ excised = true;
2462
+ noteReverted({ skinId: issue.skinId, key: issue.key });
2463
+ }
2464
+ }
2465
+ }
2466
+ }
2467
+ const healed = onlyDangling && excised;
2468
+ setFlushFailure(null, healed ? null : errorMessage);
2469
+ emit();
2470
+ if (excised && previews.size > 0) void flushNow();
2471
+ if (healed) return { flushed: 0, blocked: null, selfHealed: true };
2472
+ return { flushed: 0, blocked: "error", errorMessage };
2473
+ },
2474
+ onSuccess: async (response) => {
2475
+ await response.json().catch(() => null);
2476
+ for (const [composite, value] of intent) {
2477
+ if (previews.get(composite) === value) previews.delete(composite);
2478
+ }
2479
+ setFlushFailure(null, null);
2480
+ notices = [];
2481
+ return { flushed: count };
2482
+ },
2483
+ exhausted: () => ({ flushed: 0, blocked: "conflict" }),
2484
+ chainError: () => {
2439
2485
  setFlushFailure(null, null);
2440
2486
  emit();
2441
2487
  return { flushed: 0, blocked: "error" };
2442
2488
  }
2443
2489
  });
2444
- return writeChain;
2445
2490
  }
2446
2491
  function announce() {
2447
2492
  try {
@@ -2567,55 +2612,51 @@ ${references.join("\n")}`,
2567
2612
  return { asset: body.asset };
2568
2613
  }
2569
2614
  function runGalleryOp(operations, goalAchieved) {
2570
- writeChain = writeChain.then(async () => {
2571
- if (disposed) return { error: "disposed" };
2572
- const blocked = writeGate();
2573
- if (blocked !== null) return { error: blocked };
2574
- for (let attempt = 0; attempt < 2; attempt += 1) {
2575
- let response;
2576
- try {
2577
- response = await request("/config", {
2578
- method: "PATCH",
2579
- headers: { "content-type": "application/json" },
2580
- body: JSON.stringify({ baseRevision: snapshot.revision, operations })
2581
- });
2582
- } catch {
2583
- return { error: "OFFLINE" };
2584
- }
2585
- if (disposed) return { error: "disposed" };
2586
- if (response.status === 409) {
2587
- const conflictBody = await response.json().catch(() => null);
2588
- if (conflictBody?.code === "STORE_READONLY") {
2589
- setFlushFailure(null, null);
2590
- setStatus("unsupported-readonly");
2591
- return { error: "conflict" };
2592
- }
2615
+ const conflictDisposition = async ({ attempt }) => {
2616
+ await refetch();
2617
+ if (goalAchieved()) return { affectedSkins: [] };
2618
+ if (attempt === 1) {
2619
+ setFlushFailure(null, null);
2620
+ emit();
2621
+ return { error: "REVISION_CONFLICT" };
2622
+ }
2623
+ return RETRY;
2624
+ };
2625
+ return runWriteOp(() => operations, {
2626
+ disposed: () => ({ error: "disposed" }),
2627
+ disposedMidflight: () => ({ error: "disposed" }),
2628
+ gated: (blocked) => ({ error: blocked }),
2629
+ offline: () => ({ error: "OFFLINE" }),
2630
+ readOnly: () => ({ error: "conflict" }),
2631
+ onConflict: conflictDisposition,
2632
+ // ADR-0018 alignment: the recovery code is dispatched BY NAME now —
2633
+ // never folded into the blind conflict retry. Code passthrough: the
2634
+ // Host-stable code reaches the caller unsuppressed (host-errors.js
2635
+ // resolves it), after one refetch so the recovery banner can land.
2636
+ onRecoveryRequired: async () => {
2637
+ await refetch();
2638
+ return { error: "STORE_RECOVERY_REQUIRED" };
2639
+ },
2640
+ onUnknown409: conflictDisposition,
2641
+ // unknown 409s keep the goal-first read
2642
+ onError: async (response) => {
2643
+ const body = await response.json().catch(() => null);
2644
+ if (body?.code === "INVALID_CONFIG" && body.params?.issues?.some((issue) => issue.code === "ASSET_NOT_FOUND")) {
2593
2645
  await refetch();
2594
2646
  if (goalAchieved()) return { affectedSkins: [] };
2595
- if (attempt === 1) {
2596
- setFlushFailure(null, null);
2597
- emit();
2598
- return { error: "REVISION_CONFLICT" };
2599
- }
2600
- continue;
2601
- }
2602
- if (!response.ok) {
2603
- const body2 = await response.json().catch(() => null);
2604
- if (body2?.code === "INVALID_CONFIG" && body2.params?.issues?.some((issue) => issue.code === "ASSET_NOT_FOUND")) {
2605
- await refetch();
2606
- if (goalAchieved()) return { affectedSkins: [] };
2607
- return { error: "ASSET_NOT_FOUND" };
2608
- }
2609
- return { error: body2?.code ?? `HTTP ${response.status}` };
2647
+ return { error: "ASSET_NOT_FOUND" };
2610
2648
  }
2649
+ return { error: body?.code ?? `HTTP ${response.status}` };
2650
+ },
2651
+ onSuccess: async (response) => {
2611
2652
  const body = await response.json().catch(() => null);
2612
- await refetch();
2613
- announce();
2614
2653
  return { affectedSkins: body?.affectedSkins ?? [], revision: body?.revision };
2654
+ },
2655
+ exhausted: () => ({ error: "REVISION_CONFLICT" }),
2656
+ chainError: (error) => {
2657
+ throw error;
2615
2658
  }
2616
- return { error: "REVISION_CONFLICT" };
2617
2659
  });
2618
- return writeChain;
2619
2660
  }
2620
2661
  async function deleteImage(id) {
2621
2662
  const blocked = writeGate();
@@ -2806,11 +2847,6 @@ ${references.join("\n")}`,
2806
2847
  }
2807
2848
 
2808
2849
  // src/client/skins/translucency.js
2809
- function ridingDecl(table2, tokenKey, mode) {
2810
- const [rgb, baked] = table2.riding[tokenKey][mode];
2811
- const alpha = (Math.min(100, Math.max(0, table2.anchor + baked - table2.anchor)) / 100).toFixed(2);
2812
- return `${tokenKey}: rgba(${rgb}, ${alpha});`;
2813
- }
2814
2850
  function glassRule(bodyAttribute, glassVar) {
2815
2851
  const attr = bodyAttribute.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
2816
2852
  return `body[data-${attr}] [id="root"]{backdrop-filter:blur(var(${glassVar},0px))}`;
@@ -3173,31 +3209,9 @@ ${references.join("\n")}`,
3173
3209
  "--dsw-specific-input-major": { light: ["255, 255, 255", 60], dark: ["18, 42, 60", 65] },
3174
3210
  "--dsw-specific-login-input": { light: ["255, 255, 255", 60], dark: ["18, 42, 60", 65] }
3175
3211
  };
3176
- const SPEC = { anchor: 55, riding: RIDING };
3177
3212
  const CSS3 = `
3178
- /* ---------- ③ 配色:常量族已迁声明表 constantTokens(ADR-0016 C1),
3179
- 此处只留随动族 ridingDecl 插值行(ADR-0004 锚点字节不变式的载体) ---------- */
3180
- body[data-dsh-openbmc-skin] {
3181
- ${ridingDecl(SPEC, "--dsw-alias-bg-base", "light")}
3182
- ${ridingDecl(SPEC, "--dsw-alias-bg-module-platform", "light")}
3183
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-1", "light")}
3184
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-2", "light")}
3185
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-3", "light")}
3186
- ${ridingDecl(SPEC, "--dsw-specific-sidebar-fill", "light")}
3187
- ${ridingDecl(SPEC, "--dsw-specific-input-major", "light")}
3188
- ${ridingDecl(SPEC, "--dsw-specific-login-input", "light")}
3189
- }
3190
-
3191
- body[data-dsh-openbmc-skin][data-ds-dark-theme] {
3192
- ${ridingDecl(SPEC, "--dsw-alias-bg-base", "dark")}
3193
- ${ridingDecl(SPEC, "--dsw-alias-bg-module-platform", "dark")}
3194
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-1", "dark")}
3195
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-2", "dark")}
3196
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-3", "dark")}
3197
- ${ridingDecl(SPEC, "--dsw-specific-sidebar-fill", "dark")}
3198
- ${ridingDecl(SPEC, "--dsw-specific-input-major", "dark")}
3199
- ${ridingDecl(SPEC, "--dsw-specific-login-input", "dark")}
3200
- }
3213
+ /* ---------- ③ 配色:token(常量族 + 随动族)一律走声明表 → constantTokens /
3214
+ riding → tokenOverrides(ADR-0016 C1 + ADR-0017),CSS 模板只持品牌装饰段 ---------- */
3201
3215
 
3202
3216
  /* ---------- ④b 品牌徽章:官方 ▐HARNESS▌ 同构反色片 ----------
3203
3217
  * 规则唯一来源在 ../decoration.js badgeCss(ADR-0016;裁决史见其头注),
@@ -3360,28 +3374,9 @@ ${references.join("\n")}`,
3360
3374
  "--dsw-specific-sidebar-fill": { light: ["238, 235, 255", 60], dark: ["25, 20, 48", 72] },
3361
3375
  "--dsw-specific-input-major": { light: ["255, 255, 255", 62], dark: ["42, 34, 78", 55] }
3362
3376
  };
3363
- const SPEC = { anchor: 55, riding: RIDING };
3364
3377
  const css = `
3365
- /* 配色常量族已迁声明表 constantTokens(ADR-0016 C1);此处只留随动族
3366
- * ridingDecl 插值行(ADR-0004 锚点字节不变式的载体)。 */
3367
- body[data-dsh-uefi-harness] {
3368
- ${ridingDecl(SPEC, "--dsw-alias-bg-base", "light")}
3369
- ${ridingDecl(SPEC, "--dsw-alias-bg-module-platform", "light")}
3370
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-1", "light")}
3371
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-2", "light")}
3372
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-3", "light")}
3373
- ${ridingDecl(SPEC, "--dsw-specific-sidebar-fill", "light")}
3374
- ${ridingDecl(SPEC, "--dsw-specific-input-major", "light")}
3375
- }
3376
- body[data-dsh-uefi-harness][data-ds-dark-theme] {
3377
- ${ridingDecl(SPEC, "--dsw-alias-bg-base", "dark")}
3378
- ${ridingDecl(SPEC, "--dsw-alias-bg-module-platform", "dark")}
3379
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-1", "dark")}
3380
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-2", "dark")}
3381
- ${ridingDecl(SPEC, "--dsw-alias-bg-layer-3", "dark")}
3382
- ${ridingDecl(SPEC, "--dsw-specific-sidebar-fill", "dark")}
3383
- ${ridingDecl(SPEC, "--dsw-specific-input-major", "dark")}
3384
- }
3378
+ /* 配色 token(常量族 + 随动族)一律走声明表 → constantTokens / riding →
3379
+ * tokenOverrides(ADR-0016 C1 + ADR-0017),CSS 模板只持品牌装饰段。 */
3385
3380
  ${rootClear("body[data-dsh-uefi-harness]")}
3386
3381
  /* 流式呼吸光条(ADR-0016 视觉窗口:静态版升级呼吸,衰减色 = 暗色品牌主色
3387
3382
  * @0.20,Q16):规则唯一来源在 ../decoration.js streamingCss。 */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-skins",
3
3
  "description": "DSH Web 皮肤插件:多套品牌皮肤侧栏热切换。",
4
- "version": "1.5.14",
4
+ "version": "1.5.16",
5
5
  "homepage": "https://github.com/iasiv5/dsh-skins#readme",
6
6
  "repository": {
7
7
  "type": "git",