dsh-background-by-model 0.8.1 → 0.9.0

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/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
- * This shape used to be written out twice: the node half owns persistence and
23
- * sanitizes every write, the browser half owns the UI and its own reading of the
24
- * same file. A field added to only one side was silently dropped by the other
25
- * side's sanitizer — the matching slider stayed live in memory, `writeConfig`
26
- * wrote a config without it, and the next load fell back to the default.
27
- * `blurs.rightbar` / `rightbarOpacity` in 0.3.2 was exactly that bug, and the key
28
- * lists were duplicated 6 times across the two halves, so it was due to happen.
29
- *
30
- * The types, the key lists, the defaults and the pure normalizers therefore live
31
- * here and both halves import them: adding a field means editing this file, and
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
- * `bg` main background (`--dsw-alias-bg-base`)
75
- * `sidebar` sidebar (`--dsw-specific-sidebar-fill`)
76
- * `card` cards/panels (`--dsw-alias-bg-layer-1/2/3`, `--dsw-specific-menu`
77
- * and its 0.1.7 alias `--dsw-menu-surface-fill`)
78
- * `code` code blocks, banner included (`--dsw-alias-markdown-code-block`,
79
- * `--dsw-alias-markdown-code-block-banner`)
80
- * `input` input/control surfaces (`--dsw-specific-input-major`)
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 color.
116
+ * null when the string is not a hex colour.
124
117
  *
125
- * A holiday names its color as hex because that is how a color is quoted, and a
126
- * hand-converted triple is a magic number nothing downstream can check. The
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
- * Holidays default to `fill` rather than a rule's `fit`: these are full-bleed
162
- * festival wallpapers, and `fit` would letterbox them on every aspect ratio the
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 color.
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
- * Such a rule is deliberately valid rather than half-built: it is skipped by
194
- * matching until it has something of its own to paint (`ruleCanPaint` — a picture,
195
- * or a theme color), which is what makes a rule nobody has filled in yet invisible
196
- * instead of letting an empty background steal the fallback from rule 1.
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 written before this setting
283
- * existed has no `transition` key at all and gets the shipped default, which is
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
- * `durationMs` is clamped with `0` kept as a real value (a hard cut) rather than
292
- * read as "absent": 0 is exactly how a hard cut is asked for without changing
293
- * the effect.
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` is the RULE's own color, and it is applied only to an entry that has
309
- * no `color` key at all — the shape every release before 0.7.1 wrote, where a
310
- * rule's color was the only color there was. Lifting it here, once, is what makes
311
- * an upgrade a no-op on screen: a themed wallpaper keeps its theme instead of
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
- * The KEY's presence is the whole distinction, which is why `null` is not the
316
- * same as absent: an entry with `color: null` is an image the user explicitly set
317
- * to "follow the system theme", and re-inheriting the rule's color over that
318
- * would undo a deliberate choice on every load.
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 AT ALL.
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
- * The rule's color is resolved FIRST and handed to every image entry, because it
349
- * is what an entry written before 0.7.1 inherits (`normalizeImage`). The rule keeps
350
- * it as well: it is not a default for its images at paint time any more, but it
351
- * is still what the rule paints once its last picture is gone.
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 layout mode makes the same trip in the other direction, and this is the
354
- * last release that has to: a config written before 0.7.5 keeps `bgMode` at the RULE
355
- * level, where one value covered every picture of a rotation, so it is read here
356
- * once and handed to each image as its inherited mode. What the user chose
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, not a broken one: removing a rule's last
362
- * picture has to be expressible, and forcing an entry back in is what produced a
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`, `images` and every theme color (the rule's and each image's) come from
413
- * the DEFINITION, never from disk: a holiday's slot is the only thing tying it to
414
- * its bytes, and its theme color is a fixed part of what that holiday looks like —
415
- * so a stale or hand-edited value would either orphan the wallpaper, point the
416
- * holiday at a rule's image, or paint a festival in a color it does not have.
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
- * Three states, and the middle one is why this is a function rather than the
458
- * one-line fallback every other field gets: a real number is the card's own
459
- * value, `null` is "never owned, so follow `opacities.bg`", and a KEY that is not
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 a small RPC
538
- * surface on the dedicated `/dsh-background-by-model` channel (never the shared
539
- * `/api`, so slash commands stay intact).
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 the ordered rule list + the global interface settings
542
- * modelbg-<slot> one background image per rule (raw bytes, no extension;
543
- * the MIME is sniffed from the magic bytes when served)
544
- * holiday-cache/<tag>/ festival art downloaded from the CDN — one directory
545
- * per pinned release of the asset repository, one file per
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 — there is no separate HTTP
549
- * image route. A legacy single-wallpaper / video-background store is migrated
550
- * to rule 1 on first read.
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 two holiday wallpapers used to ship inside the package. They do not any
573
- * more: they were 746 KB of the 1.1 MB tarball — two thirds of everything anyone
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. Runtime art has to be
580
- * immutable: a branch reference would let a later reshuffle of the documentation
581
- * screenshots silently kill a shipped feature, on a day nobody is watching, with
582
- * nothing in the log — and jsDelivr serves a version reference as immutable,
583
- * which is exactly what a file that never changes wants. A tag says that to the
584
- * CDN in the same breath as it says it to a human: `v0.1.0` is the reference the
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. jsDelivr first because it is reachable from networks
598
- * where `raw.githubusercontent.com` is not; raw second because a CDN can be
599
- * blocked, rate-limited or down, and one dead mirror must not cost the easter
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
- * It is NOT part of the slot namespace and must never become part of it: the
608
- * cached files are named after the ASSET (`mid-autumn.webp`) and their parent
609
- * after the release the plugin pins (`v0.1.0`), never after a slot, so nothing
610
- * here can ever be mistaken for a user's `modelbg-<slot>`. That is what lets a
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. A cache keyed by asset name alone answers
616
- * a NEW generation with the OLD bytes and says nothing about it, on a day nobody
617
- * is watching — which is the failure the pinned reference exists to prevent, one
618
- * layer further down. With the tag in the path, a build that pins a new tag looks
619
- * in a directory nobody has written to yet, so the first holiday after an update
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
- * festival art — as a DIRECTORY, in the store's own shape.
630
- *
631
- * It used to be one theme file with every wallpaper inlined as base64: 2.9 MB
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 adding a rule entry and a
643
- * file, with no second place to keep in sync.
644
- *
645
- * Referenced by the same **release tag** as the festival art. This used to be a
646
- * branch, and deliberately: the reason the art is immutable — a reshuffle of the
647
- * assets repository would make a shipped feature disappear SILENTLY, on a day
648
- * nobody is watching — does not apply to a profile, because a profile fails
649
- * LOUDLY (the user is looking at the button and gets an error naming what went
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
- * Three of them, because a profile is now a SET of transfers and each has a
686
- * different way of going wrong. The manifest and the config are small by nature
687
- * — a manifest is one field, a config is a few kilobytes of JSON — so their caps
688
- * are what stops a mirror from answering with something enormous instead. The
689
- * images are the payload, so their cap is about the content.
690
- *
691
- * The count is the one that matters: the request set is derived from the config,
692
- * so a hostile or broken config could name a thousand slots and turn one button
693
- * press into a thousand downloads. `SLOT_RE` bounds each name and
694
- * `PRESET_IMAGE_COUNT_MAX` bounds how many names there can be. The running TOTAL
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
- * Anything without a `rules` array is an old config: its single wallpaper
728
- * becomes rule 1 (`m1`, empty match = pure fallback) carrying the old color,
729
- * layout mode, opacity, blur and framing, and the remaining legacy files
730
- * (background video, upload temp) are removed. Idempotent: the rewritten config
731
- * carries `rules`, so the next read short-circuits.
732
- *
733
- * The one case that is NOT a migration — a store with no config file at all — is
734
- * handed through untouched rather than being filled in as `rules: []`. That empty
735
- * list would be the same thing the sanitizer reads as "the user deleted every
736
- * rule", and a fresh install is the opposite of that: `normalizeConfig` turns a
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 CDN that answers 200 with an error page, a captive-portal redirect or a
883
- * truncated body has to be rejected, and neither a status code nor a
884
- * `content-type` header survives contact with a misconfigured mirror. The magic
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 from its leading magic bytes (defaults to JPEG).
897
- *
898
- * The default is deliberate for the bytes a USER uploaded: an unrecognised format
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 very different sources behind one answer. A holiday slot resolves to the
909
- * festival art — downloaded on demand and cached, never shipped (see
910
- * `readHolidayAsset`). Everything else is the user's own bytes on disk. Both are
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 a legitimate answer for a holiday — offline, a blocked CDN, a cold
915
- * cache — and the caller is built for it: `pickHoliday` treats "no bytes" as
916
- * "this holiday does not take over" and the model rules keep painting. A
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 validated rather than trusted — a process killed mid-write
945
- * (or a full disk) would otherwise leave a permanently truncated file that every
946
- * later read happily serves, and the holiday would be broken forever with no way
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 matters because the two readers can race: the boot hydrate
951
- * and the midnight rollover timer both ask for the same slot, and the timer
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 holiday that cannot be fetched is a holiday that does not paint
982
- * today, and a profile file that cannot be fetched is a sentence on the button —
983
- * neither is an error path the caller should have to wrap. What it answers with
984
- * instead is the reason, because the two callers treat those differently: the
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 — a mirror that announces a huge body
989
- * never gets to send it — while the post-read size check is the one that holds.
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
- * is one asking to be left alone for a bit, 408 is one giving up on us.
1039
- *
1040
- * Every other non-2xx means the file is not there — jsDelivr answers a path
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, deliberately, unlike the profile's (see `fetchPresetFile`): this is
1051
- * fetched during boot, on the day, before the first wallpaper is painted, and a
1052
- * retry round would add its delay to that first paint. The art does not need one
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
- * and the rounds are the retry: the case this exists for is a mirror that answers
1065
- * 502 on one round and correctly on the next, on the sixth of seven files, with
1066
- * six good downloads behind it. A round in which every failure was terminal — the
1067
- * file is not there, or it is too big — ends the attempts outright, so "the author
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, fetched in order, because they answer different questions. The
1088
- * manifest says what SHAPE the config is written for, and a config written for a
1089
- * shape this build cannot read is refused here — before anything is downloaded
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, and that is not merely tidiness: the list of
1094
- * images to download is DERIVED from `rules[].images[].slot`, so an unsanitized
1095
- * config would let a remote file choose what this process requests. After
1096
- * `normalizeConfig` every slot has passed `SLOT_RE` (see `normalizeImage`), and
1097
- * the only names left to reject are the holiday slots — which a config CAN name,
1098
- * legally, inside `holidays.items`, and which have no file at all.
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
- * Holiday slots are dropped rather than refused. They are unwritable by design
1101
- * (`writeImage` refuses them), so a rule that points at one is already inert on
1102
- * the client; failing the whole profile over a slot nothing could ever fill
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) return {
1130
- ok: false,
1131
- error: "newer version"
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
- * Written to a temp name and renamed into place, so a reader never sees a partial
1202
- * file even if this process dies between the two calls. A failure to cache is
1203
- * logged and otherwise ignored: the bytes are already in hand and the caller is
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, so a generation is only
1224
- * ever removed once its replacement is on disk: a pinned release that turns out
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 removes every entry whose name is not this build's
1228
- * tag, so the generation in use cannot be touched however the comparison goes:
1229
- * the entries it compares are generation names, never asset names. That also
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 both
1256
- * `readImage` and `writeImage` go through it rather than each testing the holiday
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
- * one rather than a config overwrite. `nextSlot` hands out `m1`, `m2`, … — a
1295
- * running counter, not a random token — so a profile and a store that both have
1296
- * pictures collide on those names as a matter of course. Emptying first is what
1297
- * makes the profile's own slots free to write, and it is why this feature needs
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 — every generation of it. It is not
1301
- * the user's configuration: it is downloaded festival art that belongs to no
1302
- * slot, costs a network round trip to get back, and has nothing to do with which
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 — one unremovable file must not abort the rest and leave
1307
- * the store half wiped — but the failures are logged rather than swallowed,
1308
- * because the caller is about to write a fresh config over whatever survives.
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
- /** Dispatch one decoded RPC method to the matching persistence routine and
1426
- * return the wire `result` half of the server-response envelope. */
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
- const method = endpoint.slice(`${NS}/`.length);
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(`${RPC_CHANNEL}/`) ? pathname.slice(25) : void 0;
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 };