From ba712f66a8d9230cef4e9c16d814a4e4e5f670b9 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:38:45 +0900 Subject: [PATCH 01/27] x --- src/app/service/service_worker/resource.ts | 142 +++++++++++---------- src/app/service/service_worker/utils.ts | 18 ++- 2 files changed, 87 insertions(+), 73 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index f82bad975..48adaac73 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -7,10 +7,10 @@ import { type IMessageQueue } from "@Packages/message/message_queue"; import { type Group } from "@Packages/message/server"; import type { ResourceBackup } from "@App/pkg/backup/struct"; import { isText } from "@App/pkg/utils/istextorbinary"; -import { blobToBase64, randNum } from "@App/pkg/utils/utils"; +import { blobToBase64, randNum, sleep } from "@App/pkg/utils/utils"; import { type TDeleteScript } from "../queue"; import { calculateHashFromArrayBuffer } from "@App/pkg/utils/crypto"; -import { isBase64, parseUrlSRI } from "./utils"; +import { isBase64, parseUrlSRI, type TUrlSRIInfo } from "./utils"; import { stackAsyncTask } from "@App/pkg/utils/async_queue"; import { blobToUint8Array } from "@App/pkg/utils/datatype"; import { readBlobContent } from "@App/pkg/utils/encoding"; @@ -29,35 +29,32 @@ export class ResourceService { public async getResource( uuid: string, - url: string, + u: TUrlSRIInfo, type: ResourceType, - loadNow: boolean + loadNow: boolean, + oldResources: Resource | undefined ): Promise { - const res = await this.getResourceModel(url); - if (res) { + if (oldResources) { // 读取过但失败的资源加载也会被放在缓存,避免再加载资源 // 因此 getResource 时不会再加载资源,直接返回 undefined 表示没有资源 - if (!res.contentType) return undefined; - return res; + if (!oldResources.contentType) return undefined; + return oldResources; } - // 缓存中无资源加载纪录 - if (loadNow) { - // 立即尝试加载资源 - try { - return await this.updateResource(uuid, url, type); - } catch (e: any) { - this.logger.error("load resource error", { url }, Logger.E(e)); - } - } else { - // 等一下尝试加载资源 (在后台异步加载) - // 先返回 undefined 表示没有资源 + // 缓存中无资源加载纪录,需要取得资源 + const url = u.originalUrl; + if (!loadNow) { + // 等一下尝试加载资源(例入 import) // 避免所有资源立即同一时间加载, delay设为 1.2s ~ 2.4s - setTimeout( - () => { - this.updateResource(uuid, url, type); - }, - randNum(1200, 2400) - ); + const delay = randNum(1200, 2400); + await sleep(delay); + const updatedResource = await this.getResourceModel(u); + // 如果等候期间有其他程序已生成 resource, 则不用呼叫 updateResource + if (updatedResource?.contentType) return updatedResource; + } + try { + return await this.updateResource(uuid, u, type, undefined); + } catch (e: any) { + this.logger.error("load resource error", { url }, Logger.E(e)); } return undefined; } @@ -98,12 +95,14 @@ export class ResourceService { } } if (path) { + const u = parseUrlSRI(path); + const oldResources = await this.getResourceModel(u); if (uri.startsWith("file:///")) { // 如果是file://协议,则每次请求更新一下文件 - const res = await this.updateResource(script.uuid, path, type); + const res = await this.updateResource(script.uuid, u, type, oldResources); ret[resourceKey] = res; } else { - const res = await this.getResource(script.uuid, path, type, load); + const res = await this.getResource(script.uuid, u, type, load, oldResources); if (res) { ret[resourceKey] = res; } @@ -114,49 +113,53 @@ export class ResourceService { return ret; } - updateResourceByType(script: Script, type: ResourceType) { + // 只需要等待Promise返回,不理会返回值(失败也可以) + updateResourceByType(script: Script, type: ResourceType): Promise | void { + const uuid = script.uuid; const promises = script.metadata[type]?.map(async (u) => { + let url = ""; if (type === "resource") { const split = u.split(/\s+/); if (split.length === 2) { - return this.checkResource(script.uuid, split[1], "resource"); + url = split[1]; } } else { - return this.checkResource(script.uuid, u, type); + url = u; } - }); - return promises?.length && Promise.allSettled(promises); - } - - // 检查资源是否存在,如果不存在则重新加载 - async checkResource(uuid: string, url: string, type: ResourceType) { - let res = await this.getResourceModel(url); - const updateTime = res?.updatetime; - // 判断1天过期 - if (updateTime && updateTime > Date.now() - 1000 * 86400) { - return res; - } - try { - res = await this.updateResource(uuid, url, type); - if (res?.contentType) { - return res; + if (url) { + // 检查资源是否存在,如果不存在则重新加载 + // 如果有旧资源,而没有新资讯,则继续使用旧资源 + // 只需要等待Promise返回,不理会返回值(失败也可以) + const u = parseUrlSRI(url); + const oldResources = await this.getResourceModel(u); + const updateTime = oldResources?.updatetime; + // 资源最后更新是24小时内则不更新 + if (updateTime && updateTime > Date.now() - 86400_000) return; + // 旧资源或没有资源记录,尝试更新 + try { + await this.updateResource(uuid, u, type, oldResources); + } catch (e: any) { + this.logger.error("check resource failed", { uuid, url }, Logger.E(e)); + } } - } catch (e: any) { - // ignore - this.logger.error("check resource failed", { uuid, url }, Logger.E(e)); - } - return undefined; + }); + if (promises?.length) return Promise.allSettled(promises); } - async updateResource(uuid: string, url: string, type: ResourceType) { + async updateResource( + uuid: string, + u: TUrlSRIInfo, + type: ResourceType, + oldResources: Resource | null | undefined = null + ) { // 重新加载 - const u = parseUrlSRI(url); - let result = await this.getResourceModel(u.url); + if (oldResources === null) oldResources = await this.getResourceModel(u); + let result: Resource; try { const resource = await this.loadByUrl(u.url, type); const now = Date.now(); resource.updatetime = now; - if (!result || !result.contentType) { + if (!oldResources || !oldResources.contentType) { // 资源不存在,保存 resource.createtime = now; resource.link = { [uuid]: true }; @@ -164,19 +167,28 @@ export class ResourceService { result = resource; this.logger.info("reload new resource success", { url: u.url }); } else { - result.base64 = resource.base64; - result.content = resource.content; - result.contentType = resource.contentType; - result.hash = resource.hash; - result.updatetime = resource.updatetime; - result.link[uuid] = true; + result = { + ...oldResources, + base64: resource.base64, + content: resource.content, + contentType: resource.contentType, + hash: resource.hash, + updatetime: resource.updatetime, + link: { ...oldResources.link, [uuid]: true }, + }; await this.resourceDAO.update(result.url, result); this.logger.info("reload resource success", { url: u.url, }); } + return result; } catch (e) { - // 资源错误时保存一个空纪录以防止再度尝试加载 + // 如果有旧资源,则使用旧资源 + if (oldResources) { + this.logger.error("load resource error - fallback to old resource", { url: u.url }, Logger.E(e)); + return oldResources; + } + // 资源错误时(且没有旧资源)保存一个空纪录以防止再度尝试加载 // this.resourceDAO.save 自身出错的话忽略 await this.resourceDAO .save({ @@ -199,11 +211,9 @@ export class ResourceService { this.logger.error("load resource error", { url: u.url }, Logger.E(e)); throw e; } - return result; } - async getResourceModel(url: string) { - const u = parseUrlSRI(url); + async getResourceModel(u: TUrlSRIInfo) { const resource = await this.resourceDAO.get(u.url); if (resource) { // 校验hash @@ -229,7 +239,7 @@ export class ResourceService { } } if (!flag) { - resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${url} due to a SRI error ");`; + resource.content = `console.warn("ScriptCat: couldn't load resource from URL ${u.originalUrl} due to a SRI error ");`; } } return resource; diff --git a/src/app/service/service_worker/utils.ts b/src/app/service/service_worker/utils.ts index 9e7adfeba..7f1bfccf8 100644 --- a/src/app/service/service_worker/utils.ts +++ b/src/app/service/service_worker/utils.ts @@ -66,22 +66,26 @@ export function isBase64(str: string): boolean { return false; } -// 解析URL SRI -export function parseUrlSRI(url: string): { +export type TUrlSRIInfo = { url: string; - hash?: { [key: string]: string }; -} { + hash: { [key: string]: string } | undefined; + originalUrl: string; +}; + +// 解析URL SRI +export function parseUrlSRI(url: string): TUrlSRIInfo { const urls = url.split("#"); if (urls.length < 2) { - return { url: urls[0], hash: undefined }; + return { url: urls[0], hash: undefined, originalUrl: url }; } const hashs = urls[1].split(/[,;]/); const hash: { [key: string]: string } = {}; + const pattern = /^([a-zA-Z0-9]+)[-=](.+)$/; for (const val of hashs) { // 接受以下格式 // sha256-abc123== 格式 // sha256=abc123== 格式 - const match = val.match(/^([a-zA-Z0-9]+)[-=](.+)$/); + const match = pattern.exec(val); if (match) { const [, key, value] = match; hash[key] = value; @@ -89,7 +93,7 @@ export function parseUrlSRI(url: string): { } // 即使没有解析到任何哈希值,也只会返回空对象而不是 undefined - return { url: urls[0], hash }; + return { url: urls[0], hash, originalUrl: url }; } export async function notificationsUpdate( From 29cc3c6b5f742ac43974423bc35eed652ae95808 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:45:31 +0900 Subject: [PATCH 02/27] =?UTF-8?q?=E5=A4=84=E7=90=86=E5=B9=B6=E8=A1=8Cfetch?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 104 +++++++++++++++++---- src/app/service/service_worker/runtime.ts | 2 +- src/app/service/service_worker/script.ts | 2 +- 3 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 48adaac73..8f15ff5b8 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -15,6 +15,64 @@ import { stackAsyncTask } from "@App/pkg/utils/async_queue"; import { blobToUint8Array } from "@App/pkg/utils/datatype"; import { readBlobContent } from "@App/pkg/utils/encoding"; +class Semaphore { + private running = 0; + private readonly queue: Array<() => void> = []; + + constructor(readonly limit: number) { + if (limit < 1) throw new Error("limit must be >= 1"); + } + + async acquire(): Promise { + if (this.running < this.limit) { + this.running++; + return; + } + await new Promise((resolve) => this.queue.push(resolve)); + this.running++; + } + + release(): void { + if (this.running <= 0) { + console.warn("Semaphore double release detected"); + return; + } + this.running--; + this.queue.shift()?.(); + } +} + +const fetchSemaphore = new Semaphore(5); + +type TWithTimeoutNotifyResult = { + timeouted: boolean; + result: T | undefined; + done: boolean; + err: undefined | Error; +}; +const withTimeoutNotify = (promise: Promise, time: number, fn: (res: TWithTimeoutNotifyResult) => any) => { + const res: TWithTimeoutNotifyResult = { timeouted: false, result: undefined, done: false, err: undefined }; + const cid = setTimeout(() => { + res.timeouted = true; + fn(res); + }, time); + return promise + .then((result: T) => { + clearTimeout(cid); + res.result = result; + res.done = true; + fn(res); + return res; + }) + .catch((e) => { + clearTimeout(cid); + res.err = e; + res.done = true; + fn(res); + return res; + }); +}; + export class ResourceService { logger: Logger; resourceDAO: ResourceDAO = new ResourceDAO(); @@ -31,7 +89,6 @@ export class ResourceService { uuid: string, u: TUrlSRIInfo, type: ResourceType, - loadNow: boolean, oldResources: Resource | undefined ): Promise { if (oldResources) { @@ -42,15 +99,6 @@ export class ResourceService { } // 缓存中无资源加载纪录,需要取得资源 const url = u.originalUrl; - if (!loadNow) { - // 等一下尝试加载资源(例入 import) - // 避免所有资源立即同一时间加载, delay设为 1.2s ~ 2.4s - const delay = randNum(1200, 2400); - await sleep(delay); - const updatedResource = await this.getResourceModel(u); - // 如果等候期间有其他程序已生成 resource, 则不用呼叫 updateResource - if (updatedResource?.contentType) return updatedResource; - } try { return await this.updateResource(uuid, u, type, undefined); } catch (e: any) { @@ -59,11 +107,11 @@ export class ResourceService { return undefined; } - public async getScriptResources(script: Script, load: boolean): Promise<{ [key: string]: Resource }> { + public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> { const [require, require_css, resource] = await Promise.all([ - this.getResourceByType(script, "require", load), - this.getResourceByType(script, "require-css", load), - this.getResourceByType(script, "resource", load), + this.getResourceByType(script, "require"), + this.getResourceByType(script, "require-css"), + this.getResourceByType(script, "resource"), ]); return { @@ -73,7 +121,7 @@ export class ResourceService { }; } - async getResourceByType(script: Script, type: ResourceType, load: boolean): Promise<{ [key: string]: Resource }> { + async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> { if (!script.metadata[type]) { return {}; } @@ -102,7 +150,7 @@ export class ResourceService { const res = await this.updateResource(script.uuid, u, type, oldResources); ret[resourceKey] = res; } else { - const res = await this.getResource(script.uuid, u, type, load, oldResources); + const res = await this.getResource(script.uuid, u, type, oldResources); if (res) { ret[resourceKey] = res; } @@ -269,7 +317,27 @@ export class ResourceService { async loadByUrl(url: string, type: ResourceType): Promise { const u = parseUrlSRI(url); - const resp = await fetch(u.url); + + await fetchSemaphore.acquire(); + // Semaphore 锁 - 同期只有五个 fetch 一起执行 + const delay = randNum(100, 150); // 100~150ms delay before starting fetch + await sleep(delay); + // 执行 fetch, 若超过 800ms, 不会中止 fetch 但会启动下一个网络连接任务 + // 这只为了避免等候时间过长,同时又不会有过多网络任务同时发生,使Web伺服器返回错误 + const { result, err } = await withTimeoutNotify(fetch(u.url), 800, ({ done, timeouted, err }) => { + if (timeouted || done || err) { + // fetch 成功 或 发生错误 或 timeout 时解锁 + fetchSemaphore.release(); + } + }); + // Semaphore 锁已解锁。继续处理 fetch Response 的结果 + + if (err) { + throw new Error(`resource fetch failed: ${err.message || err}`); + } + + const resp = result! as Response; + if (resp.status !== 200) { throw new Error(`resource response status not 200: ${resp.status}`); } @@ -342,7 +410,7 @@ export class ResourceService { } requestGetScriptResources(script: Script): Promise<{ [key: string]: Resource }> { - return this.getScriptResources(script, false); + return this.getScriptResources(script); } init() { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 4fa3acbbc..c35f0c4c0 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1214,7 +1214,7 @@ export class RuntimeService { script.value = value; }), // 加载resource - resource.getScriptResources(script, false).then((resource) => { + resource.getScriptResources(script).then((resource) => { script.resource = resource; for (const name of Object.keys(resource)) { const res = script.resource[name]; diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index e7b4e3b75..b7a503127 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -649,7 +649,7 @@ export class ScriptService { const ret = buildScriptRunResourceBasic(script); return Promise.all([ this.valueService.getScriptValue(ret), - this.resourceService.getScriptResources(ret, true), + this.resourceService.getScriptResources(ret), this.scriptCodeDAO.get(script.uuid), ]).then(([value, resource, code]) => { if (!code) { From 79b68f761395efa0869a98320035711e02ac485e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:52:51 +0900 Subject: [PATCH 03/27] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20src/app/service/serv?= =?UTF-8?q?ice=5Fworker/runtime.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index c35f0c4c0..adcc664ea 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -14,6 +14,7 @@ import { buildScriptRunResourceBasic, compileInjectionCode, getUserScriptRegister, + parseUrlSRI, scriptURLPatternResults, } from "./utils"; import { @@ -679,7 +680,7 @@ export class RuntimeService { async buildAndSaveCompiledResourceFromScript(script: Script, withCode: boolean = false) { const scriptRes = withCode ? await this.script.buildScriptRunResource(script) : buildScriptRunResourceBasic(script); - const resources = withCode ? scriptRes.resource : await this.resource.getScriptResources(scriptRes, true); + const resources = withCode ? scriptRes.resource : await this.resource.getScriptResources(scriptRes); const resourceUrls = (script.metadata["require"] || []).map((res) => resources[res]?.url).filter((res) => res); const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes); if (!scriptMatchInfo) return undefined; @@ -1156,7 +1157,7 @@ export class RuntimeService { if (!enableScriptList.length) return null; const scriptCodes = {} as Record; - // 更新资源使用了file协议的脚本 + // 更新资源使用了file协议的脚本 ( 不能在其他地方更新嗎?? 見 Issue #918 ) const scriptsWithUpdatedResources = new Map(); for (const scriptRes of enableScriptList) { const uuid = scriptRes.uuid; @@ -1166,8 +1167,11 @@ export class RuntimeService { for (const [url, [sha512, type]] of Object.entries(resourceCheck)) { const resourceList = scriptRes.metadata[type]; if (!resourceList) continue; - const updatedResource = await this.resource.updateResource(scriptRes.uuid, url, type); + const u = parseUrlSRI(url); + const oldResources = await this.resource.getResourceModel(u); + const updatedResource = await this.resource.updateResource(scriptRes.uuid, u, type, oldResources); if (updatedResource.hash?.sha512 !== sha512) { + // ----- 感觉这里是跟 resource.updateResource 内容的更新重复了 ----- for (const uri of resourceList) { /** 资源键名 */ let resourceKey = uri; @@ -1197,6 +1201,7 @@ export class RuntimeService { } } } + // ----- 感觉这里是跟 resource.updateResource 内容的更新重复了 ----- } } if (resourceUpdated) { From e83aae2b04eca2ba6483caa4161a9938e35eb671 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:58:50 +0900 Subject: [PATCH 04/27] =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/runtime.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index adcc664ea..7f1578a53 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1168,8 +1168,17 @@ export class RuntimeService { const resourceList = scriptRes.metadata[type]; if (!resourceList) continue; const u = parseUrlSRI(url); - const oldResources = await this.resource.getResourceModel(u); + if (u.hash) { + // 如果有 校验hash 的话,根本不用更新本地资源呀! + continue; + } + // const oldResources = await this.resource.getResourceModel(u); + const oldResources = await this.resource.resourceDAO.get(u.url); const updatedResource = await this.resource.updateResource(scriptRes.uuid, u, type, oldResources); + if (updatedResource === oldResources) { + // 如果新旧一样就忽视吧 - 不用更新本地资源 + continue; + } if (updatedResource.hash?.sha512 !== sha512) { // ----- 感觉这里是跟 resource.updateResource 内容的更新重复了 ----- for (const uri of resourceList) { From 96f34be874f4c5e7f66bfde7208fbd41073b05cf Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:02:15 +0900 Subject: [PATCH 05/27] Update script.ts --- src/app/service/service_worker/script.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index b7a503127..43d2ad696 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -420,6 +420,7 @@ export class ScriptService { this.resourceService.updateResourceByType(script, "require-css"), this.resourceService.updateResourceByType(script, "resource"), ]); + // 如果资源不完整,还是要接受安装吗??? // 广播一下 // Runtime 會負責更新 CompiledResource From 983f89ea4220bbc997e3a5c39835095e5e51d7c7 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:05:55 +0900 Subject: [PATCH 06/27] fix --- src/app/service/service_worker/synchronize.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index e1da378ed..ceb046eb7 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -118,9 +118,9 @@ export class SynchronizeService { } const lastModificationDate = script.updatetime || script.createtime || undefined; const [values, valueRet] = await this.value.getScriptValueDetails(script); - const requires = await this.resource.getResourceByType(script, "require", false); - const requiresCss = await this.resource.getResourceByType(script, "require-css", false); - const resources = await this.resource.getResourceByType(script, "resource", false); + const requires = await this.resource.getResourceByType(script, "require"); + const requiresCss = await this.resource.getResourceByType(script, "require-css"); + const resources = await this.resource.getResourceByType(script, "resource"); const storage: ValueStorage = { data: { ...values }, ts: valueRet?.updatetime || lastModificationDate || Date.now(), From bbe6c1d6678cb9808c07c98d68d5d547462943cf Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:23:13 +0900 Subject: [PATCH 07/27] getResourceByType -> getResourceByTypes --- src/app/service/service_worker/resource.ts | 82 +++++++++---------- src/app/service/service_worker/synchronize.ts | 6 +- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 8f15ff5b8..a2b49a86c 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -108,11 +108,7 @@ export class ResourceService { } public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> { - const [require, require_css, resource] = await Promise.all([ - this.getResourceByType(script, "require"), - this.getResourceByType(script, "require-css"), - this.getResourceByType(script, "resource"), - ]); + const [require, require_css, resource] = await this.getResourceByTypes(script, ["require", "require-css", "resource"]); return { ...require, @@ -121,44 +117,46 @@ export class ResourceService { }; } - async getResourceByType(script: Script, type: ResourceType): Promise<{ [key: string]: Resource }> { - if (!script.metadata[type]) { - return {}; - } - const ret: { [key: string]: Resource } = {}; - await Promise.allSettled( - script.metadata[type].map(async (uri) => { - /** 资源键名 */ - let resourceKey = uri; - /** 文件路径 */ - let path: string | null = uri; - if (type === "resource") { - // @resource xxx https://... - const split = uri.split(/\s+/); - if (split.length === 2) { - resourceKey = split[0]; - path = split[1].trim(); - } else { - path = null; - } - } - if (path) { - const u = parseUrlSRI(path); - const oldResources = await this.getResourceModel(u); - if (uri.startsWith("file:///")) { - // 如果是file://协议,则每次请求更新一下文件 - const res = await this.updateResource(script.uuid, u, type, oldResources); - ret[resourceKey] = res; - } else { - const res = await this.getResource(script.uuid, u, type, oldResources); - if (res) { - ret[resourceKey] = res; + public getResourceByTypes(script: Script, types: ResourceType[]): Promise[]> { + const promises = types.map(async (type) => { + const ret: Record = {}; + if (script.metadata[type]) { + await Promise.allSettled( + script.metadata[type].map(async (uri) => { + /** 资源键名 */ + let resourceKey = uri; + /** 文件路径 */ + let path: string | null = uri; + if (type === "resource") { + // @resource xxx https://... + const split = uri.split(/\s+/); + if (split.length === 2) { + resourceKey = split[0]; + path = split[1].trim(); + } else { + path = null; + } } - } - } - }) - ); - return ret; + if (path) { + const u = parseUrlSRI(path); + const oldResources = await this.getResourceModel(u); + if (uri.startsWith("file:///")) { + // 如果是file://协议,则每次请求更新一下文件 + const res = await this.updateResource(script.uuid, u, type, oldResources); + ret[resourceKey] = res; + } else { + const res = await this.getResource(script.uuid, u, type, oldResources); + if (res) { + ret[resourceKey] = res; + } + } + } + }) + ); + } + return ret; + }); + return Promise.all(promises); } // 只需要等待Promise返回,不理会返回值(失败也可以) diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index ceb046eb7..1f71cd8bf 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -118,9 +118,9 @@ export class SynchronizeService { } const lastModificationDate = script.updatetime || script.createtime || undefined; const [values, valueRet] = await this.value.getScriptValueDetails(script); - const requires = await this.resource.getResourceByType(script, "require"); - const requiresCss = await this.resource.getResourceByType(script, "require-css"); - const resources = await this.resource.getResourceByType(script, "resource"); + const [requires, requiresCss, resources] = await this.resource.getResourceByTypes(script, [ + "require", "require-css", "resource" + ]); const storage: ValueStorage = { data: { ...values }, ts: valueRet?.updatetime || lastModificationDate || Date.now(), From 49579bf069478802fac42e99faee1828b5c4cf0d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:28:56 +0900 Subject: [PATCH 08/27] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=B3=A8=E6=84=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index a2b49a86c..f9ea3bcd5 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -109,6 +109,17 @@ export class ResourceService { public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> { const [require, require_css, resource] = await this.getResourceByTypes(script, ["require", "require-css", "resource"]); + const ret = { + ...require, + ...require_css, + ...resource, + }; + + // 注意! 如果它们包含相同名字的Resource,会根据次序而覆盖 + const recordKeyLens = [ret, require, require_css, resource].map((record) => Object.keys(record).length); + if (recordKeyLens[0] !== recordKeyLens[1] + recordKeyLens[2] + recordKeyLens[3]) { + console.warn("One or more properties are merged in ResourceService.getScriptResources"); + } return { ...require, From 3e8f04e2375cfb84bd200636635c58aa9ace7ec2 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:30:38 +0900 Subject: [PATCH 09/27] getScriptResources -> getScriptResourceValue --- src/app/service/service_worker/resource.ts | 4 ++-- src/app/service/service_worker/runtime.ts | 4 ++-- src/app/service/service_worker/script.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index f9ea3bcd5..a7df401e2 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -107,7 +107,7 @@ export class ResourceService { return undefined; } - public async getScriptResources(script: Script): Promise<{ [key: string]: Resource }> { + public async getScriptResourceValue(script: Script): Promise<{ [key: string]: Resource }> { const [require, require_css, resource] = await this.getResourceByTypes(script, ["require", "require-css", "resource"]); const ret = { ...require, @@ -419,7 +419,7 @@ export class ResourceService { } requestGetScriptResources(script: Script): Promise<{ [key: string]: Resource }> { - return this.getScriptResources(script); + return this.getScriptResourceValue(script); } init() { diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 7f1578a53..e1ed2f4d7 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -680,7 +680,7 @@ export class RuntimeService { async buildAndSaveCompiledResourceFromScript(script: Script, withCode: boolean = false) { const scriptRes = withCode ? await this.script.buildScriptRunResource(script) : buildScriptRunResourceBasic(script); - const resources = withCode ? scriptRes.resource : await this.resource.getScriptResources(scriptRes); + const resources = withCode ? scriptRes.resource : await this.resource.getScriptResourceValue(scriptRes); const resourceUrls = (script.metadata["require"] || []).map((res) => resources[res]?.url).filter((res) => res); const scriptMatchInfo = await this.applyScriptMatchInfo(scriptRes); if (!scriptMatchInfo) return undefined; @@ -1228,7 +1228,7 @@ export class RuntimeService { script.value = value; }), // 加载resource - resource.getScriptResources(script).then((resource) => { + resource.getScriptResourceValue(script).then((resource) => { script.resource = resource; for (const name of Object.keys(resource)) { const res = script.resource[name]; diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 43d2ad696..55806bce3 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -650,7 +650,7 @@ export class ScriptService { const ret = buildScriptRunResourceBasic(script); return Promise.all([ this.valueService.getScriptValue(ret), - this.resourceService.getScriptResources(ret), + this.resourceService.getScriptResourceValue(ret), this.scriptCodeDAO.get(script.uuid), ]).then(([value, resource, code]) => { if (!code) { From bee32f0b0aa44a2f02ab3375e08f38d0babdac2d Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:47:42 +0900 Subject: [PATCH 10/27] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index a7df401e2..3e13835e5 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -133,25 +133,26 @@ export class ResourceService { const ret: Record = {}; if (script.metadata[type]) { await Promise.allSettled( - script.metadata[type].map(async (uri) => { + script.metadata[type].map(async (mdValue) => { /** 资源键名 */ - let resourceKey = uri; + let resourceKey; /** 文件路径 */ - let path: string | null = uri; + let resourcePath: string; if (type === "resource") { // @resource xxx https://... - const split = uri.split(/\s+/); - if (split.length === 2) { - resourceKey = split[0]; - path = split[1].trim(); - } else { - path = null; - } + const split = mdValue.split(/\s+/); + if (split.length !== 2) return; // @resource 必须有 key 和 path. "xxx yyy zzz" 也不符合格式要求 + resourceKey = split[0]; + resourcePath = split[1].trim(); + } else { + // require / require-css 的话,使用 url 作为 resourceKey + resourceKey = mdValue; + resourcePath = mdValue; } - if (path) { - const u = parseUrlSRI(path); + if (resourcePath) { + const u = parseUrlSRI(resourcePath); const oldResources = await this.getResourceModel(u); - if (uri.startsWith("file:///")) { + if (mdValue.startsWith("file:///")) { // 如果是file://协议,则每次请求更新一下文件 const res = await this.updateResource(script.uuid, u, type, oldResources); ret[resourceKey] = res; From 495a78299ab2418e187e558c712b8c2e5e63f5a6 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:48:26 +0900 Subject: [PATCH 11/27] `mdValue.startsWith("file:///")` -> `resourcePath.startsWith("file:///")` --- src/app/service/service_worker/resource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 3e13835e5..f465cc8a5 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -152,7 +152,7 @@ export class ResourceService { if (resourcePath) { const u = parseUrlSRI(resourcePath); const oldResources = await this.getResourceModel(u); - if (mdValue.startsWith("file:///")) { + if (resourcePath.startsWith("file:///")) { // 如果是file://协议,则每次请求更新一下文件 const res = await this.updateResource(script.uuid, u, type, oldResources); ret[resourceKey] = res; From 1a84afd89b322a9e9adf90d98cb1cf896427ea8c Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:49:56 +0900 Subject: [PATCH 12/27] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index f465cc8a5..0750c3db6 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -131,9 +131,11 @@ export class ResourceService { public getResourceByTypes(script: Script, types: ResourceType[]): Promise[]> { const promises = types.map(async (type) => { const ret: Record = {}; - if (script.metadata[type]) { + const metadataEntries = script.metadata[type]; + const uuid = script.uuid; + if (metadataEntries) { await Promise.allSettled( - script.metadata[type].map(async (mdValue) => { + metadataEntries.map(async (mdValue) => { /** 资源键名 */ let resourceKey; /** 文件路径 */ @@ -154,10 +156,10 @@ export class ResourceService { const oldResources = await this.getResourceModel(u); if (resourcePath.startsWith("file:///")) { // 如果是file://协议,则每次请求更新一下文件 - const res = await this.updateResource(script.uuid, u, type, oldResources); + const res = await this.updateResource(uuid, u, type, oldResources); ret[resourceKey] = res; } else { - const res = await this.getResource(script.uuid, u, type, oldResources); + const res = await this.getResource(uuid, u, type, oldResources); if (res) { ret[resourceKey] = res; } From 658b256bf491370030fcac606796e40a47b52813 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:58:02 +0900 Subject: [PATCH 13/27] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 25 ++++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 0750c3db6..2d2c07d88 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -154,16 +154,27 @@ export class ResourceService { if (resourcePath) { const u = parseUrlSRI(resourcePath); const oldResources = await this.getResourceModel(u); - if (resourcePath.startsWith("file:///")) { - // 如果是file://协议,则每次请求更新一下文件 - const res = await this.updateResource(uuid, u, type, oldResources); - ret[resourceKey] = res; + let freshResource: Resource | undefined = undefined; + if (oldResources && !resourcePath.startsWith("file:///")) { + // 读取过但失败的资源加载也会被放在缓存,避免再加载资源 + // 因此 getResource 时不会再加载资源,直接返回 undefined 表示没有资源 + if (!oldResources.contentType) { + freshResource = undefined; + } else { + freshResource = oldResources; + } } else { - const res = await this.getResource(uuid, u, type, oldResources); - if (res) { - ret[resourceKey] = res; + // 1) 如果是file://协议,则每次请求更新一下文件 + // 2) 缓存中无资源加载纪录,需要取得资源 + try { + freshResource = await this.updateResource(uuid, u, type, oldResources); + } catch (e: any) { + this.logger.error("load resource error", { url: u.originalUrl }, Logger.E(e)); } } + if (freshResource) { + ret[resourceKey] = freshResource; + } } }) ); From 582385542c472f1c4392c4d597b8fa3ce294df89 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 09:58:45 +0900 Subject: [PATCH 14/27] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 2d2c07d88..3b7e6fec6 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -85,28 +85,6 @@ export class ResourceService { this.resourceDAO.enableCache(); } - public async getResource( - uuid: string, - u: TUrlSRIInfo, - type: ResourceType, - oldResources: Resource | undefined - ): Promise { - if (oldResources) { - // 读取过但失败的资源加载也会被放在缓存,避免再加载资源 - // 因此 getResource 时不会再加载资源,直接返回 undefined 表示没有资源 - if (!oldResources.contentType) return undefined; - return oldResources; - } - // 缓存中无资源加载纪录,需要取得资源 - const url = u.originalUrl; - try { - return await this.updateResource(uuid, u, type, undefined); - } catch (e: any) { - this.logger.error("load resource error", { url }, Logger.E(e)); - } - return undefined; - } - public async getScriptResourceValue(script: Script): Promise<{ [key: string]: Resource }> { const [require, require_css, resource] = await this.getResourceByTypes(script, ["require", "require-css", "resource"]); const ret = { From c3d395c1a2129d53993070b90aae173c383bf4d1 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:07:15 +0900 Subject: [PATCH 15/27] `loadByUrl` -> `createResourceByUrlFetch` --- src/app/service/service_worker/resource.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 3b7e6fec6..e506b52b8 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -205,7 +205,7 @@ export class ResourceService { if (oldResources === null) oldResources = await this.getResourceModel(u); let result: Resource; try { - const resource = await this.loadByUrl(u.url, type); + const resource = await this.createResourceByUrlFetch(u.url, type); const now = Date.now(); resource.updatetime = now; if (!oldResources || !oldResources.contentType) { @@ -316,7 +316,7 @@ export class ResourceService { }); } - async loadByUrl(url: string, type: ResourceType): Promise { + async createResourceByUrlFetch(url: string, type: ResourceType): Promise { const u = parseUrlSRI(url); await fetchSemaphore.acquire(); From 1a434b82e8be68b042836593b1a53c02883ed413 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:10:37 +0900 Subject: [PATCH 16/27] =?UTF-8?q?=E7=AE=80=E5=8C=96=20updateResource=20sig?= =?UTF-8?q?nature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index e506b52b8..64c1b29b7 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -199,10 +199,8 @@ export class ResourceService { uuid: string, u: TUrlSRIInfo, type: ResourceType, - oldResources: Resource | null | undefined = null + oldResources: Resource | undefined ) { - // 重新加载 - if (oldResources === null) oldResources = await this.getResourceModel(u); let result: Resource; try { const resource = await this.createResourceByUrlFetch(u.url, type); From da70c268ea553baa8c195eb9c50b84b669c1f119 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:13:36 +0900 Subject: [PATCH 17/27] =?UTF-8?q?=E7=AE=80=E5=8C=96=20createResourceByUrlF?= =?UTF-8?q?etch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 64c1b29b7..7d56a59a7 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -203,7 +203,7 @@ export class ResourceService { ) { let result: Resource; try { - const resource = await this.createResourceByUrlFetch(u.url, type); + const resource = await this.createResourceByUrlFetch(u, type); const now = Date.now(); resource.updatetime = now; if (!oldResources || !oldResources.contentType) { @@ -314,8 +314,8 @@ export class ResourceService { }); } - async createResourceByUrlFetch(url: string, type: ResourceType): Promise { - const u = parseUrlSRI(url); + async createResourceByUrlFetch(u: TUrlSRIInfo, type: ResourceType): Promise { + const url = u.url; // 无 URI Integrity Hash await fetchSemaphore.acquire(); // Semaphore 锁 - 同期只有五个 fetch 一起执行 @@ -323,7 +323,7 @@ export class ResourceService { await sleep(delay); // 执行 fetch, 若超过 800ms, 不会中止 fetch 但会启动下一个网络连接任务 // 这只为了避免等候时间过长,同时又不会有过多网络任务同时发生,使Web伺服器返回错误 - const { result, err } = await withTimeoutNotify(fetch(u.url), 800, ({ done, timeouted, err }) => { + const { result, err } = await withTimeoutNotify(fetch(url), 800, ({ done, timeouted, err }) => { if (timeouted || done || err) { // fetch 成功 或 发生错误 或 timeout 时解锁 fetchSemaphore.release(); From 8b1fc2465e9c52dce55f643c7d8ab3db1ca7b2ff Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:24:00 +0900 Subject: [PATCH 18/27] lint --- src/app/service/service_worker/resource.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 7d56a59a7..98d483187 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -86,7 +86,11 @@ export class ResourceService { } public async getScriptResourceValue(script: Script): Promise<{ [key: string]: Resource }> { - const [require, require_css, resource] = await this.getResourceByTypes(script, ["require", "require-css", "resource"]); + const [require, require_css, resource] = await this.getResourceByTypes(script, [ + "require", + "require-css", + "resource", + ]); const ret = { ...require, ...require_css, @@ -195,12 +199,7 @@ export class ResourceService { if (promises?.length) return Promise.allSettled(promises); } - async updateResource( - uuid: string, - u: TUrlSRIInfo, - type: ResourceType, - oldResources: Resource | undefined - ) { + async updateResource(uuid: string, u: TUrlSRIInfo, type: ResourceType, oldResources: Resource | undefined) { let result: Resource; try { const resource = await this.createResourceByUrlFetch(u, type); From 0c991f25956d40fc5d437c36266f5e23e2fb5b46 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:35:16 +0900 Subject: [PATCH 19/27] `updateResourceByType` -> `updateResourceByTypes` --- src/app/service/service_worker/resource.ts | 56 ++++++++++++---------- src/app/service/service_worker/script.ts | 4 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 98d483187..b7a4a7139 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -167,36 +167,40 @@ export class ResourceService { } // 只需要等待Promise返回,不理会返回值(失败也可以) - updateResourceByType(script: Script, type: ResourceType): Promise | void { + updateResourceByTypes(script: Script, types: ResourceType[]): Promise { const uuid = script.uuid; - const promises = script.metadata[type]?.map(async (u) => { - let url = ""; - if (type === "resource") { - const split = u.split(/\s+/); - if (split.length === 2) { - url = split[1]; + const metadata = script.metadata; + const promises = types.map((type) => { + const promises = metadata[type]?.map(async (u) => { + let url = ""; + if (type === "resource") { + const split = u.split(/\s+/); + if (split.length === 2) { + url = split[1]; + } + } else { + url = u; } - } else { - url = u; - } - if (url) { - // 检查资源是否存在,如果不存在则重新加载 - // 如果有旧资源,而没有新资讯,则继续使用旧资源 - // 只需要等待Promise返回,不理会返回值(失败也可以) - const u = parseUrlSRI(url); - const oldResources = await this.getResourceModel(u); - const updateTime = oldResources?.updatetime; - // 资源最后更新是24小时内则不更新 - if (updateTime && updateTime > Date.now() - 86400_000) return; - // 旧资源或没有资源记录,尝试更新 - try { - await this.updateResource(uuid, u, type, oldResources); - } catch (e: any) { - this.logger.error("check resource failed", { uuid, url }, Logger.E(e)); + if (url) { + // 检查资源是否存在,如果不存在则重新加载 + // 如果有旧资源,而没有新资讯,则继续使用旧资源 + // 只需要等待Promise返回,不理会返回值(失败也可以) + const u = parseUrlSRI(url); + const oldResources = await this.getResourceModel(u); + const updateTime = oldResources?.updatetime; + // 资源最后更新是24小时内则不更新 + if (updateTime && updateTime > Date.now() - 86400_000) return; + // 旧资源或没有资源记录,尝试更新 + try { + await this.updateResource(uuid, u, type, oldResources); + } catch (e: any) { + this.logger.error("check resource failed", { uuid, url }, Logger.E(e)); + } } - } + }); + if (promises?.length) return Promise.allSettled(promises); }); - if (promises?.length) return Promise.allSettled(promises); + return Promise.all(promises); } async updateResource(uuid: string, u: TUrlSRIInfo, type: ResourceType, oldResources: Resource | undefined) { diff --git a/src/app/service/service_worker/script.ts b/src/app/service/service_worker/script.ts index 741306d67..63c2edbd9 100644 --- a/src/app/service/service_worker/script.ts +++ b/src/app/service/service_worker/script.ts @@ -415,9 +415,7 @@ export class ScriptService { // Cache更新 & 下载资源 await Promise.all([ compiledResourceUpdatePromise, - this.resourceService.updateResourceByType(script, "require"), - this.resourceService.updateResourceByType(script, "require-css"), - this.resourceService.updateResourceByType(script, "resource"), + this.resourceService.updateResourceByTypes(script, ["require", "require-css", "resource"]), ]); // 如果资源不完整,还是要接受安装吗??? From d6d2a29d52997c093d6c30f2e21844c579c24836 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:35:32 +0900 Subject: [PATCH 20/27] =?UTF-8?q?=E5=8A=A0=E6=B3=A8=E9=87=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index b7a4a7139..281e525a3 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -189,6 +189,7 @@ export class ResourceService { const oldResources = await this.getResourceModel(u); const updateTime = oldResources?.updatetime; // 资源最后更新是24小时内则不更新 + // 这里是假设 resources 都是 static. 使用者应该加 ?d=xxxx 之类的方式提示SC要更新资源 if (updateTime && updateTime > Date.now() - 86400_000) return; // 旧资源或没有资源记录,尝试更新 try { From 8892d60d41a0b8106b5dafa6d0bada60f807f365 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:39:20 +0900 Subject: [PATCH 21/27] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96=20-?= =?UTF-8?q?=20=E8=B5=84=E6=BA=90=E6=9B=B4=E6=96=B0=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 281e525a3..1ea817a6f 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -187,10 +187,13 @@ export class ResourceService { // 只需要等待Promise返回,不理会返回值(失败也可以) const u = parseUrlSRI(url); const oldResources = await this.getResourceModel(u); - const updateTime = oldResources?.updatetime; - // 资源最后更新是24小时内则不更新 - // 这里是假设 resources 都是 static. 使用者应该加 ?d=xxxx 之类的方式提示SC要更新资源 - if (updateTime && updateTime > Date.now() - 86400_000) return; + // 非空值 url 且 url 不是本地档案 -> 检查最后更新时间 + if (u.url && !u.url.startsWith("file:///")) { + const updateTime = oldResources?.updatetime; + // 资源最后更新是24小时内则不更新 + // 这里是假设 resources 都是 static. 使用者应该加 ?d=xxxx 之类的方式提示SC要更新资源 + if (updateTime && updateTime > Date.now() - 86400_000) return; + } // 旧资源或没有资源记录,尝试更新 try { await this.updateResource(uuid, u, type, oldResources); From ab7a08215a3ae225a150a0713716dc1e90264109 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:40:10 +0900 Subject: [PATCH 22/27] =?UTF-8?q?=E6=B3=A8=E9=87=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 1ea817a6f..c8b545c0d 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -194,7 +194,7 @@ export class ResourceService { // 这里是假设 resources 都是 static. 使用者应该加 ?d=xxxx 之类的方式提示SC要更新资源 if (updateTime && updateTime > Date.now() - 86400_000) return; } - // 旧资源或没有资源记录,尝试更新 + // 旧资源或没有资源记录或本地档案,尝试更新 try { await this.updateResource(uuid, u, type, oldResources); } catch (e: any) { From 0dcd7efa1e5bac8dbf7087d7d00dfb25b466464e Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 10:44:50 +0900 Subject: [PATCH 23/27] lint --- src/app/service/service_worker/synchronize.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/service/service_worker/synchronize.ts b/src/app/service/service_worker/synchronize.ts index 993c76592..bb2d3b262 100644 --- a/src/app/service/service_worker/synchronize.ts +++ b/src/app/service/service_worker/synchronize.ts @@ -126,7 +126,9 @@ export class SynchronizeService { const lastModificationDate = script.updatetime || script.createtime || undefined; const [values, valueRet] = await this.value.getScriptValueDetails(script); const [requires, requiresCss, resources] = await this.resource.getResourceByTypes(script, [ - "require", "require-css", "resource" + "require", + "require-css", + "resource", ]); const storage: ValueStorage = { data: { ...values }, From 360f6d4a99dccbc2992fe5380c34993defa6d27a Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:32:27 +0900 Subject: [PATCH 24/27] =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BB=A3=E7=A0=81=20-?= =?UTF-8?q?=20updateResource=20&=20createResourceByUrlFetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/repo/resource.ts | 2 +- src/app/service/service_worker/resource.ts | 114 +++++++++++---------- src/app/service/service_worker/runtime.ts | 2 +- 3 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/app/repo/resource.ts b/src/app/repo/resource.ts index 5aa0889b2..c3dd2da1e 100644 --- a/src/app/repo/resource.ts +++ b/src/app/repo/resource.ts @@ -11,7 +11,7 @@ export interface Resource { hash: ResourceHash; type: ResourceType; link: { [key: string]: boolean }; // 关联的脚本 - contentType: string; + contentType: string; // 下载成功的话必定有 contentType. 下载失败的话则没有 (空Resource) createtime: number; updatetime?: number; } diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index c8b545c0d..176c51998 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -150,11 +150,13 @@ export class ResourceService { // 2) 缓存中无资源加载纪录,需要取得资源 try { freshResource = await this.updateResource(uuid, u, type, oldResources); + // 没有 oldResources 时,下载资源失败还是会生成一个空 Resource,避免重复尝试失败的下载 } catch (e: any) { this.logger.error("load resource error", { url: u.originalUrl }, Logger.E(e)); } } if (freshResource) { + // 空资源也储存一下,确保 resourceDAO 的记录和 script 的 resourceValue 记录一致 ret[resourceKey] = freshResource; } } @@ -187,9 +189,9 @@ export class ResourceService { // 只需要等待Promise返回,不理会返回值(失败也可以) const u = parseUrlSRI(url); const oldResources = await this.getResourceModel(u); - // 非空值 url 且 url 不是本地档案 -> 检查最后更新时间 - if (u.url && !u.url.startsWith("file:///")) { - const updateTime = oldResources?.updatetime; + // 非空值 url 且 url 不是本地档案 -> 检查最后更新时间 (空资源除外) + if (u.url && !u.url.startsWith("file:///") && oldResources?.contentType) { + const updateTime = oldResources.updatetime; // 资源最后更新是24小时内则不更新 // 这里是假设 resources 都是 static. 使用者应该加 ?d=xxxx 之类的方式提示SC要更新资源 if (updateTime && updateTime > Date.now() - 86400_000) return; @@ -209,43 +211,43 @@ export class ResourceService { async updateResource(uuid: string, u: TUrlSRIInfo, type: ResourceType, oldResources: Resource | undefined) { let result: Resource; + let resource: Resource | undefined; try { - const resource = await this.createResourceByUrlFetch(u, type); - const now = Date.now(); - resource.updatetime = now; - if (!oldResources || !oldResources.contentType) { - // 资源不存在,保存 - resource.createtime = now; - resource.link = { [uuid]: true }; - await this.resourceDAO.save(resource); - result = resource; - this.logger.info("reload new resource success", { url: u.url }); + resource = await this.createResourceByUrlFetch(u, type); + } catch (e) { + this.logger.error("fetch resource error", { url: u.url }, Logger.E(e)); + } + try { + if (resource) { + if (!oldResources || !oldResources.contentType) { + // 资源不存在,保存 + resource.link = { [uuid]: true }; + result = resource; + await this.resourceDAO.save(result).catch(console.warn); + this.logger.info("reload new resource success", { url: u.url }); + } else { + result = { + ...oldResources, + base64: resource.base64, + content: resource.content, + contentType: resource.contentType, + hash: resource.hash, + updatetime: resource.updatetime, + link: { ...oldResources.link, [uuid]: true }, + }; + await this.resourceDAO.save(result).catch(console.warn); + this.logger.info("reload resource success", { + url: u.url, + }); + } + return result; } else { + // 如果有旧资源,则使用旧资源 + if (oldResources) return oldResources; + // 资源错误时(且没有旧资源)保存一个空纪录以防止再度尝试加载 + // this.resourceDAO.save 自身出错的话忽略 + const now = Date.now(); result = { - ...oldResources, - base64: resource.base64, - content: resource.content, - contentType: resource.contentType, - hash: resource.hash, - updatetime: resource.updatetime, - link: { ...oldResources.link, [uuid]: true }, - }; - await this.resourceDAO.update(result.url, result); - this.logger.info("reload resource success", { - url: u.url, - }); - } - return result; - } catch (e) { - // 如果有旧资源,则使用旧资源 - if (oldResources) { - this.logger.error("load resource error - fallback to old resource", { url: u.url }, Logger.E(e)); - return oldResources; - } - // 资源错误时(且没有旧资源)保存一个空纪录以防止再度尝试加载 - // this.resourceDAO.save 自身出错的话忽略 - await this.resourceDAO - .save({ url: u.url, content: "", contentType: "", @@ -259,9 +261,13 @@ export class ResourceService { base64: "", link: { [uuid]: true }, type, - createtime: Date.now(), - }) - .catch(console.warn); + createtime: now, + updatetime: now, + }; + await this.resourceDAO.save(result).catch(console.warn); + return result; // 下载失败还是回传一下 result + } + } catch (e) { this.logger.error("load resource error", { url: u.url }, Logger.E(e)); throw e; } @@ -354,25 +360,27 @@ export class ResourceService { blobToBase64(data), ]); const contentType = resp.headers.get("content-type"); - const resource: Resource = { - url: u.url, - content: "", - contentType: (contentType || "application/octet-stream").split(";")[0], - hash: hash, - base64: "", - link: {}, - type, - createtime: Date.now(), - }; + let content: string = ""; const uint8Array = new Uint8Array(arrayBuffer); if (isText(uint8Array)) { if (type === "require" || type === "require-css") { - resource.content = await readBlobContent(data, contentType); // @require和@require-css 是会转换成代码运行的,可以进行解码 + content = await readBlobContent(data, contentType); // @require和@require-css 是会转换成代码运行的,可以进行解码 } else { - resource.content = await data.text(); // @resource 应该要保留原汁原味 + content = await data.text(); // @resource 应该要保留原汁原味 } } - resource.base64 = base64 || ""; + const now = Date.now(); + const resource: Resource = { + url: u.url, + content: content, + contentType: (contentType || "application/octet-stream").split(";")[0], // 保证下载成功时必定有 contentType + hash: hash, + base64: base64 || "", + link: {}, + type, + createtime: now, + updatetime: now, + }; return resource; } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index e1ed2f4d7..54386806e 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1157,7 +1157,7 @@ export class RuntimeService { if (!enableScriptList.length) return null; const scriptCodes = {} as Record; - // 更新资源使用了file协议的脚本 ( 不能在其他地方更新嗎?? 見 Issue #918 ) + // 更新资源使用了file协议的脚本 ( 不能在其他地方更新吗?? 见 Issue #918 ) const scriptsWithUpdatedResources = new Map(); for (const scriptRes of enableScriptList) { const uuid = scriptRes.uuid; From 89d7a0f56b56edbb7fe9620c9eb7962381b4f633 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 14:41:46 +0900 Subject: [PATCH 25/27] =?UTF-8?q?=E7=BB=9F=E4=B8=80=20try=20catch=20?= =?UTF-8?q?=E5=9C=A8=20updateResource=20=E9=87=8C=E8=BF=9B=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 21 +++++---------------- src/app/service/service_worker/runtime.ts | 5 +++-- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 176c51998..b427e4fd3 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -148,12 +148,8 @@ export class ResourceService { } else { // 1) 如果是file://协议,则每次请求更新一下文件 // 2) 缓存中无资源加载纪录,需要取得资源 - try { - freshResource = await this.updateResource(uuid, u, type, oldResources); - // 没有 oldResources 时,下载资源失败还是会生成一个空 Resource,避免重复尝试失败的下载 - } catch (e: any) { - this.logger.error("load resource error", { url: u.originalUrl }, Logger.E(e)); - } + freshResource = await this.updateResource(uuid, u, type, oldResources); + // 没有 oldResources 时,下载资源失败还是会生成一个空 Resource,避免重复尝试失败的下载 } if (freshResource) { // 空资源也储存一下,确保 resourceDAO 的记录和 script 的 resourceValue 记录一致 @@ -197,11 +193,7 @@ export class ResourceService { if (updateTime && updateTime > Date.now() - 86400_000) return; } // 旧资源或没有资源记录或本地档案,尝试更新 - try { - await this.updateResource(uuid, u, type, oldResources); - } catch (e: any) { - this.logger.error("check resource failed", { uuid, url }, Logger.E(e)); - } + await this.updateResource(uuid, u, type, oldResources); } }); if (promises?.length) return Promise.allSettled(promises); @@ -236,9 +228,7 @@ export class ResourceService { link: { ...oldResources.link, [uuid]: true }, }; await this.resourceDAO.save(result).catch(console.warn); - this.logger.info("reload resource success", { - url: u.url, - }); + this.logger.info("reload resource success", { url: u.url }); } return result; } else { @@ -268,8 +258,7 @@ export class ResourceService { return result; // 下载失败还是回传一下 result } } catch (e) { - this.logger.error("load resource error", { url: u.url }, Logger.E(e)); - throw e; + this.logger.error("Unexpected error in updateResource", { url: u.url }, Logger.E(e)); } } diff --git a/src/app/service/service_worker/runtime.ts b/src/app/service/service_worker/runtime.ts index 54386806e..fad5d238a 100644 --- a/src/app/service/service_worker/runtime.ts +++ b/src/app/service/service_worker/runtime.ts @@ -1175,8 +1175,9 @@ export class RuntimeService { // const oldResources = await this.resource.getResourceModel(u); const oldResources = await this.resource.resourceDAO.get(u.url); const updatedResource = await this.resource.updateResource(scriptRes.uuid, u, type, oldResources); - if (updatedResource === oldResources) { - // 如果新旧一样就忽视吧 - 不用更新本地资源 + if (!updatedResource || !updatedResource.contentType || updatedResource === oldResources) { + // updateResource 出错 或 下载失败则忽略 + // 如果新旧一样也忽视吧 - 不用更新本地资源 continue; } if (updatedResource.hash?.sha512 !== sha512) { From 05e54f7aeec5921cb081b410d736dfc409ea72e5 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 15:00:20 +0900 Subject: [PATCH 26/27] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20Semaphore=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 28 ++++++++++++---------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index b427e4fd3..153f399f8 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -16,29 +16,27 @@ import { blobToUint8Array } from "@App/pkg/utils/datatype"; import { readBlobContent } from "@App/pkg/utils/encoding"; class Semaphore { - private running = 0; + private active = 0; private readonly queue: Array<() => void> = []; constructor(readonly limit: number) { if (limit < 1) throw new Error("limit must be >= 1"); } - async acquire(): Promise { - if (this.running < this.limit) { - this.running++; - return; + async acquire() { + if (this.active >= this.limit) { + await new Promise((resolve) => this.queue.push(resolve)); } - await new Promise((resolve) => this.queue.push(resolve)); - this.running++; + this.active++; } - release(): void { - if (this.running <= 0) { + release() { + if (this.active > 0) { + this.active--; + this.queue.shift()?.(); + } else { console.warn("Semaphore double release detected"); - return; } - this.running--; - this.queue.shift()?.(); } } @@ -319,6 +317,7 @@ export class ResourceService { async createResourceByUrlFetch(u: TUrlSRIInfo, type: ResourceType): Promise { const url = u.url; // 无 URI Integrity Hash + let released = false; await fetchSemaphore.acquire(); // Semaphore 锁 - 同期只有五个 fetch 一起执行 const delay = randNum(100, 150); // 100~150ms delay before starting fetch @@ -328,7 +327,10 @@ export class ResourceService { const { result, err } = await withTimeoutNotify(fetch(url), 800, ({ done, timeouted, err }) => { if (timeouted || done || err) { // fetch 成功 或 发生错误 或 timeout 时解锁 - fetchSemaphore.release(); + if (!released) { + released = true; + fetchSemaphore.release(); + } } }); // Semaphore 锁已解锁。继续处理 fetch Response 的结果 From b0f9318103084b1eaa57ab673d17c161621ca47b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:09:40 +0900 Subject: [PATCH 27/27] =?UTF-8?q?=E6=8A=8A=E5=B9=B6=E8=A1=8C=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E7=9A=84=E4=BB=A3=E7=A0=81=E7=A7=BB=E5=8A=A8=E8=87=B3?= =?UTF-8?q?=20concurrency-control.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/service/service_worker/resource.ts | 55 +-------------------- src/pkg/utils/concurrency-control.ts | 57 ++++++++++++++++++++++ 2 files changed, 58 insertions(+), 54 deletions(-) create mode 100644 src/pkg/utils/concurrency-control.ts diff --git a/src/app/service/service_worker/resource.ts b/src/app/service/service_worker/resource.ts index 153f399f8..ef10e1eee 100644 --- a/src/app/service/service_worker/resource.ts +++ b/src/app/service/service_worker/resource.ts @@ -14,63 +14,10 @@ import { isBase64, parseUrlSRI, type TUrlSRIInfo } from "./utils"; import { stackAsyncTask } from "@App/pkg/utils/async_queue"; import { blobToUint8Array } from "@App/pkg/utils/datatype"; import { readBlobContent } from "@App/pkg/utils/encoding"; - -class Semaphore { - private active = 0; - private readonly queue: Array<() => void> = []; - - constructor(readonly limit: number) { - if (limit < 1) throw new Error("limit must be >= 1"); - } - - async acquire() { - if (this.active >= this.limit) { - await new Promise((resolve) => this.queue.push(resolve)); - } - this.active++; - } - - release() { - if (this.active > 0) { - this.active--; - this.queue.shift()?.(); - } else { - console.warn("Semaphore double release detected"); - } - } -} +import { Semaphore, withTimeoutNotify } from "@App/pkg/utils/concurrency-control"; const fetchSemaphore = new Semaphore(5); -type TWithTimeoutNotifyResult = { - timeouted: boolean; - result: T | undefined; - done: boolean; - err: undefined | Error; -}; -const withTimeoutNotify = (promise: Promise, time: number, fn: (res: TWithTimeoutNotifyResult) => any) => { - const res: TWithTimeoutNotifyResult = { timeouted: false, result: undefined, done: false, err: undefined }; - const cid = setTimeout(() => { - res.timeouted = true; - fn(res); - }, time); - return promise - .then((result: T) => { - clearTimeout(cid); - res.result = result; - res.done = true; - fn(res); - return res; - }) - .catch((e) => { - clearTimeout(cid); - res.err = e; - res.done = true; - fn(res); - return res; - }); -}; - export class ResourceService { logger: Logger; resourceDAO: ResourceDAO = new ResourceDAO(); diff --git a/src/pkg/utils/concurrency-control.ts b/src/pkg/utils/concurrency-control.ts new file mode 100644 index 000000000..f796e7034 --- /dev/null +++ b/src/pkg/utils/concurrency-control.ts @@ -0,0 +1,57 @@ +export class Semaphore { + private active = 0; + private readonly queue: Array<() => void> = []; + + constructor(readonly limit: number) { + if (limit < 1) throw new Error("limit must be >= 1"); + } + + async acquire() { + if (this.active >= this.limit) { + await new Promise((resolve) => this.queue.push(resolve)); + } + this.active++; + } + + release() { + if (this.active > 0) { + this.active--; + this.queue.shift()?.(); + } else { + console.warn("Semaphore double release detected"); + } + } +} + +type TWithTimeoutNotifyResult = { + timeouted: boolean; + result: T | undefined; + done: boolean; + err: undefined | Error; +}; +export const withTimeoutNotify = ( + promise: Promise, + time: number, + fn: (res: TWithTimeoutNotifyResult) => any +) => { + const res: TWithTimeoutNotifyResult = { timeouted: false, result: undefined, done: false, err: undefined }; + const cid = setTimeout(() => { + res.timeouted = true; + fn(res); + }, time); + return promise + .then((result: T) => { + clearTimeout(cid); + res.result = result; + res.done = true; + fn(res); + return res; + }) + .catch((e) => { + clearTimeout(cid); + res.err = e; + res.done = true; + fn(res); + return res; + }); +};