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


+> [!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 @@