dsh-folia 4.0.0 → 4.0.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/lib/index.js CHANGED
@@ -77,8 +77,8 @@ import { assertSubpackagesResolvable, assertSubpackagesResolvableInHost, httpVie
77
77
  // `extOf` 反向传给会话段(清单载荷的容器后缀判据 ⇒ 见该文件头)。
78
78
  import { createHostSession } from './kernel/host-session.js';
79
79
  export { extOf };
80
- import { registerInventoryRoutes } from './routes/inventory.js';
81
- import { registerModulesRoutes } from './routes/modules.js';
80
+ import { registerInventoryRoutes, warmInventorySiteCache } from './routes/inventory.js';
81
+ import { registerModulesRoutes, warmModulesCache } from './routes/modules.js';
82
82
  import { registerBootRoutes } from './routes/boot.js';
83
83
  import { registerSiteThumbRoutes } from './routes/site-thumb.js';
84
84
  import { installBootInjection } from './boot/index.js';
@@ -307,6 +307,10 @@ export function apply(ctx) {
307
307
 
308
308
  // 站点图片缩略图代理 → lib/routes/site-thumb.js。卡片用 320px 小图,不再每张下 500 KB 原图。
309
309
  registerSiteThumbRoutes(webServer, { disposers, base: BASE, ensureImagePreview, log });
310
+
311
+ // 启动预热(有意不阻塞):站点清单与模块索引各拉一次,让第一次打开面板不冷。
312
+ warmInventorySiteCache();
313
+ warmModulesCache();
310
314
  installBootInjection(ctx, (m) => { try { log(m); } catch { /* ignore */ } });
311
315
 
312
316
  // 3./3a. 派生媒体族路由(源元信息 / 抽帧转码 + 进度 / 视频缩略图)→ lib/routes/media-derived.js。
@@ -128,7 +128,21 @@ async function readIdBody(req) {
128
128
  * 候选清单:模块索引里的 `boot` 模块 → 拉它的 manifest → `{ clips: [...] }`。
129
129
  * 失败一律返回空数组(面板显示"没有候选"),绝不抛。
130
130
  */
131
- async function fetchCandidates() {
131
+ /** 候选缓存:30 秒。页签每次打开要跳两跳(模块索引 + 站点分类),没必要每次都跳。 */
132
+ const CANDIDATE_TTL_MS = 30000;
133
+ let candidateCache = { at: 0, value: null };
134
+
135
+ async function fetchCandidates(force = false) {
136
+ if (!force && candidateCache.value && Date.now() - candidateCache.at < CANDIDATE_TTL_MS) {
137
+ return candidateCache.value;
138
+ }
139
+ const fresh = await fetchCandidatesUncached();
140
+ // 有结果才缓存(空结果多半是站点恰好不可达,别把它固化 30 秒)。
141
+ if (fresh && fresh.clips && fresh.clips.length) candidateCache = { at: Date.now(), value: fresh };
142
+ return fresh;
143
+ }
144
+
145
+ async function fetchCandidatesUncached() {
132
146
  // ① 站点分类接口(与壁纸同一个接口,只是 category 不同)
133
147
  const site = siteBase();
134
148
  if (site) {
@@ -199,7 +213,7 @@ export function registerBootRoutes(webServer, c) {
199
213
  let force = false;
200
214
  try { force = new URL(req.url || '/', 'http://x').searchParams.get('refresh') === '1'; } catch { /* ignore */ }
201
215
  if (force) await resolveModules(true);
202
- const [boot, candidates] = await Promise.all([Promise.resolve(readBoot()), fetchCandidates()]);
216
+ const [boot, candidates] = await Promise.all([Promise.resolve(readBoot()), fetchCandidates(force)]);
203
217
  const local = listLocalClips();
204
218
  const localById = new Map(local.map((x) => [x.id, x]));
205
219
  const clips = candidates.clips.map((c) => {
@@ -102,6 +102,10 @@ function rowStatusOf(probe) {
102
102
  * 站点格式(`<site>/manifest.json`):`{ images: [{ file, title, url, preview, kind?, animated? }] }`
103
103
  * —— 与 `folia-wallpaper-site` 产物一致;`url`/`preview` 可以是相对路径(相对站点基址)。
104
104
  */
105
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
106
+ import { homedir } from 'node:os';
107
+ import { dirname, join } from 'node:path';
108
+
105
109
  const FOLIA_SITE = 'https://folia-5ve.pages.dev';
106
110
  /**
107
111
  * 站点数据源的两个可能形态,按顺序尝试:
@@ -143,7 +147,46 @@ const FOLIA_TIMEOUT_MAX_MS = 15000;
143
147
  * 说清"为什么没有云端壁纸",而不是静默空库 —— 之前就是静默失败,排查时无从下手。
144
148
  * @type {{ at: number, items: any[], status: { url: string, ok: boolean, count: number, ms: number, error: string|null } | null }}
145
149
  */
146
- let foliaCache = { at: 0, items: [], status: null };
150
+ /**
151
+ * 站点清单的磁盘缓存:`~/.dsh-folia/cache/site-items.json`。
152
+ *
153
+ * 为什么落盘:内存缓存只活一个进程。没有它,**每次重启后第一次打开面板**都要等一次
154
+ * 完整的跨境往返(实测 ~800ms),这段时间界面是空的。有了它,冷启动也能立刻出内容。
155
+ *
156
+ * 纪律:读失败一律当"没有缓存"(坏缓存只该慢一次,不该出错);只缓存**成功**的结果,
157
+ * 失败不落盘 —— 否则一次断网会把空列表固化下来,之后每次启动都显示没壁纸。
158
+ */
159
+ function foliaDiskCachePath() {
160
+ const home = process.env.DSH_FOLIA_HOME || join(homedir(), '.dsh-folia');
161
+ return join(home, 'cache', 'site-items.json');
162
+ }
163
+
164
+ function readFoliaDiskCache() {
165
+ try {
166
+ const doc = JSON.parse(readFileSync(foliaDiskCachePath(), 'utf8'));
167
+ if (!doc || !Array.isArray(doc.items) || !doc.items.length) return null;
168
+ return { at: Number(doc.at) || 0, items: doc.items, status: doc.status || null };
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+
174
+ function writeFoliaDiskCache(entry) {
175
+ try {
176
+ const p = foliaDiskCachePath();
177
+ mkdirSync(dirname(p), { recursive: true });
178
+ writeFileSync(p, JSON.stringify({ at: entry.at, items: entry.items, status: entry.status }));
179
+ } catch { /* 只读盘 / 配额:写不进去只影响下次速度,不影响正确性 */ }
180
+ }
181
+
182
+ /** 内存缓存;**用磁盘缓存打底** —— 冷启动第一次请求就能立刻返回旧列表。 */
183
+ let foliaCache = (() => {
184
+ const disk = readFoliaDiskCache();
185
+ return disk ? { at: disk.at, items: disk.items, status: disk.status } : { at: 0, items: [], status: null };
186
+ })();
187
+
188
+ /** 后台刷新是否在途(避免多个请求同时触发同一次刷新)。 */
189
+ let foliaRefreshInflight = false;
147
190
 
148
191
  /** 超时从 env 读,越界收敛到 [1s, 15s]。`DSH_FOLIA_SITE_TIMEOUT_MS=8000` 可放宽弱网。 */
149
192
  function foliaTimeoutMs() {
@@ -291,12 +334,32 @@ function foliaTypeOf(it) {
291
334
  * `disabled` 通道被 `DSH_FOLIA_SITE=off` 关掉 · `timeout` 超时 · `tls` 证书问题
292
335
  * `http-<code>` 状态码非 2xx · `json` 清单不是合法 JSON · `network` 其它网络错误
293
336
  */
337
+ /**
338
+ * 对外入口:**先给已有结果,再后台刷新**(stale-while-revalidate)。
339
+ *
340
+ * 这是面板"打开就能看到东西"的关键:清单过期 ≠ 让用户等。有缓存就立刻返回,
341
+ * 同时后台拉一次新的(下次打开就是新的)。只有**一条缓存都没有**时才会阻塞等待。
342
+ */
294
343
  async function foliaRemote(force = false) {
295
344
  const base = foliaSiteBase();
296
345
  if (!base) return { items: [], status: null };
297
- if (!force && Date.now() - foliaCache.at < FOLIA_TTL_MS) {
346
+
347
+ const fresh = Date.now() - foliaCache.at < FOLIA_TTL_MS;
348
+ if (!force && foliaCache.items.length) {
349
+ if (!fresh && !foliaRefreshInflight) {
350
+ foliaRefreshInflight = true;
351
+ // 有意不 await:这次先把旧列表发出去,刷新结果留给下一次请求。
352
+ foliaRefresh().catch(() => {}).finally(() => { foliaRefreshInflight = false; });
353
+ }
298
354
  return { items: foliaCache.items, status: foliaCache.status };
299
355
  }
356
+ return foliaRefresh();
357
+ }
358
+
359
+ /** 真正去站点抓一次(缓存为空、或调用方要求强制刷新时走这里)。 */
360
+ async function foliaRefresh(force = false) {
361
+ const base = foliaSiteBase();
362
+ if (!base) return { items: [], status: null };
300
363
 
301
364
  const startedAt = Date.now();
302
365
  /** @type {any[]} */
@@ -391,11 +454,19 @@ async function foliaRemote(force = false) {
391
454
  error,
392
455
  at: Date.now(),
393
456
  };
394
- // 失败也写缓存:否则每次 /inventory 都要再等一次超时。
457
+ // 失败也写内存缓存:否则每次 /inventory 都要再等一次超时。
395
458
  foliaCache = { at: Date.now(), items, status };
459
+ // **成功**才落盘(失败了别把空列表固化到磁盘上)。
460
+ if (!error && items.length) writeFoliaDiskCache(foliaCache);
396
461
  return { items, status };
397
462
  }
398
463
 
464
+ // 启动预热:插件一装好就在后台抓一次站点清单,这样**重启后的第一次打开面板**也不冷。
465
+ // 有意不 await、失败静默 —— 预热只是优化,绝不能影响启动。
466
+ export function warmInventorySiteCache() {
467
+ try { foliaRemote(true).catch(() => {}); } catch { /* ignore */ }
468
+ }
469
+
399
470
  export function registerInventoryRoutes(webServer, c) {
400
471
  const { disposers, base: BASE, mediaOriginApi, buildInventory, componentsProbe } = c;
401
472
 
@@ -168,6 +168,12 @@ export async function resolveModules(force = false) {
168
168
  return payload;
169
169
  }
170
170
 
171
+ // 启动预热:模块索引很小(~1.6KB),但第一跳要走站点代理;提前拉一次,
172
+ // 开机动画页签第一次打开就不用等这一跳。
173
+ export function warmModulesCache() {
174
+ try { resolveModules(true).catch(() => {}); } catch { /* ignore */ }
175
+ }
176
+
171
177
  export function registerModulesRoutes(webServer, c) {
172
178
  const { disposers, base: BASE } = c;
173
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-folia",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
4
4
  "description": "Folia 壁纸引擎:把本机 Wallpaper Engine 壁纸搬进 DSH 界面(场景实时渲染 / 视频 / 网页),内置液态玻璃与 Folia 吉祥物,并可从自建站点拉取壁纸。单包形态(组件已合并)。",
5
5
  "type": "module",
6
6
  "engines": {