dsh-background-by-model 0.8.2 → 0.9.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.
- package/CHANGELOG.md +17 -4
- package/CHANGELOG.zh.md +17 -1
- package/README.md +8 -10
- package/README.zh.md +8 -9
- package/lib/client.js +1747 -1489
- package/lib/index.js +228 -379
- package/package.json +4 -2
package/lib/index.js
CHANGED
|
@@ -19,23 +19,16 @@ const HOLIDAYS = [{
|
|
|
19
19
|
/**
|
|
20
20
|
* The persisted configuration shape — declared exactly ONCE, for both halves.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* the two halves cannot disagree about the shape. The host additionally logs one
|
|
33
|
-
* warning per unknown key while sanitizing a write (see `warnUnknownConfigKeys`
|
|
34
|
-
* in `src/index.ts`), so a field that reaches the node half without being
|
|
35
|
-
* declared here shows up in the host log instead of quietly disappearing.
|
|
36
|
-
*
|
|
37
|
-
* Nothing in this module may touch the DOM, Node or the Cordis context: it is
|
|
38
|
-
* imported by the browser bundle and by the node half alike.
|
|
22
|
+
* The types, the key lists, the defaults and the pure normalizers live here and
|
|
23
|
+
* both halves import them, so adding a field means editing this file and the two
|
|
24
|
+
* halves cannot disagree about the shape. (This was written twice until 0.3.2,
|
|
25
|
+
* when a field declared on one side was silently dropped by the other side's
|
|
26
|
+
* sanitizer.) The host also logs one warning per unknown key while sanitizing a
|
|
27
|
+
* write (`warnUnknownConfigKeys` in `src/index.ts`), so a field that reaches the
|
|
28
|
+
* node half without being declared here shows up in the log.
|
|
29
|
+
*
|
|
30
|
+
* Nothing here may touch the DOM, Node or the Cordis context: it is imported by
|
|
31
|
+
* the browser bundle and by the node half alike.
|
|
39
32
|
*/
|
|
40
33
|
const DEFAULT_BG_STATE = {
|
|
41
34
|
zoom: 1,
|
|
@@ -70,14 +63,14 @@ function defaultRotation() {
|
|
|
70
63
|
return { ...DEFAULT_ROTATION };
|
|
71
64
|
}
|
|
72
65
|
/**
|
|
73
|
-
* Main interface opacities (0..1), global (Interface page)
|
|
74
|
-
*
|
|
75
|
-
* `
|
|
76
|
-
* `
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* `input`
|
|
66
|
+
* Main interface opacities (0..1), global (Interface page) — each part is the token
|
|
67
|
+
* set it owns:
|
|
68
|
+
* `bg` `--dsw-alias-bg-base`
|
|
69
|
+
* `sidebar` `--dsw-specific-sidebar-fill`
|
|
70
|
+
* `card` `--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu` (and its alias
|
|
71
|
+
* `--dsw-menu-surface-fill`)
|
|
72
|
+
* `code` `--dsw-alias-markdown-code-block`, `…-banner`
|
|
73
|
+
* `input` `--dsw-specific-input-major`
|
|
81
74
|
*/
|
|
82
75
|
const PART_OPACITY_KEYS = [
|
|
83
76
|
"bg",
|
|
@@ -120,12 +113,10 @@ const DEFAULT_TRAJECTORY_OPACITY = .73;
|
|
|
120
113
|
const DEFAULT_RIGHTBAR_OPACITY = .45;
|
|
121
114
|
/**
|
|
122
115
|
* `#RRGGBB` → the `[h, s, l]` triple every rule stores (`s` and `l` in 0..1), or
|
|
123
|
-
* null when the string is not a hex
|
|
116
|
+
* null when the string is not a hex colour.
|
|
124
117
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* conversion lives here rather than in ./holiday because `[h, s, l]` is this
|
|
128
|
-
* module's own rule shape.
|
|
118
|
+
* Holidays name their colour as hex (`HOLIDAYS`) because a hand-converted triple
|
|
119
|
+
* is a magic number nothing downstream can check.
|
|
129
120
|
*/
|
|
130
121
|
function hexToHsl(hex) {
|
|
131
122
|
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
@@ -158,11 +149,8 @@ function hexToHsl(hex) {
|
|
|
158
149
|
/**
|
|
159
150
|
* One holiday's appearance, untouched.
|
|
160
151
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
* art was not cut for. The mode now lives on the IMAGE (see `BgImage.bgMode`), so
|
|
164
|
-
* that default is written there — and `fill` is what a holiday's read-back
|
|
165
|
-
* sanitizer refuses to lose (see `normalizeHolidayRule`).
|
|
152
|
+
* The mode is `fill` rather than a rule's `fit`: full-bleed festival art would be
|
|
153
|
+
* letterboxed by `fit` on every aspect ratio it was not cut for.
|
|
166
154
|
*/
|
|
167
155
|
function defaultHolidayRule(def) {
|
|
168
156
|
const color = hexToHsl(def.color);
|
|
@@ -184,21 +172,13 @@ function defaultHolidayRule(def) {
|
|
|
184
172
|
};
|
|
185
173
|
}
|
|
186
174
|
/**
|
|
187
|
-
* A rule with nothing in it yet: no picture, no match string, no
|
|
188
|
-
*
|
|
189
|
-
* Two states need this factory and they are the same state — a fresh install
|
|
190
|
-
* (see `freshThemeConfig` and `normalizeConfig`), and the "add rule" tile one
|
|
191
|
-
* entry further down the list.
|
|
175
|
+
* A rule with nothing in it yet: no picture, no match string, no colour — a fresh
|
|
176
|
+
* install's rule 1 and the "add rule" tile alike.
|
|
192
177
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* It used to be born with an allocated slot and an empty image entry, which is
|
|
199
|
-
* what put a blank tile at position 1 of a new rule's strip — and, worse, stopped
|
|
200
|
-
* the picture the user added next from being the first one (a rule paints its
|
|
201
|
-
* FIRST image). The slot is allocated when the first picture arrives.
|
|
178
|
+
* Valid rather than half-built: `ruleCanPaint` skips it until it has something of
|
|
179
|
+
* its own, so it cannot steal the fallback from rule 1. It deliberately carries NO
|
|
180
|
+
* image entry: a rule paints its FIRST image, and a placeholder slot left the
|
|
181
|
+
* picture the user added next in position 2. The slot is allocated on arrival.
|
|
202
182
|
*/
|
|
203
183
|
function blankRule(id) {
|
|
204
184
|
return {
|
|
@@ -279,18 +259,12 @@ function legacyFadeMs(rawRules) {
|
|
|
279
259
|
}
|
|
280
260
|
}
|
|
281
261
|
/**
|
|
282
|
-
* Coerce one persisted transition block; a config
|
|
283
|
-
*
|
|
284
|
-
* the behaviour it already had.
|
|
285
|
-
*
|
|
286
|
-
* `legacyMs` is the per-rule `fadeMs` this shape replaced (see `legacyFadeMs`),
|
|
287
|
-
* adopted only when the incoming config has no `durationMs` of its own — so a
|
|
288
|
-
* deliberate hand-tuned blend survives the upgrade instead of being dropped with
|
|
289
|
-
* the field it used to live on.
|
|
262
|
+
* Coerce one persisted transition block; a config without one gets the shipped
|
|
263
|
+
* default, which is what it was already showing.
|
|
290
264
|
*
|
|
291
|
-
* `
|
|
292
|
-
*
|
|
293
|
-
*
|
|
265
|
+
* `legacyMs` is the per-rule `fadeMs` this shape replaced (`legacyFadeMs`), adopted
|
|
266
|
+
* only when the incoming config carries no `durationMs` of its own. `0` is kept as
|
|
267
|
+
* a real value (a hard cut), not read as "absent".
|
|
294
268
|
*/
|
|
295
269
|
function normalizeTransition(raw, legacyMs) {
|
|
296
270
|
const t = raw ?? {};
|
|
@@ -305,25 +279,15 @@ function normalizeTransition(raw, legacyMs) {
|
|
|
305
279
|
/**
|
|
306
280
|
* Coerce one persisted image entry, or null when it lacks a usable slot.
|
|
307
281
|
*
|
|
308
|
-
* `inherit`
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
* silently dropping to the system palette, and it happens in the shared sanitizer
|
|
313
|
-
* so both halves lift identically.
|
|
282
|
+
* `inherit` (the RULE's colour) is applied only to an entry with no `color` KEY at
|
|
283
|
+
* all — the shape written before per-image colours, where the rule's colour was the
|
|
284
|
+
* only one there was. The key's PRESENCE is the whole distinction: `color: null` is
|
|
285
|
+
* a deliberate "follow the system theme" and must not be re-inherited on every load.
|
|
314
286
|
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* `inheritMode` is the same one-shot lift for the layout mode, and it is the
|
|
321
|
-
* caller's to resolve because the two callers answer differently: a user rule
|
|
322
|
-
* passes the mode its config carried at the RULE level (a schema-5 config, where
|
|
323
|
-
* 适应/填充 was one choice for the whole rotation), a holiday passes `fill` (its
|
|
324
|
-
* own default, which is not up for the lift to change). An entry that HAS a
|
|
325
|
-
* `bgMode` key keeps it — including one that has just been set to a value equal
|
|
326
|
-
* to the default, which is a decision and not an absence.
|
|
287
|
+
* `inheritMode` is the same one-shot lift for the layout mode. It is the caller's to
|
|
288
|
+
* resolve because the callers answer differently: a user rule passes the mode its
|
|
289
|
+
* config carried at the RULE level (schema 5), a holiday passes its own `fill`. An
|
|
290
|
+
* entry that HAS a `bgMode` key keeps it, even when it equals the default.
|
|
327
291
|
*/
|
|
328
292
|
function normalizeImage(raw, inherit = null, inheritMode = "fit") {
|
|
329
293
|
const i = raw ?? {};
|
|
@@ -336,36 +300,21 @@ function normalizeImage(raw, inherit = null, inheritMode = "fit") {
|
|
|
336
300
|
};
|
|
337
301
|
}
|
|
338
302
|
/**
|
|
339
|
-
* Coerce one persisted rule, or null when it names no image
|
|
340
|
-
*
|
|
341
|
-
* Accepts BOTH image shapes: the 0.7 list (`images`), and the single
|
|
342
|
-
* `slot`/`bgState` pair every earlier version wrote — the latter is lifted to a
|
|
343
|
-
* one-entry list here rather than at a dozen call sites, which is what keeps the
|
|
344
|
-
* rest of the codebase free of "old config" branches. Duplicate slots inside one
|
|
345
|
-
* rule are dropped: two entries pointing at the same bytes would only make the
|
|
346
|
-
* rotation look stuck.
|
|
303
|
+
* Coerce one persisted rule, or null when it names no image ANYWHERE.
|
|
347
304
|
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
305
|
+
* Both image shapes are accepted here and nowhere else: the 0.7 list (`images`),
|
|
306
|
+
* and the single `slot`/`bgState` pair every earlier version wrote, lifted into a
|
|
307
|
+
* one-entry list. That is what keeps the rest of the codebase free of "old config"
|
|
308
|
+
* branches. Duplicate slots inside one rule are dropped — two entries pointing at
|
|
309
|
+
* the same bytes only make a rotation look stuck.
|
|
352
310
|
*
|
|
353
|
-
* The
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* therefore survives the move the same way it survived the color's — the mode is
|
|
358
|
-
* not reset to the default on upgrade, and the field it used to live on is simply
|
|
359
|
-
* not written back.
|
|
311
|
+
* The rule's colour is resolved FIRST and handed to every image entry (what a
|
|
312
|
+
* pre-0.7.1 entry inherits), and the rule keeps it: it is what the rule paints once
|
|
313
|
+
* its last picture is gone. The layout mode makes the same trip from the RULE level
|
|
314
|
+
* a pre-0.7.5 config carries, so the user's choice survives the move.
|
|
360
315
|
*
|
|
361
|
-
* An EMPTY list is a legitimate rule
|
|
362
|
-
*
|
|
363
|
-
* phantom blank image that took slot 1 and could never be deleted again (the next
|
|
364
|
-
* upload landed at position 2 and the rule kept painting nothing). What such a
|
|
365
|
-
* rule then does is `ruleCanPaint`'s business: with a color of its own it paints
|
|
366
|
-
* the interface, with neither it is skipped by matching — which the card's own
|
|
367
|
-
* hint says in so many words. Only a rule that names no image anywhere — no list,
|
|
368
|
-
* no legacy slot, or a slot that could never be a filename — is dropped.
|
|
316
|
+
* An EMPTY list is a legitimate rule: removing a rule's last picture has to be
|
|
317
|
+
* expressible. `ruleCanPaint` decides what such a rule then does.
|
|
369
318
|
*/
|
|
370
319
|
function normalizeRule(raw) {
|
|
371
320
|
const r = raw ?? {};
|
|
@@ -409,17 +358,11 @@ function normalizeRule(raw) {
|
|
|
409
358
|
/**
|
|
410
359
|
* Coerce one persisted holiday entry.
|
|
411
360
|
*
|
|
412
|
-
* `id`,
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* Everything else is sanitized exactly like a rule's field — except three things
|
|
418
|
-
* that stay the definition's own: the image list and the rotation (so a festival
|
|
419
|
-
* can never be swapped or cycled), and the layout mode, which is the definition's
|
|
420
|
-
* `fill` for the same reason the color is its own — full-bleed art letterboxed by
|
|
421
|
-
* a stale `fit` is the one thing this feature must not do, and the mode is not
|
|
422
|
-
* editable for a holiday anywhere in the panel.
|
|
361
|
+
* `id`, the image list and every theme colour come from the DEFINITION, never from
|
|
362
|
+
* disk: a stale or hand-edited value would orphan the wallpaper, point the holiday
|
|
363
|
+
* at a rule's image, or paint a festival in a colour it does not have. The layout
|
|
364
|
+
* mode and the rotation stay the definition's own for the same reason (a festival
|
|
365
|
+
* is never swapped or cycled). Everything else is sanitized like a rule's field.
|
|
423
366
|
*/
|
|
424
367
|
function normalizeHolidayRule(def, raw) {
|
|
425
368
|
const r = raw ?? {};
|
|
@@ -452,21 +395,12 @@ function normalizeHolidays(raw) {
|
|
|
452
395
|
};
|
|
453
396
|
}
|
|
454
397
|
/**
|
|
455
|
-
* The file-preview panel's own opacity
|
|
398
|
+
* The file-preview panel's own opacity: a number is the card's own value, `null` is
|
|
399
|
+
* "never owned, follow `opacities.bg`", and a MISSING key is the shipped 45%.
|
|
456
400
|
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* there at all — a hand-written config, or one written before the option existed
|
|
461
|
-
* — is the shipped profile's 45% (see DEFAULT_RIGHTBAR_OPACITY).
|
|
462
|
-
*
|
|
463
|
-
* `null` has to survive as ITSELF instead of falling through to the default:
|
|
464
|
-
* every release so far has written it — a panel the user never dragged is
|
|
465
|
-
* persisted as an explicit `null` — so treating it as "missing" would hand the
|
|
466
|
-
* shipped 45% to every configuration whose preview panel was following the main
|
|
467
|
-
* background, a silent change to a setup nobody asked to change. It is also
|
|
468
|
-
* unreachable from the UI (no control returns to it); it exists for configs that
|
|
469
|
-
* were written while the shipped default was `null`.
|
|
401
|
+
* `null` has to survive as itself: every release so far persisted it for a panel the
|
|
402
|
+
* user never dragged, so treating it as missing would silently hand the shipped 45%
|
|
403
|
+
* to every configuration that was following the main background.
|
|
470
404
|
*/
|
|
471
405
|
function normalizeRightbarOpacity(v) {
|
|
472
406
|
if (v === null) return null;
|
|
@@ -534,20 +468,19 @@ function presetRetryDelayMs(failedAttempt) {
|
|
|
534
468
|
/**
|
|
535
469
|
* Node half of dsh-background-by-model: file-backed rule persistence.
|
|
536
470
|
*
|
|
537
|
-
* Owns the `~/.dsh/.dsh-background-by-model-data/` store and exposes
|
|
538
|
-
* surface on the
|
|
539
|
-
* `/api`,
|
|
471
|
+
* Owns the `~/.dsh/.dsh-background-by-model-data/` store and exposes the RPC
|
|
472
|
+
* surface on the plugin's own `/dsh-background-by-model` channel (never the shared
|
|
473
|
+
* `/api`, which slash commands own).
|
|
540
474
|
*
|
|
541
|
-
* theme-config.json
|
|
542
|
-
* modelbg-<slot>
|
|
543
|
-
*
|
|
544
|
-
* holiday-cache/<tag>/ festival art downloaded from the CDN
|
|
545
|
-
*
|
|
546
|
-
* holiday asset inside it, deliberately NOT a slot (below)
|
|
475
|
+
* theme-config.json the ordered rule list + the global interface settings
|
|
476
|
+
* modelbg-<slot> one background image per rule (raw bytes, no extension;
|
|
477
|
+
* the MIME is sniffed from the magic bytes when served)
|
|
478
|
+
* holiday-cache/<tag>/ festival art downloaded from the CDN, one directory per
|
|
479
|
+
* pinned release, deliberately NOT in the slot namespace
|
|
547
480
|
*
|
|
548
|
-
* Images travel as data URLs over the RPC channel
|
|
549
|
-
*
|
|
550
|
-
*
|
|
481
|
+
* Images travel as data URLs over the RPC channel; there is no separate HTTP image
|
|
482
|
+
* route. A legacy single-wallpaper / video-background store is migrated to rule 1
|
|
483
|
+
* on first read.
|
|
551
484
|
*/
|
|
552
485
|
const name = "dsh-background-by-model";
|
|
553
486
|
const inject = ["connection", "webServer"];
|
|
@@ -569,105 +502,59 @@ const WALLPAPER_FETCH_TIMEOUT = 2e4;
|
|
|
569
502
|
/**
|
|
570
503
|
* Where the festival art is hosted, and which revision of it this build wants.
|
|
571
504
|
*
|
|
572
|
-
* The
|
|
573
|
-
*
|
|
574
|
-
* downloaded — for art that is looked at on one day a year, and they are the one
|
|
575
|
-
* part of the package that is a picture rather than code. The bytes now live in
|
|
576
|
-
* the `assets` repository beside the README screenshots and are fetched from a
|
|
577
|
-
* CDN the first time a holiday actually needs one (see `readHolidayAsset`).
|
|
505
|
+
* The art is fetched from the `assets` repository the first time a holiday needs it
|
|
506
|
+
* (see `readHolidayAsset`), never shipped in the package.
|
|
578
507
|
*
|
|
579
|
-
* The reference is a **release tag**, never a branch
|
|
580
|
-
* immutable
|
|
581
|
-
* screenshots silently kill a shipped feature
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
* README's screenshots and the recommended profile are read at, so one release
|
|
586
|
-
* of the assets repository is one version of every byte this plugin fetches,
|
|
587
|
-
* where a raw commit hash was a value nobody could compare against anything.
|
|
588
|
-
* The cost is unchanged — new art means a new tag there and a new value here —
|
|
589
|
-
* with one addition: a tag that is MOVED does not reach jsDelivr's edge on its
|
|
590
|
-
* own (the reference is served as immutable), so re-pointing one means a purge
|
|
591
|
-
* request. Cutting a new tag is the cheap move.
|
|
508
|
+
* The reference is a **release tag**, never a branch: runtime art has to be
|
|
509
|
+
* immutable, and a branch reference would let a reshuffle of the documentation
|
|
510
|
+
* screenshots silently kill a shipped feature. One tag also means one version of
|
|
511
|
+
* everything the plugin fetches (art, profile, README screenshots). A MOVED tag
|
|
512
|
+
* does not reach jsDelivr's edge on its own, so new art means a new tag, never a
|
|
513
|
+
* force-push of the old one.
|
|
592
514
|
*/
|
|
593
515
|
const HOLIDAY_ASSETS_REPO = "HarmlessFunny/assets";
|
|
594
516
|
const HOLIDAY_ASSETS_REF = "v0.1.0";
|
|
595
517
|
const HOLIDAY_ASSETS_DIR = "dsh-background-by-model/holiday";
|
|
596
518
|
/**
|
|
597
|
-
* Mirrors, tried in order
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
* egg. Every mirror serves the same version, so a fallback cannot mix revisions.
|
|
519
|
+
* Mirrors, tried in order: jsDelivr is reachable where `raw.githubusercontent.com`
|
|
520
|
+
* is not, and raw is the fallback for a CDN that is blocked or down. Both serve the
|
|
521
|
+
* same version, so a fallback cannot mix revisions.
|
|
601
522
|
*/
|
|
602
523
|
const HOLIDAY_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${HOLIDAY_ASSETS_REPO}@${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`, `https://raw.githubusercontent.com/${HOLIDAY_ASSETS_REPO}/${HOLIDAY_ASSETS_REF}/${HOLIDAY_ASSETS_DIR}`];
|
|
603
524
|
/**
|
|
604
525
|
* Sub-directory of the store holding downloaded festival art: one directory per
|
|
605
526
|
* release of the asset repository, one file per asset inside it.
|
|
606
527
|
*
|
|
607
|
-
*
|
|
608
|
-
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
* holiday slot stay read-only — `writeImage` still refuses it, and the store
|
|
612
|
-
* still never holds a `modelbg-h-*` file — while the art itself is now fetched
|
|
613
|
-
* rather than shipped.
|
|
528
|
+
* NOT part of the slot namespace, and must never become part of it: files are named
|
|
529
|
+
* after the ASSET and their parent after the pinned release, never after a slot, so
|
|
530
|
+
* nothing here can be mistaken for a user's `modelbg-<slot>`. That is what lets a
|
|
531
|
+
* holiday slot stay read-only while the art itself is downloaded.
|
|
614
532
|
*
|
|
615
|
-
* The version segment is load-bearing
|
|
616
|
-
* a NEW generation with the OLD bytes and
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* is a cache MISS instead of a stale hit. What happens to the generation that
|
|
621
|
-
* leaves behind is `pruneHolidayCache`.
|
|
533
|
+
* The version segment is load-bearing: a cache keyed by asset name alone would
|
|
534
|
+
* answer a NEW generation with the OLD bytes and say nothing about it. With the tag
|
|
535
|
+
* in the path, a build that pins a new tag looks in a directory nobody has written
|
|
536
|
+
* to yet, so the first holiday after an update is a miss. Superseded generations
|
|
537
|
+
* are swept by `pruneHolidayCache`.
|
|
622
538
|
*/
|
|
623
539
|
const HOLIDAY_CACHE_DIR = "holiday-cache";
|
|
624
540
|
/** Cap and timeout for one asset download; these files are 228 KB and 500 KB. */
|
|
625
541
|
const HOLIDAY_FETCH_MAX = 8388608;
|
|
626
542
|
const HOLIDAY_FETCH_TIMEOUT = 1e4;
|
|
627
543
|
/**
|
|
628
|
-
* The recommended profile, served from the same assets repository as the
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
* where 99.8% of the bytes were a picture, so changing the `match` string of one
|
|
633
|
-
* rule meant finding it inside a single line of several hundred thousand
|
|
634
|
-
* characters. Now the published layout is exactly the layout of
|
|
635
|
-
* `~/.dsh/.dsh-background-by-model-data/` — a readable `theme-config.json` and
|
|
636
|
-
* one raw file per slot — which makes the configuration editable in place and
|
|
637
|
-
* makes publishing it a `cp` out of the data directory rather than a special
|
|
638
|
-
* export step. The bytes on the wire are the same; only their arrangement is
|
|
639
|
-
* different.
|
|
544
|
+
* The recommended profile, served from the same assets repository as the festival
|
|
545
|
+
* art — as a DIRECTORY in the store's own shape: a readable `theme-config.json`
|
|
546
|
+
* plus one raw file per slot. Publishing it is a `cp` out of the data directory,
|
|
547
|
+
* not an export step.
|
|
640
548
|
*
|
|
641
549
|
* Nothing lists the images: the request set is DERIVED from the config's own
|
|
642
|
-
* `rules[].images[].slot`, so adding a wallpaper is
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
* missing), and a branch bought the ability to correct a bad recommendation
|
|
651
|
-
* without a plugin release.
|
|
652
|
-
*
|
|
653
|
-
* That trade is now the other way round. What a branch also bought was a plugin
|
|
654
|
-
* whose bytes could change under the user between the build and the click, and
|
|
655
|
-
* with one tag covering the screenshots, the festival art and this profile, one
|
|
656
|
-
* release of the assets repository is one version of everything the plugin
|
|
657
|
-
* fetches — the same value in the README, in `HOLIDAY_ASSETS_REF` and here. The
|
|
658
|
-
* price is that correcting one JSON file now costs a tag in `assets`, a new
|
|
659
|
-
* value in this file and a release on npm, and that MOVING the tag does not
|
|
660
|
-
* reach jsDelivr's edge by itself (it is served as immutable, and the purge tool
|
|
661
|
-
* is the only way to re-point it) — so a correction is a new tag, never a
|
|
662
|
-
* force-push of the old one.
|
|
663
|
-
*
|
|
664
|
-
* The re-pointed-tag case is why `presetKey` survives this change: an edge that
|
|
665
|
-
* has not been purged can still answer with the previous bytes of the same URL,
|
|
666
|
-
* so two fetches of "the profile" are not guaranteed to be one revision, and a
|
|
667
|
-
* press that resumes must not glue them together. See `fetchPresetConfig`.
|
|
668
|
-
*
|
|
669
|
-
* The risk that IS real — a profile written for a config shape this build cannot
|
|
670
|
-
* read — is what `preset.json` exists for (see `fetchPresetConfig`).
|
|
550
|
+
* `rules[].images[].slot`, so adding a wallpaper is a rule entry and a file.
|
|
551
|
+
*
|
|
552
|
+
* Referenced by the same release tag as the art (one tag = one version of
|
|
553
|
+
* everything the plugin fetches). The re-pointed-tag case is why `presetKey`
|
|
554
|
+
* exists: an edge that has not been purged can still answer with the previous
|
|
555
|
+
* bytes of the same URL, and a resumed download must not glue two revisions
|
|
556
|
+
* together (see `fetchPresetConfig`). A profile written for a config shape this
|
|
557
|
+
* build cannot read is refused by the `preset.json` gate.
|
|
671
558
|
*/
|
|
672
559
|
const PRESET_ASSETS_REPO = "HarmlessFunny/assets";
|
|
673
560
|
const PRESET_ASSETS_REF = "v0.1.0";
|
|
@@ -680,28 +567,18 @@ const PRESET_IMAGE_PREFIX = "modelbg-";
|
|
|
680
567
|
/** Mirrors, tried in order — same reason as the festival art's. */
|
|
681
568
|
const PRESET_ASSET_HOSTS = [`https://cdn.jsdelivr.net/gh/${PRESET_ASSETS_REPO}@${PRESET_ASSETS_REF}/${PRESET_ASSET_DIR}`, `https://raw.githubusercontent.com/${PRESET_ASSETS_REPO}/${PRESET_ASSETS_REF}/${PRESET_ASSET_DIR}`];
|
|
682
569
|
/**
|
|
683
|
-
* Caps for the profile, now per file rather than for one document
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
* of what those transfers add up to is bounded on the client instead, because
|
|
696
|
-
* these calls are independent and this side never sees more than one file.
|
|
697
|
-
*
|
|
698
|
-
* The timeout is set by measurement, not by symmetry with the festival art's
|
|
699
|
-
* 10 s. A 350 KB wallpaper takes a second or two over jsDelivr from a normal
|
|
700
|
-
* connection, and the mirror that exists for the case where jsDelivr does not
|
|
701
|
-
* answer is the slower one — so a tight budget would fail exactly in the
|
|
702
|
-
* situation it was written for. A minute tolerates a slow link and is still
|
|
703
|
-
* bounded; the user is watching a button that says it is downloading, which is
|
|
704
|
-
* the one place waiting is honest.
|
|
570
|
+
* Caps for the profile, now per file rather than for one document: a profile is a
|
|
571
|
+
* SET of transfers, and each has a different way of going wrong.
|
|
572
|
+
*
|
|
573
|
+
* The count is the one that matters — the request set is derived from the config,
|
|
574
|
+
* so a hostile config could name a thousand slots and turn one button press into a
|
|
575
|
+
* thousand downloads. `SLOT_RE` bounds each name, `PRESET_IMAGE_COUNT_MAX` bounds
|
|
576
|
+
* how many there are, and the running TOTAL is bounded on the client, which is the
|
|
577
|
+
* only side that sees more than one file.
|
|
578
|
+
*
|
|
579
|
+
* The timeout is set by measurement: a 350 KB wallpaper takes a second or two over
|
|
580
|
+
* jsDelivr, and the fallback mirror is the slower one, so a tight budget would fail
|
|
581
|
+
* exactly in the situation it exists for.
|
|
705
582
|
*/
|
|
706
583
|
const PRESET_MANIFEST_MAX = 65536;
|
|
707
584
|
const PRESET_CONFIG_MAX = 1048576;
|
|
@@ -722,19 +599,18 @@ const exists = async (p) => {
|
|
|
722
599
|
}
|
|
723
600
|
};
|
|
724
601
|
/**
|
|
725
|
-
* One-shot migration of the pre-0.3 store.
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
*
|
|
737
|
-
* config that names no rule list into the one blank rule the panel opens on.
|
|
602
|
+
* One-shot migration of the pre-0.3 store. Idempotent: the rewritten config carries
|
|
603
|
+
* `rules`, so the next read short-circuits.
|
|
604
|
+
*
|
|
605
|
+
* Anything without a `rules` array is an old config: its single wallpaper becomes
|
|
606
|
+
* rule 1 (`m1`, empty match = pure fallback) carrying the old colour, layout mode,
|
|
607
|
+
* opacity, blur and framing, and the remaining legacy files (background video,
|
|
608
|
+
* upload temp) are removed.
|
|
609
|
+
*
|
|
610
|
+
* A store with NO config file at all is NOT migrated: it is handed through as `{}`.
|
|
611
|
+
* Filling it in as `rules: []` would be the same thing the sanitizer reads as "the
|
|
612
|
+
* user deleted every rule", while a fresh install is the opposite of that —
|
|
613
|
+
* `normalizeConfig` turns a config that names no rule list into one blank rule.
|
|
738
614
|
*/
|
|
739
615
|
async function migrateLegacy(raw) {
|
|
740
616
|
const r = raw ?? {};
|
|
@@ -879,10 +755,9 @@ async function writeConfig(config) {
|
|
|
879
755
|
* not a recognised image at all.
|
|
880
756
|
*
|
|
881
757
|
* The null answer is what a DOWNLOAD is checked against (see `fetchHolidayAsset`):
|
|
882
|
-
* a
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* bytes of the payload are the only claim that cannot lie.
|
|
758
|
+
* neither a status code nor a `content-type` header survives contact with a
|
|
759
|
+
* misconfigured mirror, so the payload's own magic bytes are the only claim that
|
|
760
|
+
* cannot lie.
|
|
886
761
|
*/
|
|
887
762
|
function imageMimeOrNull(buf) {
|
|
888
763
|
if (buf.length >= 4 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
@@ -893,11 +768,9 @@ function imageMimeOrNull(buf) {
|
|
|
893
768
|
return null;
|
|
894
769
|
}
|
|
895
770
|
/**
|
|
896
|
-
* Sniff an image's MIME
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
* is better served as a JPEG and left to the browser than refused outright — the
|
|
900
|
-
* browser decides, and it can sniff what this list does not know.
|
|
771
|
+
* Sniff an image's MIME (defaults to JPEG). The default is deliberate for the bytes
|
|
772
|
+
* a USER uploaded: an unrecognised format is better served as a JPEG and left to
|
|
773
|
+
* the browser, which can sniff what this list does not know.
|
|
901
774
|
*/
|
|
902
775
|
function sniffImageMime(buf) {
|
|
903
776
|
return imageMimeOrNull(buf) ?? "image/jpeg";
|
|
@@ -905,16 +778,13 @@ function sniffImageMime(buf) {
|
|
|
905
778
|
/**
|
|
906
779
|
* Read one slot as a data URL, or null when it has nothing to serve.
|
|
907
780
|
*
|
|
908
|
-
* Two
|
|
909
|
-
*
|
|
910
|
-
*
|
|
911
|
-
* resolved lazily, so neither is read, fetched or even looked for until a
|
|
912
|
-
* wallpaper actually needs it.
|
|
781
|
+
* Two sources behind one answer: a holiday slot resolves to the festival art
|
|
782
|
+
* (downloaded on demand, never shipped — see `readHolidayAsset`), everything else is
|
|
783
|
+
* the user's own bytes on disk.
|
|
913
784
|
*
|
|
914
|
-
* `null` is
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* festival that cannot be downloaded must never blank the interface.
|
|
785
|
+
* `null` is legitimate for a holiday — offline, a blocked CDN, a cold cache — and
|
|
786
|
+
* the caller is built for it: `pickHoliday` treats "no bytes" as "this holiday does
|
|
787
|
+
* not take over". A festival that cannot be downloaded must never blank the screen.
|
|
918
788
|
*/
|
|
919
789
|
async function readImage(slot) {
|
|
920
790
|
const asset = holidayAssetFor(slot);
|
|
@@ -941,16 +811,12 @@ const holidayInFlight = /* @__PURE__ */ new Map();
|
|
|
941
811
|
* One holiday's art: from the cache if it is there and sound, otherwise from the
|
|
942
812
|
* network.
|
|
943
813
|
*
|
|
944
|
-
* The cached copy is
|
|
945
|
-
*
|
|
946
|
-
*
|
|
947
|
-
* back short of deleting a file nobody knows about. A cache entry that is not an
|
|
948
|
-
* image is treated exactly like a miss and overwritten by the next download.
|
|
814
|
+
* The cached copy is VALIDATED rather than trusted: a process killed mid-write would
|
|
815
|
+
* otherwise leave a truncated file that every later read serves, breaking the holiday
|
|
816
|
+
* forever. A cache entry that is not an image is treated as a miss.
|
|
949
817
|
*
|
|
950
|
-
* The in-flight map
|
|
951
|
-
*
|
|
952
|
-
* fires precisely when a machine that was asleep all day is waking up. Without
|
|
953
|
-
* it that is two simultaneous half-megabyte transfers for one picture.
|
|
818
|
+
* The in-flight map exists because the boot hydrate and the midnight rollover can
|
|
819
|
+
* ask for the same slot at the same moment (a machine waking from sleep).
|
|
954
820
|
*/
|
|
955
821
|
async function readHolidayAsset(asset) {
|
|
956
822
|
const cached = await readFileOrNull(holidayCachePath(asset));
|
|
@@ -978,21 +844,14 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
978
844
|
/**
|
|
979
845
|
* One asset over the wire, or WHY it did not arrive.
|
|
980
846
|
*
|
|
981
|
-
* Never throws: a
|
|
982
|
-
*
|
|
983
|
-
*
|
|
984
|
-
*
|
|
985
|
-
* profile asks `presetRetryable` whether the reason is weather (./preset) and the
|
|
986
|
-
* festival art takes the null and waits for the next apply.
|
|
847
|
+
* Never throws: a festival that cannot be fetched simply does not paint today, and a
|
|
848
|
+
* profile file that cannot be fetched is a sentence on the button. It answers with
|
|
849
|
+
* the reason instead, because the two callers treat it differently — the profile
|
|
850
|
+
* asks `presetRetryable` (./preset), the art waits for the next apply.
|
|
987
851
|
*
|
|
988
|
-
* The `content-length` check is a courtesy
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
* Deliberately says nothing about WHAT the bytes are: the festival art and the
|
|
992
|
-
* recommended profile are both "a file from the assets repository", and the one
|
|
993
|
-
* thing they do not share is how a payload is recognised (magic bytes vs JSON).
|
|
994
|
-
* That check belongs to the caller, which is the only half that knows what it
|
|
995
|
-
* asked for.
|
|
852
|
+
* The `content-length` check is a courtesy; the post-read size check is the one that
|
|
853
|
+
* holds. It says nothing about WHAT the bytes are: magic bytes vs JSON is the
|
|
854
|
+
* caller's check, because only the caller knows what it asked for.
|
|
996
855
|
*/
|
|
997
856
|
async function fetchAsset(url, max, timeout) {
|
|
998
857
|
const ctl = new AbortController();
|
|
@@ -1034,12 +893,10 @@ async function fetchAsset(url, max, timeout) {
|
|
|
1034
893
|
}
|
|
1035
894
|
}
|
|
1036
895
|
/**
|
|
1037
|
-
* The status codes worth another round: 5xx is a mirror having a bad moment, 429
|
|
1038
|
-
*
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
* outside the repository with 403, both hosts answer a missing file with 404 — and
|
|
1042
|
-
* the profile follows a BRANCH, so that answer has to arrive fast and unchanged.
|
|
896
|
+
* The status codes worth another round: 5xx is a mirror having a bad moment, 429 one
|
|
897
|
+
* asking to be left alone, 408 one giving up on us. Every other non-2xx means the
|
|
898
|
+
* file is not there (jsDelivr answers 403 outside the repository, 404 for a missing
|
|
899
|
+
* file), and that answer must arrive fast and unchanged.
|
|
1043
900
|
*/
|
|
1044
901
|
function fetchFailureForStatus(status) {
|
|
1045
902
|
return status >= 500 || status === 429 || status === 408 ? "server" : "missing";
|
|
@@ -1047,11 +904,9 @@ function fetchFailureForStatus(status) {
|
|
|
1047
904
|
/**
|
|
1048
905
|
* One festival picture, or null when no mirror served an image.
|
|
1049
906
|
*
|
|
1050
|
-
* One round,
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
* either — it is cached once it arrives, and every later read tries again, while a
|
|
1054
|
-
* button press has no later read of its own.
|
|
907
|
+
* One round, unlike the profile's (`fetchPresetFile`): this is fetched during boot,
|
|
908
|
+
* before the first wallpaper is painted, and a retry round would delay that paint.
|
|
909
|
+
* The art is cached once it arrives and every later read tries again.
|
|
1055
910
|
*/
|
|
1056
911
|
async function fetchHolidayAsset(url) {
|
|
1057
912
|
const got = await fetchAsset(url, HOLIDAY_FETCH_MAX, HOLIDAY_FETCH_TIMEOUT);
|
|
@@ -1060,13 +915,11 @@ async function fetchHolidayAsset(url) {
|
|
|
1060
915
|
/**
|
|
1061
916
|
* One file out of the profile directory, or null when no mirror served it.
|
|
1062
917
|
*
|
|
1063
|
-
* Every mirror is asked in every round, so a round is "all the hosts there are"
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* renamed a file" still costs exactly two requests and a sentence naming the slot
|
|
1069
|
-
* (see `presetRetryable`, ./preset).
|
|
918
|
+
* Every mirror is asked in every round, so a round is "all the hosts there are" and
|
|
919
|
+
* the rounds are the retry (a mirror that answers 502 once and correctly the next
|
|
920
|
+
* time, on the sixth of seven files). A round whose failures were all terminal — the
|
|
921
|
+
* file is gone, or too big — ends the attempts outright, so a renamed file costs two
|
|
922
|
+
* requests and a sentence naming the slot (`presetRetryable`, ./preset).
|
|
1070
923
|
*/
|
|
1071
924
|
async function fetchPresetFile(name, max) {
|
|
1072
925
|
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
@@ -1084,27 +937,20 @@ async function fetchPresetFile(name, max) {
|
|
|
1084
937
|
/**
|
|
1085
938
|
* The profile's shape gate and its configuration, in one answer.
|
|
1086
939
|
*
|
|
1087
|
-
* Two files
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
1090
|
-
* in earnest and long before anything is deleted. The config then says what the
|
|
1091
|
-
* profile IS.
|
|
940
|
+
* Two files in order: the manifest says what SHAPE the config is written for, and a
|
|
941
|
+
* config this build cannot read is refused here — before anything is downloaded in
|
|
942
|
+
* earnest, long before anything is deleted. The config then says what the profile IS.
|
|
1092
943
|
*
|
|
1093
|
-
* The sanitizer runs on this side
|
|
1094
|
-
*
|
|
1095
|
-
*
|
|
1096
|
-
* `
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
944
|
+
* The sanitizer runs on this side on purpose: the download list is DERIVED from
|
|
945
|
+
* `rules[].images[].slot`, so an unsanitized config would let a remote file choose
|
|
946
|
+
* what this process requests. After `normalizeConfig` every slot has passed `SLOT_RE`
|
|
947
|
+
* (`normalizeImage`). Holiday slots are then DROPPED rather than refused: they are
|
|
948
|
+
* named legally in `holidays.items`, have no file at all, and `writeImage` refuses
|
|
949
|
+
* them anyway, so a rule pointing at one is already inert on the client.
|
|
1099
950
|
*
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
* would be a worse answer than ignoring it.
|
|
1104
|
-
*
|
|
1105
|
-
* Nothing is cached to disk, unlike the festival art. That cache exists because
|
|
1106
|
-
* the art is fetched on a schedule without anyone asking and must work offline
|
|
1107
|
-
* afterwards; this is a button whose entire point is to fetch what is there NOW.
|
|
951
|
+
* Nothing is cached to disk, unlike the festival art: that cache exists because the
|
|
952
|
+
* art is fetched on a schedule and must work offline afterwards, while this is a
|
|
953
|
+
* button whose whole point is to fetch what is there NOW.
|
|
1108
954
|
*/
|
|
1109
955
|
async function fetchPresetConfig() {
|
|
1110
956
|
const manifestRaw = await fetchPresetFile(PRESET_MANIFEST, PRESET_MANIFEST_MAX);
|
|
@@ -1126,10 +972,14 @@ async function fetchPresetConfig() {
|
|
|
1126
972
|
ok: false,
|
|
1127
973
|
error: "not a profile"
|
|
1128
974
|
};
|
|
1129
|
-
if (declared > 9)
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
975
|
+
if (declared > 9) {
|
|
976
|
+
console.warn(`dsh-background-by-model: the recommended profile declares schema ${declared}, this build writes 9`);
|
|
977
|
+
return {
|
|
978
|
+
ok: false,
|
|
979
|
+
error: "newer version"
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
if (declared < 9) console.warn(`dsh-background-by-model: the recommended profile is written for schema ${declared}; importing it with defaults for the fields it predates`);
|
|
1133
983
|
const configRaw = await fetchPresetFile(PRESET_CONFIG, PRESET_CONFIG_MAX);
|
|
1134
984
|
if (configRaw === null) return {
|
|
1135
985
|
ok: false,
|
|
@@ -1198,11 +1048,9 @@ async function fetchPresetImage(slot) {
|
|
|
1198
1048
|
/**
|
|
1199
1049
|
* Cache one downloaded asset, atomically and best-effort.
|
|
1200
1050
|
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
* waiting for them — losing the cache costs a re-download next time, and failing
|
|
1205
|
-
* the read over it would cost the holiday.
|
|
1051
|
+
* Temp name + rename, so no reader ever sees a partial file. A failure is logged
|
|
1052
|
+
* and otherwise ignored: the bytes are in hand and the caller is waiting; losing
|
|
1053
|
+
* the cache only costs a re-download.
|
|
1206
1054
|
*/
|
|
1207
1055
|
async function writeHolidayCache(asset, buf) {
|
|
1208
1056
|
try {
|
|
@@ -1220,19 +1068,12 @@ async function writeHolidayCache(asset, buf) {
|
|
|
1220
1068
|
/**
|
|
1221
1069
|
* Drop every generation of the cache except the one this build reads.
|
|
1222
1070
|
*
|
|
1223
|
-
* Called AFTER a successful download, never before it
|
|
1224
|
-
*
|
|
1225
|
-
* to be unreachable must not cost the user the art they already had.
|
|
1071
|
+
* Called AFTER a successful download, never before it: a pinned release that turns
|
|
1072
|
+
* out to be unreachable must not cost the user the art they already had.
|
|
1226
1073
|
*
|
|
1227
|
-
* It walks the cache ROOT and
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1230
|
-
* makes an upgrade clean up after itself — 0.7.9 cached flat `holiday-cache/<asset>`
|
|
1231
|
-
* files, which this build never reads and this sweep removes.
|
|
1232
|
-
*
|
|
1233
|
-
* Best-effort, like the write that calls it: a cache that could not be pruned is
|
|
1234
|
-
* a cache with one extra entry in it, and failing a festival over that would be
|
|
1235
|
-
* the tail wagging the dog.
|
|
1074
|
+
* It walks the cache ROOT and compares GENERATION names, never asset names, so the
|
|
1075
|
+
* generation in use cannot be touched. That also makes an upgrade clean up after
|
|
1076
|
+
* itself (0.7.9's flat `holiday-cache/<asset>` files are removed here).
|
|
1236
1077
|
*/
|
|
1237
1078
|
async function pruneHolidayCache() {
|
|
1238
1079
|
try {
|
|
@@ -1252,11 +1093,8 @@ async function pruneHolidayCache() {
|
|
|
1252
1093
|
* The wallpaper a holiday slot resolves to, or null when the slot is not a
|
|
1253
1094
|
* holiday's.
|
|
1254
1095
|
*
|
|
1255
|
-
* This mapping is the ONLY thing that makes a holiday slot read-only, so
|
|
1256
|
-
*
|
|
1257
|
-
* list on their own. It reads no config: the festival art has stopped being a
|
|
1258
|
-
* fallback that a flag could disable. What changed is only WHERE the bytes come
|
|
1259
|
-
* from — the slot's identity, and its refusal to accept a write, are untouched.
|
|
1096
|
+
* This mapping is the ONLY thing that makes a holiday slot read-only, so `readImage`
|
|
1097
|
+
* and `writeImage` both go through it instead of testing the holiday list each.
|
|
1260
1098
|
*/
|
|
1261
1099
|
function holidayAssetFor(slot) {
|
|
1262
1100
|
return HOLIDAYS.find((h) => h.slot === slot)?.asset ?? null;
|
|
@@ -1290,22 +1128,19 @@ async function listSlots() {
|
|
|
1290
1128
|
/**
|
|
1291
1129
|
* Empty the store: the config and every rule image, and nothing else.
|
|
1292
1130
|
*
|
|
1293
|
-
* The hard reset behind "use the recommended profile", and it has to be a real
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
*
|
|
1298
|
-
* no slot remapping: there is nothing left to collide with.
|
|
1131
|
+
* The hard reset behind "use the recommended profile", and it has to be a real one
|
|
1132
|
+
* rather than a config overwrite: `nextSlot` hands out `m1`, `m2`, … as a running
|
|
1133
|
+
* counter, so a profile and a store that both have pictures collide on those names
|
|
1134
|
+
* as a matter of course. Emptying first is what makes the profile's own slots free
|
|
1135
|
+
* to write, which is why no slot remapping is needed.
|
|
1299
1136
|
*
|
|
1300
|
-
* `holiday-cache/` is deliberately untouched
|
|
1301
|
-
*
|
|
1302
|
-
*
|
|
1303
|
-
* profile is selected. Pruning superseded generations is `pruneHolidayCache`'s
|
|
1304
|
-
* job, and it is not this one.
|
|
1137
|
+
* `holiday-cache/` is deliberately untouched: downloaded art that belongs to no
|
|
1138
|
+
* slot and costs a round trip to get back. Pruning generations is
|
|
1139
|
+
* `pruneHolidayCache`'s job.
|
|
1305
1140
|
*
|
|
1306
|
-
* Best-effort per entry
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1141
|
+
* Best-effort per entry (one unremovable file must not leave the store half wiped),
|
|
1142
|
+
* but failures are logged, because a fresh config is about to be written over
|
|
1143
|
+
* whatever survives.
|
|
1309
1144
|
*/
|
|
1310
1145
|
async function resetStore() {
|
|
1311
1146
|
let ok = true;
|
|
@@ -1419,13 +1254,27 @@ function defaultModel(ctx) {
|
|
|
1419
1254
|
} catch {}
|
|
1420
1255
|
return null;
|
|
1421
1256
|
}
|
|
1422
|
-
const NS = "dshBackgroundByModel";
|
|
1423
1257
|
const RPC_CHANNEL = "/dsh-background-by-model";
|
|
1258
|
+
/** One request's ceiling. The host's own bridge uses the same number. */
|
|
1424
1259
|
const RPC_BODY_MAX = 314572800;
|
|
1425
|
-
/**
|
|
1426
|
-
|
|
1260
|
+
/** The namespace half of every endpoint on it: `dshBackgroundByModel/read`. */
|
|
1261
|
+
const RPC_NS = "dshBackgroundByModel";
|
|
1262
|
+
/**
|
|
1263
|
+
* One RPC method, as `ConnectionRpcHandler` delivers it: `(endpoint, payload,
|
|
1264
|
+
* signal, peer)`, with the endpoint spelled `dshBackgroundByModel/<method>`.
|
|
1265
|
+
* Envelope parsing, the body cap, the 401/403 fence and the method-match check
|
|
1266
|
+
* belong to the connection service (see `apply`).
|
|
1267
|
+
*/
|
|
1427
1268
|
async function handleRpcMethod(ctx, endpoint, payload) {
|
|
1428
|
-
|
|
1269
|
+
if (typeof endpoint !== "string") return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
error: {
|
|
1272
|
+
code: "dsh-background-by-model/bad-request",
|
|
1273
|
+
message: "missing endpoint",
|
|
1274
|
+
details: {}
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
const method = endpoint.startsWith(`${RPC_NS}/`) ? endpoint.slice(21) : "";
|
|
1429
1278
|
const rawSlot = payload?.slot;
|
|
1430
1279
|
const slot = typeof rawSlot === "string" && SLOT_RE.test(rawSlot) ? rawSlot : null;
|
|
1431
1280
|
try {
|
|
@@ -1550,7 +1399,7 @@ function apply(ctx) {
|
|
|
1550
1399
|
return;
|
|
1551
1400
|
}
|
|
1552
1401
|
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
1553
|
-
const endpoint = pathname.startsWith(
|
|
1402
|
+
const endpoint = pathname.startsWith(`/dsh-background-by-model/`) ? pathname.slice(25) : void 0;
|
|
1554
1403
|
if (endpoint === void 0 || endpoint.length === 0) {
|
|
1555
1404
|
res.writeHead(404);
|
|
1556
1405
|
res.end();
|
|
@@ -1624,4 +1473,4 @@ function apply(ctx) {
|
|
|
1624
1473
|
});
|
|
1625
1474
|
}
|
|
1626
1475
|
//#endregion
|
|
1627
|
-
export { apply, inject, name };
|
|
1476
|
+
export { RPC_CHANNEL, apply, handleRpcMethod, inject, name };
|