diff --git a/Makefile b/Makefile index e933da6..5440ad0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean install dev valid test prod +.PHONY: clean install dev valid test prod serve_mirror .DEFAULT_GOAL := dev DENO_DEV = NODE_ENV=development deno run --watch DENO_PROD = NODE_ENV=production deno run @@ -30,5 +30,13 @@ test: valid prod: test rm -rf $(BUILD_DIR) $(CHROME_ZIP) $(DENO_PROD) $(DENO_OPTIONS) $(BUILD_SCRIPT) + zip -r $(CHROME_ZIP) $(OUTPUT_DIR) ./manifest.json > /dev/null - tree -Dis $(BUILD_DIR) *.zip + zip --delete $(CHROME_ZIP) "$(OUTPUT_DIR)mirror.html" "$(BUILD_DIR)mirror/*" > /dev/null + + tree -Dis $(BUILD_DIR) *.zip | grep -E "api|zip" + +serve_mirror: + @echo "🎗 reminder to switch extension off" + @echo "served at: http://localhost:5555/mirror.html" + python3 -m http.server 5555 -d ./public/ diff --git a/README.md b/README.md index fb96f2b..9b5ee07 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,82 @@ -# Browser API Monitor +# API Monitor - Chrome Developer Tools extension - Available in Chrome Web Store as [API Monitor](https://chromewebstore.google.com/detail/api-monitor/bghmfoakiidiedpheejcjhciekobjcjp) -If you're web developer and want to assess implementation correctness - this tool adds additional panel to the browser’s DevTool that enables to see scheduled timeouts and active intervals, as well as to review and navigate to initiators of: eval, setTimeout, setInterval, requestAnimationFrame, requestIdleCallback and their terminator functions. +Chrome Developer Tools `API 🔎` panel tries to gather every bit of useful information from the usage of certain native functions that are prone to human errors, or are difficult to spot intuitively. -#### Allows: +#### Motivation -- to measure callback execution self-time. -- to see `requestAnimationFrame` callback request frame rate. -- visit every function in the call stack (if available), bypass or pause while debugging. -- detect `eval` function usage, see its argument and return value, same for `setTimeout` and `setInterval` when called with a string instead of a function. -- for every mounted video or audio media element's to see it’s state and properties. +To assess Web Application implementation correctness and expedite issues discovery. [See examples](./doc/issues.log.md). -#### Helps to spot: +#### Functionality + +- Gather callstack that is used to call every wrapped function: + - **short** - just the nearest initiator. + - **full** - from the root to the nearest initiator (from left to right). -- incorrect timeout delay. -- bad handler for terminator function. -- terminating non-existing or elapsed timeout. +- Aggregate information about currently scheduled timeouts and running active intervals. -#### Motivation: +- Gather details about which terminators are cancelling certain scheduled setters. -- To expedite issues discovery. +- Allow to initiate a debugging session by redirecting the code flow to a `debugger` breakpoint right before the callback invocation. + - Hit F11 (step inside) **twice** in order to progress into the callback itself. -#### Wrapped native functions: +- Allow to bypass (skip) setter's callback, or terminator invocation function. -- eval (by default off) -- setTimeout -- clearTimeout -- setInterval -- clearInterval -- requestAnimationFrame -- cancelAnimationFrame -- requestIdleCallback -- cancelIdleCallback +- Detect anomalies in passed arguments such as: + - Passing incorrect timeout delay to `setTimeout`, `setInterval`, `requestIdleCallback`. + - Correct one is `undefined` or a number that is greater or equal to `0`. + - Invoking terminator function with handler that is non-positive integer, or of non-existent or already elapsed setter. -> [!NOTE] -> While measuring performance of your code – consider disabling this extension as it may affect the results. +- Measure callback's execution self-time. + - Warn if it exceeds 4/5 (13.33ms) of 60 FPS hardcoded frame-rate (16.66ms). + - currently, there is no API to detect monitor refresh-rate at runtime due to browser security and privacy restrictions, hence hardcoded to 60 FPS. + +- Count `requestAnimationFrame` calls per second (CPS). + - If requested recursively - it reflects animation FPS. + +- Detect `eval` function usage in runtime, as well as `setTimeout` and `setInterval` when called with a `string` callback instead of a `function`. + +- Scan DOM each second for mounted `video` or `audio` media elements. + - Present control panel with basic media functions. + - Show media events and number of times they have been fired. + - Show current state of properties. + - Allow to toggle the state of changeable boolean properties e.g. `controls`, `preservesPitch`... + +- Prevent the system from going to Sleep state due to user inactivity for a better observational experience. + - By default `off`
- Example + Wrapped native functions + +- `eval` + - by default `off`, cause the fact of wrapping it, excludes the access to local scope variables from the eval script, and that, as a result, may brake the application if it does need it. +- `setTimeout` + - `clearTimeout` +- `setInterval` + - `clearInterval` +- `requestAnimationFrame` + - `cancelAnimationFrame` +- `requestIdleCallback` + - `cancelIdleCallback` + +
+
+ Screenshots ![screenshot](./doc/screenshot-02.png) ![screenshot](./doc/screenshot-04.png)
+> [!NOTE] +> While measuring performance of your code – consider disabling this extension as it may affect the results. + ### Build requirements - OS: Linux - Node: 22.14.0 (LTS) -- [Deno](https://docs.deno.com/runtime/getting_started/installation/) 2.2.12 +- [Deno](https://docs.deno.com/runtime/getting_started/installation/) 2.3.5 ### Build instructions diff --git a/build.ts b/build.ts index f563414..cd06602 100644 --- a/build.ts +++ b/build.ts @@ -17,6 +17,7 @@ const buildOptions: BuildOptions = { './src/api-monitor-cs-main.ts', './src/api-monitor-cs-isolated.ts', './src/api-monitor-devtools-panel.ts', + './src/mirror/mirror.ts', ], outdir: './public/build/', define: { diff --git a/deno.lock b/deno.lock index f0fdc3a..43bc820 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "jsr:@luca/esbuild-deno-loader@0.11.1": "0.11.1", "jsr:@std/assert@^1.0.12": "1.0.12", @@ -14,17 +14,17 @@ "jsr:@std/path@^1.0.8": "1.0.8", "jsr:@std/testing@^1.0.11": "1.0.11", "npm:@noble/hashes@1.8.0": "1.8.0", - "npm:@types/chrome@*": "0.0.317", - "npm:@types/chrome@0.0.317": "0.0.317", - "npm:@types/deno@2.2.0": "2.2.0", - "npm:esbuild-svelte@0.9.2": "0.9.2_esbuild@0.25.3_svelte@5.28.2__acorn@8.14.1", + "npm:@types/chrome@*": "0.0.326", + "npm:@types/chrome@0.0.326": "0.0.326", + "npm:@types/deno@2.3.0": "2.3.0", + "npm:esbuild-svelte@0.9.2": "0.9.2_esbuild@0.25.5_svelte@5.33.18__acorn@8.15.0", "npm:happy-dom@17.4.4": "17.4.4", "npm:jsondiffpatch@0.7.3": "0.7.3", - "npm:sass@1.87.0": "1.87.0", - "npm:sv@0.8.3": "0.8.3", - "npm:svelte-check@4.1.6": "4.1.6_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3", - "npm:svelte-preprocess@6.0.3": "6.0.3_sass@1.87.0_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3", - "npm:svelte@5.28.2": "5.28.2_acorn@8.14.1", + "npm:sass@1.89.1": "1.89.1", + "npm:sv@0.8.8": "0.8.8", + "npm:svelte-check@4.2.1": "4.2.1_svelte@5.33.18__acorn@8.15.0_typescript@5.8.3", + "npm:svelte-preprocess@6.0.3": "6.0.3_sass@1.89.1_svelte@5.33.18__acorn@8.15.0_typescript@5.8.3", + "npm:svelte@5.33.18": "5.33.18_acorn@8.15.0", "npm:typescript@5.8.3": "5.8.3" }, "jsr": { @@ -97,79 +97,254 @@ "integrity": "sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==" }, "@esbuild/aix-ppc64@0.25.3": { - "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==" + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "os": ["aix"], + "cpu": ["ppc64"] + }, + "@esbuild/aix-ppc64@0.25.5": { + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "os": ["aix"], + "cpu": ["ppc64"] }, "@esbuild/android-arm64@0.25.3": { - "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==" + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "os": ["android"], + "cpu": ["arm64"] + }, + "@esbuild/android-arm64@0.25.5": { + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "os": ["android"], + "cpu": ["arm64"] }, "@esbuild/android-arm@0.25.3": { - "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==" + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "os": ["android"], + "cpu": ["arm"] + }, + "@esbuild/android-arm@0.25.5": { + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "os": ["android"], + "cpu": ["arm"] }, "@esbuild/android-x64@0.25.3": { - "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==" + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "os": ["android"], + "cpu": ["x64"] + }, + "@esbuild/android-x64@0.25.5": { + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "os": ["android"], + "cpu": ["x64"] }, "@esbuild/darwin-arm64@0.25.3": { - "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==" + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "os": ["darwin"], + "cpu": ["arm64"] + }, + "@esbuild/darwin-arm64@0.25.5": { + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "os": ["darwin"], + "cpu": ["arm64"] }, "@esbuild/darwin-x64@0.25.3": { - "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==" + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "os": ["darwin"], + "cpu": ["x64"] + }, + "@esbuild/darwin-x64@0.25.5": { + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "os": ["darwin"], + "cpu": ["x64"] }, "@esbuild/freebsd-arm64@0.25.3": { - "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==" + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "os": ["freebsd"], + "cpu": ["arm64"] + }, + "@esbuild/freebsd-arm64@0.25.5": { + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "os": ["freebsd"], + "cpu": ["arm64"] }, "@esbuild/freebsd-x64@0.25.3": { - "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==" + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "os": ["freebsd"], + "cpu": ["x64"] + }, + "@esbuild/freebsd-x64@0.25.5": { + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "os": ["freebsd"], + "cpu": ["x64"] }, "@esbuild/linux-arm64@0.25.3": { - "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==" + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "os": ["linux"], + "cpu": ["arm64"] + }, + "@esbuild/linux-arm64@0.25.5": { + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "os": ["linux"], + "cpu": ["arm64"] }, "@esbuild/linux-arm@0.25.3": { - "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==" + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "os": ["linux"], + "cpu": ["arm"] + }, + "@esbuild/linux-arm@0.25.5": { + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "os": ["linux"], + "cpu": ["arm"] }, "@esbuild/linux-ia32@0.25.3": { - "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==" + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "os": ["linux"], + "cpu": ["ia32"] + }, + "@esbuild/linux-ia32@0.25.5": { + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "os": ["linux"], + "cpu": ["ia32"] }, "@esbuild/linux-loong64@0.25.3": { - "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==" + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "os": ["linux"], + "cpu": ["loong64"] + }, + "@esbuild/linux-loong64@0.25.5": { + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "os": ["linux"], + "cpu": ["loong64"] }, "@esbuild/linux-mips64el@0.25.3": { - "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==" + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "os": ["linux"], + "cpu": ["mips64el"] + }, + "@esbuild/linux-mips64el@0.25.5": { + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "os": ["linux"], + "cpu": ["mips64el"] }, "@esbuild/linux-ppc64@0.25.3": { - "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==" + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "os": ["linux"], + "cpu": ["ppc64"] + }, + "@esbuild/linux-ppc64@0.25.5": { + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "os": ["linux"], + "cpu": ["ppc64"] }, "@esbuild/linux-riscv64@0.25.3": { - "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==" + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "os": ["linux"], + "cpu": ["riscv64"] + }, + "@esbuild/linux-riscv64@0.25.5": { + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "os": ["linux"], + "cpu": ["riscv64"] }, "@esbuild/linux-s390x@0.25.3": { - "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==" + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "os": ["linux"], + "cpu": ["s390x"] + }, + "@esbuild/linux-s390x@0.25.5": { + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "os": ["linux"], + "cpu": ["s390x"] }, "@esbuild/linux-x64@0.25.3": { - "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==" + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "os": ["linux"], + "cpu": ["x64"] + }, + "@esbuild/linux-x64@0.25.5": { + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "os": ["linux"], + "cpu": ["x64"] }, "@esbuild/netbsd-arm64@0.25.3": { - "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==" + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "os": ["netbsd"], + "cpu": ["arm64"] + }, + "@esbuild/netbsd-arm64@0.25.5": { + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "os": ["netbsd"], + "cpu": ["arm64"] }, "@esbuild/netbsd-x64@0.25.3": { - "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==" + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "os": ["netbsd"], + "cpu": ["x64"] + }, + "@esbuild/netbsd-x64@0.25.5": { + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "os": ["netbsd"], + "cpu": ["x64"] }, "@esbuild/openbsd-arm64@0.25.3": { - "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==" + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "os": ["openbsd"], + "cpu": ["arm64"] + }, + "@esbuild/openbsd-arm64@0.25.5": { + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "os": ["openbsd"], + "cpu": ["arm64"] }, "@esbuild/openbsd-x64@0.25.3": { - "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==" + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "os": ["openbsd"], + "cpu": ["x64"] + }, + "@esbuild/openbsd-x64@0.25.5": { + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "os": ["openbsd"], + "cpu": ["x64"] }, "@esbuild/sunos-x64@0.25.3": { - "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==" + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "os": ["sunos"], + "cpu": ["x64"] + }, + "@esbuild/sunos-x64@0.25.5": { + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "os": ["sunos"], + "cpu": ["x64"] }, "@esbuild/win32-arm64@0.25.3": { - "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==" + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "os": ["win32"], + "cpu": ["arm64"] + }, + "@esbuild/win32-arm64@0.25.5": { + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "os": ["win32"], + "cpu": ["arm64"] }, "@esbuild/win32-ia32@0.25.3": { - "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==" + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "os": ["win32"], + "cpu": ["ia32"] + }, + "@esbuild/win32-ia32@0.25.5": { + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "os": ["win32"], + "cpu": ["ia32"] }, "@esbuild/win32-x64@0.25.3": { - "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==" + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "os": ["win32"], + "cpu": ["x64"] + }, + "@esbuild/win32-x64@0.25.5": { + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "os": ["win32"], + "cpu": ["x64"] }, "@jridgewell/gen-mapping@0.3.8": { "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", @@ -199,47 +374,79 @@ "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==" }, "@parcel/watcher-android-arm64@2.5.1": { - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==" + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "os": ["android"], + "cpu": ["arm64"] }, "@parcel/watcher-darwin-arm64@2.5.1": { - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==" + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "os": ["darwin"], + "cpu": ["arm64"] }, "@parcel/watcher-darwin-x64@2.5.1": { - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==" + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "os": ["darwin"], + "cpu": ["x64"] }, "@parcel/watcher-freebsd-x64@2.5.1": { - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==" + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "os": ["freebsd"], + "cpu": ["x64"] }, "@parcel/watcher-linux-arm-glibc@2.5.1": { - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==" + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "os": ["linux"], + "cpu": ["arm"] }, "@parcel/watcher-linux-arm-musl@2.5.1": { - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==" + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "os": ["linux"], + "cpu": ["arm"] }, "@parcel/watcher-linux-arm64-glibc@2.5.1": { - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==" + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "os": ["linux"], + "cpu": ["arm64"] }, "@parcel/watcher-linux-arm64-musl@2.5.1": { - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==" + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "os": ["linux"], + "cpu": ["arm64"] }, "@parcel/watcher-linux-x64-glibc@2.5.1": { - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==" + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "os": ["linux"], + "cpu": ["x64"] }, "@parcel/watcher-linux-x64-musl@2.5.1": { - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==" + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "os": ["linux"], + "cpu": ["x64"] }, "@parcel/watcher-win32-arm64@2.5.1": { - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==" + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "os": ["win32"], + "cpu": ["arm64"] }, "@parcel/watcher-win32-ia32@2.5.1": { - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==" + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "os": ["win32"], + "cpu": ["ia32"] }, "@parcel/watcher-win32-x64@2.5.1": { - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==" + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "os": ["win32"], + "cpu": ["x64"] }, "@parcel/watcher@2.5.1": { "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", "dependencies": [ + "detect-libc", + "is-glob", + "micromatch", + "node-addon-api" + ], + "optionalDependencies": [ "@parcel/watcher-android-arm64", "@parcel/watcher-darwin-arm64", "@parcel/watcher-darwin-x64", @@ -252,31 +459,28 @@ "@parcel/watcher-linux-x64-musl", "@parcel/watcher-win32-arm64", "@parcel/watcher-win32-ia32", - "@parcel/watcher-win32-x64", - "detect-libc", - "is-glob", - "micromatch", - "node-addon-api" - ] + "@parcel/watcher-win32-x64" + ], + "scripts": true }, - "@sveltejs/acorn-typescript@1.0.5_acorn@8.14.1": { + "@sveltejs/acorn-typescript@1.0.5_acorn@8.15.0": { "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", "dependencies": [ "acorn" ] }, - "@types/chrome@0.0.317": { - "integrity": "sha512-ibKycbXX8ZZToFshjgWg98BTvFUSvQht8m53Xc+87ye3Z6ZoHJubLjoiDsil8rtW+noWE+Z0+7y0nwLxArU+CQ==", + "@types/chrome@0.0.326": { + "integrity": "sha512-WS7jKf3ZRZFHOX7dATCZwqNJgdfiSF0qBRFxaO0LhIOvTNBrfkab26bsZwp6EBpYtqp8loMHJTnD6vDTLWPKYw==", "dependencies": [ "@types/filesystem", "@types/har-format" ] }, - "@types/deno@2.2.0": { - "integrity": "sha512-4x6M/ZSyoQy6fJeMArP0dvvNT4IOolfySyukuqqKhsLmSXDV4wGanqXIZ+xFihw3TlReS6JTa4hRG9nAZInpmw==" + "@types/deno@2.3.0": { + "integrity": "sha512-/4SyefQpKjwNKGkq9qG3Ln7MazfbWKvydyVFBnXzP5OQA4u1paoFtaOe1iHKycIWHHkhYag0lPxyheThV1ijzw==" }, - "@types/estree@1.0.7": { - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==" + "@types/estree@1.0.8": { + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" }, "@types/filesystem@0.0.36": { "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", @@ -290,8 +494,9 @@ "@types/har-format@1.2.16": { "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==" }, - "acorn@8.14.1": { - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==" + "acorn@8.15.0": { + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "bin": true }, "aria-query@5.3.2": { "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==" @@ -315,9 +520,10 @@ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" }, "detect-libc@1.0.3": { - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "bin": true }, - "esbuild-svelte@0.9.2_esbuild@0.25.3_svelte@5.28.2__acorn@8.14.1": { + "esbuild-svelte@0.9.2_esbuild@0.25.5_svelte@5.33.18__acorn@8.15.0": { "integrity": "sha512-8Jq6+rh+g1E2mkBOZKdYZ8JtlbtDq2Fydwvn+/cBvUX9S0cdKv6AISZcEbErKQ0TpLC/Cv04l1vKaqXOBO8+VQ==", "dependencies": [ "@jridgewell/trace-mapping", @@ -325,47 +531,49 @@ "svelte" ] }, - "esbuild@0.25.3": { - "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", - "dependencies": [ - "@esbuild/aix-ppc64", - "@esbuild/android-arm", - "@esbuild/android-arm64", - "@esbuild/android-x64", - "@esbuild/darwin-arm64", - "@esbuild/darwin-x64", - "@esbuild/freebsd-arm64", - "@esbuild/freebsd-x64", - "@esbuild/linux-arm", - "@esbuild/linux-arm64", - "@esbuild/linux-ia32", - "@esbuild/linux-loong64", - "@esbuild/linux-mips64el", - "@esbuild/linux-ppc64", - "@esbuild/linux-riscv64", - "@esbuild/linux-s390x", - "@esbuild/linux-x64", - "@esbuild/netbsd-arm64", - "@esbuild/netbsd-x64", - "@esbuild/openbsd-arm64", - "@esbuild/openbsd-x64", - "@esbuild/sunos-x64", - "@esbuild/win32-arm64", - "@esbuild/win32-ia32", - "@esbuild/win32-x64" - ] + "esbuild@0.25.5": { + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "optionalDependencies": [ + "@esbuild/aix-ppc64@0.25.5", + "@esbuild/android-arm@0.25.5", + "@esbuild/android-arm64@0.25.5", + "@esbuild/android-x64@0.25.5", + "@esbuild/darwin-arm64@0.25.5", + "@esbuild/darwin-x64@0.25.5", + "@esbuild/freebsd-arm64@0.25.5", + "@esbuild/freebsd-x64@0.25.5", + "@esbuild/linux-arm@0.25.5", + "@esbuild/linux-arm64@0.25.5", + "@esbuild/linux-ia32@0.25.5", + "@esbuild/linux-loong64@0.25.5", + "@esbuild/linux-mips64el@0.25.5", + "@esbuild/linux-ppc64@0.25.5", + "@esbuild/linux-riscv64@0.25.5", + "@esbuild/linux-s390x@0.25.5", + "@esbuild/linux-x64@0.25.5", + "@esbuild/netbsd-arm64@0.25.5", + "@esbuild/netbsd-x64@0.25.5", + "@esbuild/openbsd-arm64@0.25.5", + "@esbuild/openbsd-x64@0.25.5", + "@esbuild/sunos-x64@0.25.5", + "@esbuild/win32-arm64@0.25.5", + "@esbuild/win32-ia32@0.25.5", + "@esbuild/win32-x64@0.25.5" + ], + "scripts": true, + "bin": true }, "esm-env@1.2.2": { "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==" }, - "esrap@1.4.6": { - "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", + "esrap@1.4.9": { + "integrity": "sha512-3OMlcd0a03UGuZpPeUC1HxR3nA23l+HEyCiZw3b3FumJIN9KphoGzDJKMXI1S72jVS1dsenDyQC0kJlO1U9E1g==", "dependencies": [ "@jridgewell/sourcemap-codec" ] }, - "fdir@6.4.4": { - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==" + "fdir@6.4.5": { + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==" }, "fill-range@7.1.1": { "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", @@ -380,8 +588,8 @@ "whatwg-mimetype" ] }, - "immutable@5.1.1": { - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==" + "immutable@5.1.2": { + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==" }, "is-extglob@2.1.1": { "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" @@ -405,7 +613,8 @@ "integrity": "sha512-zd4dqFiXSYyant2WgSXAZ9+yYqilNVvragVNkNRn2IFZKgjyULNrKRznqN4Zon0MkLueCg+3QaPVCnDAVP20OQ==", "dependencies": [ "@dmsnell/diff-match-patch" - ] + ], + "bin": true }, "locate-character@3.0.0": { "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" @@ -444,23 +653,27 @@ "mri" ] }, - "sass@1.87.0": { - "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "sass@1.89.1": { + "integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==", "dependencies": [ - "@parcel/watcher", "chokidar", "immutable", "source-map-js" - ] + ], + "optionalDependencies": [ + "@parcel/watcher" + ], + "bin": true }, "source-map-js@1.2.1": { "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" }, - "sv@0.8.3": { - "integrity": "sha512-y/RIbFUowsykbShu8rJnxILieNtV1yduSN6dPhmCqNa+oMDSroRtjXBWyZe8MoZetAPN+m1tpvW1aA/CJNmIMw==" + "sv@0.8.8": { + "integrity": "sha512-AGoTjXdCi8ihjFjBa2mysY5ZpntfAczQigqDCScAtXx814vPuKElemf2pmhwA7WiUc8zYcAoVp+wInSgAwFWAQ==", + "bin": true }, - "svelte-check@4.1.6_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3": { - "integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==", + "svelte-check@4.2.1_svelte@5.33.18__acorn@8.15.0_typescript@5.8.3": { + "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==", "dependencies": [ "@jridgewell/trace-mapping", "chokidar", @@ -469,18 +682,24 @@ "sade", "svelte", "typescript" - ] + ], + "bin": true }, - "svelte-preprocess@6.0.3_sass@1.87.0_svelte@5.28.2__acorn@8.14.1_typescript@5.8.3": { + "svelte-preprocess@6.0.3_sass@1.89.1_svelte@5.33.18__acorn@8.15.0_typescript@5.8.3": { "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", "dependencies": [ "sass", "svelte", "typescript" - ] + ], + "optionalPeers": [ + "sass", + "typescript" + ], + "scripts": true }, - "svelte@5.28.2_acorn@8.14.1": { - "integrity": "sha512-FbWBxgWOpQfhKvoGJv/TFwzqb4EhJbwCD17dB0tEpQiw1XyUEKZJtgm4nA4xq3LLsMo7hu5UY/BOFmroAxKTMg==", + "svelte@5.33.18_acorn@8.15.0": { + "integrity": "sha512-GVAhi8vi8pGne/wlEdnfWIJvSR9eKvEknxjfL5Sr8gQALiyk8Ey+H0lhUYLpjW+MrqgH9h4dgh2NF6/BTFprRg==", "dependencies": [ "@ampproject/remapping", "@jridgewell/sourcemap-codec", @@ -505,7 +724,8 @@ ] }, "typescript@5.8.3": { - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==" + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "bin": true }, "webidl-conversions@7.0.0": { "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" @@ -536,13 +756,13 @@ "packageJson": { "dependencies": [ "npm:@noble/hashes@1.8.0", - "npm:@types/chrome@0.0.317", - "npm:@types/deno@2.2.0", + "npm:@types/chrome@0.0.326", + "npm:@types/deno@2.3.0", "npm:jsondiffpatch@0.7.3", - "npm:sass@1.87.0", - "npm:sv@0.8.3", - "npm:svelte-check@4.1.6", - "npm:svelte@5.28.2", + "npm:sass@1.89.1", + "npm:sv@0.8.8", + "npm:svelte-check@4.2.1", + "npm:svelte@5.33.18", "npm:typescript@5.8.3" ] } diff --git a/doc/issues.log.md b/doc/issues.log.md new file mode 100644 index 0000000..bc53fd8 --- /dev/null +++ b/doc/issues.log.md @@ -0,0 +1,26 @@ +> [!NOTE] +> The content provided is for educational and informational purpose only. Websites, dates, names are not mentioned on purpose. + +### Issues, that could have been spotted during the development + +- A ~10ms delay interval, from an old third-party library, constantly consuming approximately 10% of CPU solely to check if the window was resized. + +- A bundled dependency library that utilizes the `eval` function, thereby preventing the removal of `unsafe-eval` from the `Content-Security-Policy` header. + + - Code that uses `eval` with modern syntax to check if it's supported by browser (not throws exception). + + - Dependency package that was bundled with webpack config's [devtool: 'eval'](https://webpack.js.org/configuration/devtool/) in production mode. + +- A substantial number of hidden video elements in DOM stopped working, after Chrome unexpectedly limited them to 100 per domain (later the limit was lifted to 1000). + +- Redundant duplicate `video` element, hidden under the actual video on a landing-page. + +- `setTimeout` used to animate instead of `requestAnimationFrame`. + +- `setTimeout` with dynamically computed delay value ends to be called with `NaN`. + +- Hidden UI feature runs its logic in the background. + + - Indirectly, discovered from the bursts of short timeouts, fired in loop, from ResizeObserver of an invisible feature that appears to be for a power user only (or partially deprecated). + + - Animation still runs (plus network requests) in the background after a "paywall" fullscreen popup. Despite claiming "it's to conserve data bandwidth". CPU usage doesn't drop to 0%. diff --git a/manifest.json b/manifest.json index 0d15a73..b8ee8bd 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,6 @@ { - "version": "1.2.0", + "version": "1.3.0", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxQCaHgX3DkPnGmHr+rhWyPvYemxMhBbvulmj4RvEpAnGVprdPCUiHSY0jOcDn3vnU6zm8mR1mT3sdlYoUGikBIT19/Jf1iGlc2dySt2bmDQXlTrqllT/XB8HW/wruFej9waMw9yqtW1wOJtElxWnT11pzXkKeflH1Sh+//Jnplr577vOmWh9TU8JLJHS9WklPHJyXCCMGrg/0Sxqte5qWryE2yIm9375KGkKN4ZKjSIxaCg0qodhf5Ug9s2QD7/s5xt548gbEUm9LqQHkNoIH3KXuYOnLksJFxi7FDwhg+oXalsONr5eEvPjkwxYpMKJXfRSg8sB8N6cXLUfgLAKUwIDAQAB", "name": "API Monitor", "manifest_version": 3, "description": "Show active intervals, scheduled timeouts, animation frames, idle callbacks, eval invocations, media events and properties", @@ -13,6 +14,7 @@ "64": "public/img/panel-icon64.png", "128": "public/img/panel-icon128.png" }, + "incognito": "split", "content_scripts": [ { "world": "MAIN", diff --git a/package.json b/package.json index 0297f21..95e5a55 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "type": "module", "devDependencies": { - "@types/chrome": "0.0.317", - "@types/deno": "2.2.0", - "sass": "1.87.0", - "sv": "0.8.3", - "svelte": "5.28.2", - "svelte-check": "4.1.6", + "@types/chrome": "0.0.326", + "@types/deno": "2.3.0", + "sass": "1.89.1", + "sv": "0.8.8", + "svelte": "5.33.18", + "svelte-check": "4.2.1", "typescript": "5.8.3" }, "dependencies": { diff --git a/public/mirror.html b/public/mirror.html new file mode 100644 index 0000000..c9993dd --- /dev/null +++ b/public/mirror.html @@ -0,0 +1,20 @@ + + + + + + + Mirror - Browser API Monitor + + + + + + + + + + + + + diff --git a/src/api-monitor-cs-isolated.ts b/src/api-monitor-cs-isolated.ts index ef18b12..ccc10b2 100644 --- a/src/api-monitor-cs-isolated.ts +++ b/src/api-monitor-cs-isolated.ts @@ -5,11 +5,14 @@ import { windowListen, windowPost, } from './api/communication.ts'; -import { loadLocalStorage, onLocalStorageChange } from './api/storage.local.ts'; +import { + loadLocalStorage, + onLocalStorageChange, +} from './api/storage/storage.local.ts'; import { loadSessionStorage, onSessionStorageChange, -} from './api/storage.session.ts'; +} from './api/storage/storage.session.ts'; Promise.all([loadLocalStorage(), loadSessionStorage()]).then( ([config, session]) => { @@ -23,11 +26,11 @@ Promise.all([loadLocalStorage(), loadSessionStorage()]).then( portListen(windowPost); windowListen(runtimePost); - onLocalStorageChange((newValue) => { - windowPost({ msg: EMsg.CONFIG, config: newValue }); + onLocalStorageChange((config) => { + windowPost({ msg: EMsg.CONFIG, config }); }); - onSessionStorageChange((newValue) => { - windowPost({ msg: EMsg.SESSION, session: newValue }); + onSessionStorageChange((session) => { + windowPost({ msg: EMsg.SESSION, session }); }); runtimePost({ msg: EMsg.CONTENT_SCRIPT_LOADED }); diff --git a/src/api-monitor-cs-main.ts b/src/api-monitor-cs-main.ts index e9079ed..afd0323 100644 --- a/src/api-monitor-cs-main.ts +++ b/src/api-monitor-cs-main.ts @@ -47,31 +47,29 @@ const tick = new Timer( ); windowListen((o) => { - if (o.msg === EMsg.TELEMETRY_ACKNOWLEDGED) { + if (EMsg.TELEMETRY_ACKNOWLEDGED === o.msg) { tick.delay = adjustTelemetryDelay(o.timeOfCollection); originalMetrics = currentMetrics; eachSecond.isPending() && tick.start(); - } else if ( - o.msg === EMsg.CONFIG && o.config && typeof o.config === 'object' - ) { + } else if (EMsg.CONFIG === o.msg) { applyConfig(o.config); - } else if (o.msg === EMsg.START_OBSERVE) { + } else if (EMsg.START_OBSERVE === o.msg) { originalMetrics = currentMetrics = null; tick.trigger(); eachSecond.start(); - } else if (o.msg === EMsg.STOP_OBSERVE) { + } else if (EMsg.STOP_OBSERVE === o.msg) { tick.stop(); eachSecond.stop(); originalMetrics = currentMetrics = null; - } else if (o.msg === EMsg.RESET_WRAPPER_HISTORY) { + } else if (EMsg.RESET_WRAPPER_HISTORY === o.msg) { originalMetrics = currentMetrics = null; cleanHistory(); !tick.isPending() && tick.trigger(); - } else if (o.msg === EMsg.TIMER_COMMAND) { + } else if (EMsg.TIMER_COMMAND === o.msg) { runTimerCommand(o.type, o.handler); - } else if (o.msg === EMsg.MEDIA_COMMAND) { + } else if (EMsg.MEDIA_COMMAND === o.msg) { runMediaCommand(o.mediaId, o.cmd, o.property); - } else if (o.msg === EMsg.SESSION) { + } else if (EMsg.SESSION === o.msg) { applySession(o.session); } }); diff --git a/src/api-monitor-devtools-panel.ts b/src/api-monitor-devtools-panel.ts index d951c71..8eda8d0 100644 --- a/src/api-monitor-devtools-panel.ts +++ b/src/api-monitor-devtools-panel.ts @@ -2,8 +2,10 @@ import { mount } from 'svelte'; import App from './view/App.svelte'; import { initConfigState } from './state/config.state.svelte.ts'; import { onHidePanel } from './devtoolsPanelUtil.ts'; +import { establishTelemetryReceiver } from './state/telemetry.state.svelte.ts'; initConfigState().then(() => { mount(App, { target: document.body }); + establishTelemetryReceiver(); globalThis.addEventListener('beforeunload', onHidePanel); }); diff --git a/src/api-monitor-devtools.ts b/src/api-monitor-devtools.ts index bc94884..a86f8ff 100644 --- a/src/api-monitor-devtools.ts +++ b/src/api-monitor-devtools.ts @@ -1,6 +1,9 @@ import { EMsg, portPost } from './api/communication.ts'; -import { loadLocalStorage, saveLocalStorage } from './api/storage.local.ts'; -import { enableSessionInContentScript } from './api/storage.session.ts'; +import { + loadLocalStorage, + saveLocalStorage, +} from './api/storage/storage.local.ts'; +import { enableSessionInContentScript } from './api/storage/storage.session.ts'; import { onHidePanel } from './devtoolsPanelUtil.ts'; // tabId may be null if user opened the devtools of the devtools diff --git a/src/api/communication.ts b/src/api/communication.ts index 6dcc477..f0cea7b 100644 --- a/src/api/communication.ts +++ b/src/api/communication.ts @@ -14,21 +14,26 @@ import { APPLICATION_NAME } from './env.ts'; import { ERRORS_IGNORED } from './const.ts'; import { ETimerType } from '../wrapper/TimerWrapper.ts'; import type { TTelemetry } from '../wrapper/Wrapper.ts'; -import type { TConfig } from './storage.local.ts'; +import type { TConfig } from './storage/storage.local.ts'; import type { TMediaCommand } from '../wrapper/MediaWrapper.ts'; import type { Delta } from 'jsondiffpatch'; -import type { TSession } from './storage.session.ts'; +import type { TSession } from './storage/storage.session.ts'; let port: chrome.runtime.Port | null = null; export function portPost(payload: TMsgOptions) { + if (!chrome.runtime) { + windowPost(payload); + return; + } + if (!port) { port = chrome.tabs.connect(chrome.devtools.inspectedWindow.tabId, { name: APPLICATION_NAME, }); - port.onDisconnect.addListener(() => void (port = null)); + port?.onDisconnect.addListener(() => void (port = null)); } - port.postMessage(payload); + port?.postMessage(payload); } export function portListen(callback: (payload: TMsgOptions) => void) { @@ -67,16 +72,20 @@ export function runtimePost(payload: TMsgOptions) { } export function runtimeListen(callback: (payload: TMsgOptions) => void) { - chrome.runtime.onMessage.addListener( - (payload, sender: chrome.runtime.MessageSender, sendResponse) => { - if ( - sender.tab?.id === chrome.devtools.inspectedWindow.tabId - ) { - callback(payload); - sendResponse(); - } - }, - ); + if (chrome?.runtime) { + chrome.runtime.onMessage.addListener( + (payload, sender: chrome.runtime.MessageSender, sendResponse) => { + if ( + sender.tab?.id === chrome.devtools.inspectedWindow.tabId + ) { + callback(payload); + sendResponse(); + } + }, + ); + } else { + windowListen(callback); + } } function handleRuntimeMessageResponse(): void { diff --git a/src/api/const.ts b/src/api/const.ts index 19e95d9..e453a7f 100644 --- a/src/api/const.ts +++ b/src/api/const.ts @@ -6,7 +6,8 @@ export const ERRORS_IGNORED = [ ]; export const TELEMETRY_FREQUENCY_30PS = 33.3333333333; // ms export const TELEMETRY_FREQUENCY_1PS = 1000; // ms -export const FRAME_1of60 = 0.0166666666667; // ms +export const TIME_60FPS_SEC = 0.0166666666667; // s +export const TIME_60FPS_MS = 16.666666666666668; export const VARIABLE_ANIMATION_THROTTLE = 3500; // eye blinking average frequency export const SELF_TIME_MAX_GOOD = 13.333333333333332; // ms diff --git a/src/api/storage.local.ts b/src/api/storage/storage.local.ts similarity index 85% rename from src/api/storage.local.ts rename to src/api/storage/storage.local.ts index 35f49ab..66058cf 100644 --- a/src/api/storage.local.ts +++ b/src/api/storage/storage.local.ts @@ -1,15 +1,16 @@ import type { TCancelIdleCallbackHistory, TRequestIdleCallbackHistory, -} from '../wrapper/IdleWrapper.ts'; +} from '../../wrapper/IdleWrapper.ts'; import type { TCancelAnimationFrameHistory, TRequestAnimationFrameHistory, -} from '../wrapper/AnimationWrapper.ts'; +} from '../../wrapper/AnimationWrapper.ts'; import type { TClearTimerHistory, TSetTimerHistory, -} from '../wrapper/TimerWrapper.ts'; +} from '../../wrapper/TimerWrapper.ts'; +import { CONFIG_VERSION, local } from './storage.ts'; type TPanelKey = | 'callsSummary' @@ -24,19 +25,20 @@ type TPanelKey = | 'cancelAnimationFrame' | 'requestIdleCallback' | 'cancelIdleCallback'; -export type TPanelMap = { - [K in TPanelKey]: TPanel; -}; + export type TPanel = { key: TPanelKey; label: string; visible: boolean; wrap: boolean | null; }; +export type TPanelMap = { + [K in TPanelKey]: TPanel; +}; + export type TConfig = typeof DEFAULT_CONFIG; export type TConfigField = Partial; -const CONFIG_VERSION = '2025-04-25'; export const DEFAULT_PANELS: TPanel[] = [ { key: 'callsSummary', label: 'Calls Summary', visible: false, wrap: null }, { key: 'media', label: 'Media', visible: true, wrap: null }, @@ -143,32 +145,36 @@ export function panelsArray2Map(panels: TPanel[]) { } export async function loadLocalStorage(): Promise { - let store = await chrome.storage.local.get([CONFIG_VERSION]); + let store = await local.get([CONFIG_VERSION]); const isEmpty = !Object.keys(store).length; if (isEmpty) { - await chrome.storage.local.clear(); // reset previous version - await chrome.storage.local.set({ [CONFIG_VERSION]: DEFAULT_CONFIG }); - store = await chrome.storage.local.get([CONFIG_VERSION]); + await local.clear(); // reset previous version + await local.set({ [CONFIG_VERSION]: DEFAULT_CONFIG }); + store = await local.get([CONFIG_VERSION]); } return store[CONFIG_VERSION]; } export async function saveLocalStorage(value: TConfigField) { - const store = await chrome.storage.local.get([CONFIG_VERSION]); + const store = await local.get([CONFIG_VERSION]); Object.assign(store[CONFIG_VERSION], value); - return await chrome.storage.local.set(store); + return await local.set(store); } export function onLocalStorageChange( callback: (newValue: TConfig, oldValue: TConfig) => void, ) { - chrome.storage.local.onChanged.addListener((change) => { + local.onChanged.addListener((change: { + [key: string]: chrome.storage.StorageChange; + }) => { if ( - change && change[CONFIG_VERSION] && change[CONFIG_VERSION].newValue + change && + change[CONFIG_VERSION] && + change[CONFIG_VERSION].newValue ) { callback( change[CONFIG_VERSION].newValue, diff --git a/src/api/storage.session.ts b/src/api/storage/storage.session.ts similarity index 62% rename from src/api/storage.session.ts rename to src/api/storage/storage.session.ts index 8a74cce..0fe618f 100644 --- a/src/api/storage.session.ts +++ b/src/api/storage/storage.session.ts @@ -1,4 +1,4 @@ -const SESSION_VERSION = '2025-04-25'; +import { session, SESSION_VERSION } from './storage.ts'; export type TSession = typeof DEFAULT_SESSION; type TSessionProperty = Partial; @@ -8,36 +8,36 @@ const DEFAULT_SESSION = { }; export function enableSessionInContentScript() { - return chrome.storage.session.setAccessLevel({ + return session.setAccessLevel({ accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS', }); } export async function loadSessionStorage(): Promise { - let store = await chrome.storage.session.get([SESSION_VERSION]); + let store = await session.get([SESSION_VERSION]); const isEmpty = !Object.keys(store).length; if (isEmpty) { - await chrome.storage.session.clear(); // reset previous version - await chrome.storage.session.set({ [SESSION_VERSION]: DEFAULT_SESSION }); - store = await chrome.storage.session.get([SESSION_VERSION]); + await session.clear(); // reset previous version + await session.set({ [SESSION_VERSION]: DEFAULT_SESSION }); + store = await session.get([SESSION_VERSION]); } return store[SESSION_VERSION]; } export async function saveSessionStorage(value: TSessionProperty) { - const store = await chrome.storage.session.get([SESSION_VERSION]); + const store = await session.get([SESSION_VERSION]); Object.assign(store[SESSION_VERSION], value); - return await chrome.storage.session.set(store); + return await session.set(store); } export function onSessionStorageChange( callback: (newValue: TSession, oldValue: TSession) => void, ) { - chrome.storage.session.onChanged.addListener((change) => { + session.onChanged.addListener((change) => { if ( change && change[SESSION_VERSION] && change[SESSION_VERSION].newValue ) { diff --git a/src/api/storage/storage.ts b/src/api/storage/storage.ts new file mode 100644 index 0000000..c5c926e --- /dev/null +++ b/src/api/storage/storage.ts @@ -0,0 +1,88 @@ +export const CONFIG_VERSION = '2025-04-25'; +export const SESSION_VERSION = '2025-04-25'; + +export const local = /*@__PURE__*/ (() => { + return globalThis.chrome?.storage + ? chrome.storage.local + : mockChromeStorageWith(globalThis.localStorage, CONFIG_VERSION); +})(); + +export const session = /*@__PURE__*/ (() => { + return globalThis.chrome?.storage + ? chrome.storage.session + : mockChromeStorageWith(globalThis.sessionStorage, SESSION_VERSION); +})(); + +type TOnChangeSignature = (changes: { + [key: string]: chrome.storage.StorageChange; +}) => void; + +const LOCAL_KEY = 'mock'; + +/** + * @NOTE: minimalistic coverage, just to accommodate project's basic needs + */ +function mockChromeStorageWith( + storage: Storage, + APP_KEY: string, +): chrome.storage.LocalStorageArea { + const allListeners = new Set(); + + return { + QUOTA_BYTES: 10485760, + + clear() { + return new Promise((resolve) => { + storage.clear(); + resolve(); + }); + }, + + getBytesInUse() { + return new Promise((resolve) => { + resolve( + (storage.getItem(LOCAL_KEY) || '').length, + ); + }); + }, + + async setAccessLevel() { + // NOOP in window runtime + }, + + // @ts-expect-error partial implementation + onChanged: { + addListener(callback: TOnChangeSignature) { + allListeners.add(callback); + }, + + removeListener(callback: TOnChangeSignature) { + allListeners.delete(callback); + }, + }, + + async set(o: object) { + storage.setItem(LOCAL_KEY, JSON.stringify(o)); + + // dispatch `change` events + const oo = await this.get(LOCAL_KEY); + for (const listener of allListeners) { + listener({ + [APP_KEY]: { + newValue: oo[APP_KEY], + }, + }); + } + }, + + get() { + return new Promise((resolve, reject) => { + try { + resolve(JSON.parse(storage.getItem(LOCAL_KEY) || '{}') || {}); + } catch (e) { + reject(e); + } + }); + }, + }; +} diff --git a/src/api/time.ts b/src/api/time.ts index 2b56e4e..0ed0b88 100644 --- a/src/api/time.ts +++ b/src/api/time.ts @@ -4,9 +4,10 @@ import { requestAnimationFrame, setTimeout, TELEMETRY_FREQUENCY_30PS, + TIME_60FPS_MS, } from './const.ts'; -export function callingOnce< +export function callableOnce< T extends (...args: Parameters) => ReturnType, >( fn: T | null, @@ -52,34 +53,30 @@ export class Stopper { return this.#finish - this.#start; } + toString() { + return Stopper.toString(this.value()); + } + static toString(msTime: number | unknown) { if (typeof msTime !== 'number' || !Number.isFinite(msTime)) { return; } - if (msTime < 1) { - return `${Math.trunc(msTime * 1e3)}μs`; - } else if (msTime < 3) { + if (msTime <= TIME_60FPS_MS) { const ms = Math.trunc(msTime); - return `${ms}.${Math.trunc((msTime - ms) * 1e2)}ms`; + return `${ms}.${ + toPaddedString(Math.trunc((msTime - ms) * 1e2), 2) + }\u00a0ms`; } else if (msTime < 1e3) { - return `${Math.trunc(msTime)}ms`; + return `${Math.trunc(msTime)}\u00a0ms`; } else if (msTime < 60e3) { const s = Math.trunc(msTime / 1e3) % 60; const ms = Math.trunc(msTime % 1e3); - return `${s}.${ms.toString().padStart(3, '0')}s`; + return `${s}.${toPaddedString(ms, 3)}\u00a0s`; } - const h = Math.trunc(msTime / 3600e3); - const m = Math.trunc(msTime / 60e3) % 60; - const s = Math.trunc(msTime / 1e3) % 60; - - return `${h.toString().padStart(2, '0')}:${ - m - .toString() - .padStart(2, '0') - }:${s.toString().padStart(2, '0')}`; + return ms2HMS(msTime); } } @@ -91,38 +88,38 @@ interface ITimerOptions { /** act as requestAnimationFrame called from another requestAnimationFrame (default: false); if true - `delay` is redundant */ animation?: boolean; - /** populate `executionTime` with measured execution time of `callback` (default: false) */ + /** populate `callbackSelfTime` with measured execution time of `callback` (default: false) */ measurable?: boolean; } /** * A unification of ways to delay a callback to another time in javascript event-loop * - `repetitive: false` - will call `setTimeout` with constant `delay`. - * - `repetitive: true` - will call `setTimeout` but act as `setInterval` with changable `delay`. + * - `repetitive: true` - will call `setTimeout` but act as `setInterval` with changeable `delay`. * - `animation: true` - will call `requestAnimationFrame` in recursive way (means to follow the browser's frame-rate). * - `measurable: true` - measure the callback's execution time. */ export class Timer { - readonly options: ITimerOptions; - readonly #defaultOptions: ITimerOptions = { + delay: number = 0; + /** callback's self-time in milliseconds */ + callbackSelfTime: number = -1; + #handler: number = 0; + readonly #fn: (...args: unknown[]) => void; + readonly #stopper?: Stopper; + readonly #options: ITimerOptions; + static readonly DEFAULT_OPTIONS: ITimerOptions = { delay: 0, repetitive: false, animation: false, measurable: false, }; - delay: number = 0; - /** callback's self-time in milliseconds */ - executionTime: number = -1; - #fn: (...args: unknown[]) => void; - #handler: number = 0; - readonly #stopper?: Stopper; constructor(o: ITimerOptions, fn: (...args: unknown[]) => void) { - this.options = Object.assign(this.#defaultOptions, o); + this.#options = Object.assign({}, Timer.DEFAULT_OPTIONS, o); this.#fn = fn; - this.delay = this.options.delay || 0; + this.delay = this.#options.delay || 0; - if (this.options.measurable) { + if (this.#options.measurable) { this.#stopper = new Stopper(); } } @@ -132,12 +129,12 @@ export class Timer { this.stop(); } - if (this.options.animation) { + if (this.#options.animation) { this.#handler = requestAnimationFrame(() => { this.trigger(...args); this.#handler = 0; - if (this.options.repetitive) { + if (this.#options.repetitive) { this.start(...args); } }); @@ -146,7 +143,7 @@ export class Timer { this.trigger(...args); this.#handler = 0; - if (this.options.repetitive) { + if (this.#options.repetitive) { this.start(...args); } }, this.delay); @@ -159,7 +156,7 @@ export class Timer { this.#stopper?.start(); this.#fn(...args); if (this.#stopper) { - this.executionTime = this.#stopper.stop().value(); + this.callbackSelfTime = this.#stopper.stop().value(); } return this; @@ -167,7 +164,7 @@ export class Timer { stop() { if (this.#handler) { - if (this.options.animation) { + if (this.#options.animation) { cancelAnimationFrame(this.#handler); } else { clearTimeout(this.#handler); @@ -221,12 +218,24 @@ export class Fps { } } -export function trim2microsecond(ms: T) { - return typeof ms === 'number' ? Math.trunc(ms * 1e3) / 1e3 : ms; +export function wait(timeout: number) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +export function trim2ms(ms: T) { + return typeof ms === 'number' ? Math.trunc(ms * 1e2) / 1e2 : ms; +} + +export function ms2HMS(ms: number) { + return `${toPaddedString(Math.trunc(ms / 3600e3), 2)}:${ + toPaddedString(Math.trunc(ms / 60e3) % 60, 2) + }:${toPaddedString(Math.trunc(ms / 1e3) % 60, 2)}`; } -export function msToHms(delay: number | unknown): string | undefined { - return delay && Number(delay) > 10e3 ? Stopper.toString(delay) : undefined; +function toPaddedString(value: number, padding: number) { + return value.toString().padStart(padding, '0'); } const TICK_TIME_LAG_SCALAR = 3; diff --git a/src/devtoolsPanelUtil.ts b/src/devtoolsPanelUtil.ts index 3e23cb9..2912d1c 100644 --- a/src/devtoolsPanelUtil.ts +++ b/src/devtoolsPanelUtil.ts @@ -4,10 +4,36 @@ * as a dependency */ import { EMsg, portPost } from './api/communication.ts'; -import { saveLocalStorage } from './api/storage.local.ts'; +import { saveLocalStorage } from './api/storage/storage.local.ts'; +import { ms2HMS } from './api/time.ts'; export async function onHidePanel() { chrome.power.releaseKeepAwake(); portPost({ msg: EMsg.STOP_OBSERVE }); await saveLocalStorage({ devtoolsPanelShown: false }); } + +type TColourScheme = 'light' | 'dark'; + +export function onColourSchemeChange( + callback: (scheme: TColourScheme) => void, +) { + const devtoolsScheme = chrome?.devtools?.panels.themeName; + const osDarkScheme = globalThis.matchMedia('(prefers-color-scheme: dark)'); + + if (devtoolsScheme === 'dark' || osDarkScheme.matches) { + callback('dark'); + } else { + callback('light'); + } + + osDarkScheme.addEventListener('change', (e: MediaQueryListEvent) => { + callback(e.matches ? 'dark' : 'light'); + }); +} + +export function delayTooltip(delay: number | unknown) { + if (typeof delay == 'number' && Number.isFinite(delay) && delay > 1e4) { + return ms2HMS(delay); + } +} diff --git a/src/global.d.ts b/src/global.d.ts index b76eac5..268851e 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,5 +1,5 @@ -/// export {}; + declare global { let __development__: boolean; let __app_name__: string; diff --git a/src/mirror/mirror.ts b/src/mirror/mirror.ts new file mode 100644 index 0000000..7480789 --- /dev/null +++ b/src/mirror/mirror.ts @@ -0,0 +1,36 @@ +import { mount } from 'svelte'; +import App from '../view/App.svelte'; +import { initConfigState } from '../state/config.state.svelte.ts'; +import { establishTelemetryReceiverMirror } from '../state/telemetry.state.svelte.ts'; +import { EMsg, windowPost } from '../api/communication.ts'; +import { + loadLocalStorage, + onLocalStorageChange, +} from '../api/storage/storage.local.ts'; +import { + loadSessionStorage, + onSessionStorageChange, +} from '../api/storage/storage.session.ts'; + +initConfigState().then(() => { + mount(App, { target: document.body }); + establishTelemetryReceiverMirror(); + + Promise.all([loadLocalStorage(), loadSessionStorage()]).then( + ([config, session]) => { + windowPost({ msg: EMsg.CONFIG, config }); + windowPost({ msg: EMsg.SESSION, session }); + + if (!config.paused) { + windowPost({ msg: EMsg.START_OBSERVE }); + } + + onLocalStorageChange((config) => { + windowPost({ msg: EMsg.CONFIG, config }); + }); + onSessionStorageChange((session) => { + windowPost({ msg: EMsg.SESSION, session }); + }); + }, + ); +}); diff --git a/src/state/config.state.svelte.ts b/src/state/config.state.svelte.ts index a9e5ec8..44c8eba 100644 --- a/src/state/config.state.svelte.ts +++ b/src/state/config.state.svelte.ts @@ -5,7 +5,7 @@ import { loadLocalStorage, saveLocalStorage, type TConfig, -} from '../api/storage.local.ts'; +} from '../api/storage/storage.local.ts'; let config: TConfig = $state(DEFAULT_CONFIG); @@ -33,9 +33,9 @@ export async function toggleKeepAwake() { await saveLocalStorage({ keepAwake: $state.snapshot(config.keepAwake) }); if (config.keepAwake) { - chrome.power.requestKeepAwake('display'); + chrome.power?.requestKeepAwake('display'); } else { - chrome.power.releaseKeepAwake(); + chrome.power?.releaseKeepAwake(); } } diff --git a/src/state/session.state.svelte.ts b/src/state/session.state.svelte.ts index fc2c186..38fee95 100644 --- a/src/state/session.state.svelte.ts +++ b/src/state/session.state.svelte.ts @@ -1,8 +1,9 @@ import { loadSessionStorage, saveSessionStorage, -} from '../api/storage.session.ts'; +} from '../api/storage/storage.session.ts'; import { SvelteSet } from 'svelte/reactivity'; +import { session } from '../api/storage/storage.ts'; export const sessionState = $state({ bypass: > new SvelteSet(), @@ -35,7 +36,7 @@ export async function toggleDebug(traceId: string) { } } -const QUOTA_THRESHOLD = chrome.storage.session.QUOTA_BYTES; +const QUOTA_THRESHOLD = session.QUOTA_BYTES; const MARGINAL_SIZE = 40; // for ASCII string in an array async function toggleSet(set: Set, traceId: string): Promise { if (set.has(traceId)) { @@ -44,7 +45,7 @@ async function toggleSet(set: Set, traceId: string): Promise { } const freeSpace = QUOTA_THRESHOLD - - await chrome.storage.session.getBytesInUse(); + await session.getBytesInUse(); if (freeSpace - traceId.length - MARGINAL_SIZE >= 0) { set.add(traceId); diff --git a/src/state/telemetry.state.svelte.ts b/src/state/telemetry.state.svelte.ts index c9da58a..3b7d81f 100644 --- a/src/state/telemetry.state.svelte.ts +++ b/src/state/telemetry.state.svelte.ts @@ -1,5 +1,11 @@ import type { TTelemetry } from '../wrapper/Wrapper.ts'; -import { EMsg, portPost, runtimeListen } from '../api/communication.ts'; +import { + EMsg, + portPost, + runtimeListen, + windowListen, + windowPost, +} from '../api/communication.ts'; import diff from '../api/diff.ts'; import { type Writable, writable } from 'svelte/store'; @@ -14,17 +20,19 @@ export function useTelemetryState() { return state; } -runtimeListen((o) => { - if (o.msg === EMsg.TELEMETRY) { - telemetryProgressive = structuredClone(o.telemetry); - state.telemetry = o.telemetry; - acknowledgeTelemetry(o.timeOfCollection); - } else if (o.msg === EMsg.TELEMETRY_DELTA) { - diff.patch(telemetryProgressive, o.telemetryDelta); - state.telemetry = structuredClone(telemetryProgressive); - acknowledgeTelemetry(o.timeOfCollection); - } -}); +export function establishTelemetryReceiver() { + runtimeListen((o) => { + if (o.msg === EMsg.TELEMETRY) { + telemetryProgressive = structuredClone(o.telemetry); + state.telemetry = o.telemetry; + acknowledgeTelemetry(o.timeOfCollection); + } else if (o.msg === EMsg.TELEMETRY_DELTA) { + diff.patch(telemetryProgressive, o.telemetryDelta); + state.telemetry = structuredClone(telemetryProgressive); + acknowledgeTelemetry(o.timeOfCollection); + } + }); +} function acknowledgeTelemetry(timeOfCollection: number) { portPost({ @@ -34,3 +42,26 @@ function acknowledgeTelemetry(timeOfCollection: number) { state.timeOfCollection.set(timeOfCollection); } + +export function establishTelemetryReceiverMirror() { + windowListen((o) => { + if (o.msg === EMsg.TELEMETRY) { + telemetryProgressive = structuredClone(o.telemetry); + state.telemetry = o.telemetry; + acknowledgeTelemetryMirror(o.timeOfCollection); + } else if (o.msg === EMsg.TELEMETRY_DELTA) { + diff.patch(telemetryProgressive, o.telemetryDelta); + state.telemetry = structuredClone(telemetryProgressive); + acknowledgeTelemetryMirror(o.timeOfCollection); + } + }); +} + +function acknowledgeTelemetryMirror(timeOfCollection: number) { + windowPost({ + msg: EMsg.TELEMETRY_ACKNOWLEDGED, + timeOfCollection, + }); + + state.timeOfCollection.set(timeOfCollection); +} diff --git a/src/view/App.svelte b/src/view/App.svelte index 44d80e8..3418dc5 100644 --- a/src/view/App.svelte +++ b/src/view/App.svelte @@ -1,36 +1,14 @@
- -
- {#if __development__} - -
- {/if} - -
- -
- -
- -
- -
+
-
- +
diff --git a/src/view/menu/DevReload.svelte b/src/view/menu/DevReload.svelte index 09512bd..193496d 100644 --- a/src/view/menu/DevReload.svelte +++ b/src/view/menu/DevReload.svelte @@ -1,8 +1,10 @@ + + +
+{#if __development__} + +
+{/if} + +
+ +
+ +
+ +
+ +
diff --git a/src/view/menu/SummaryBar.svelte b/src/view/menu/SummaryBar.svelte index 93cae4b..2a5c6c5 100644 --- a/src/view/menu/SummaryBar.svelte +++ b/src/view/menu/SummaryBar.svelte @@ -1,5 +1,5 @@ @@ -27,9 +26,6 @@