Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions patches/@tauri-apps__api@2.12.4.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
diff --git a/event.cjs b/event.cjs
index b53f3497d826d26e6182e28781b519e6e14f39c8..c926acf081427b7c79bcf170c0028a18fceda786 100644
--- a/event.cjs
+++ b/event.cjs
@@ -99,7 +99,13 @@ exports.TauriEvent = void 0;
* @returns
*/
async function _unlisten(event, eventId) {
- window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId);
+ try {
+ window.__TAURI_EVENT_PLUGIN_INTERNALS__?.unregisterListener(event, eventId);
+ }
+ catch {
+ // Native registration can complete before its JavaScript registry eval.
+ // The captured callback has already been released by _listen.
+ }
await core.invoke('plugin:event|unlisten', {
event,
eventId
@@ -134,17 +140,44 @@ async function _unlisten(event, eventId) {
* @since 1.0.0
*/
async function listen(event, handler, options) {
+ return _listen(event, handler, options, false);
+}
+async function _listen(event, handler, options, onlyOnce) {
var _a;
const target = typeof (options === null || options === void 0 ? void 0 : options.target) === 'string'
? { kind: 'AnyLabel', label: options.target }
: ((_a = options === null || options === void 0 ? void 0 : options.target) !== null && _a !== void 0 ? _a : { kind: 'Any' });
- return core.invoke('plugin:event|listen', {
- event,
- target,
- handler: core.transformCallback(handler)
- }).then((eventId) => {
- return async () => _unlisten(event, eventId);
+ let disposed = false;
+ const releaseCallback = () => {
+ window.__TAURI_INTERNALS__.unregisterCallback?.(handlerId);
+ };
+ const cleanup = async (eventId) => {
+ if (disposed)
+ return;
+ disposed = true;
+ releaseCallback();
+ await _unlisten(event, eventId);
+ };
+ const handlerId = core.transformCallback((eventData) => {
+ if (disposed)
+ return;
+ if (onlyOnce)
+ void cleanup(eventData.id).catch(() => { });
+ handler(eventData);
});
+ try {
+ const eventId = await core.invoke('plugin:event|listen', {
+ event,
+ target,
+ handler: handlerId
+ });
+ return async () => cleanup(eventId);
+ }
+ catch (error) {
+ disposed = true;
+ releaseCallback();
+ throw error;
+ }
}
/**
* Listens once to an emitted event to any {@link EventTarget|target}.
@@ -177,10 +210,7 @@ async function listen(event, handler, options) {
* @since 1.0.0
*/
async function once(event, handler, options) {
- return listen(event, (eventData) => {
- void _unlisten(event, eventData.id);
- handler(eventData);
- }, options);
+ return _listen(event, handler, options, true);
}
/**
* Emits an event to all {@link EventTarget|targets}.
diff --git a/event.js b/event.js
index 1e03752962d8cb5edad90d59732e486e187bee5b..3a3ebc94e4d5a58a95f1529a031615d162946f03 100644
--- a/event.js
+++ b/event.js
@@ -97,7 +97,13 @@ var TauriEvent;
* @returns
*/
async function _unlisten(event, eventId) {
- window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(event, eventId);
+ try {
+ window.__TAURI_EVENT_PLUGIN_INTERNALS__?.unregisterListener(event, eventId);
+ }
+ catch {
+ // Native registration can complete before its JavaScript registry eval.
+ // The captured callback has already been released by _listen.
+ }
await invoke('plugin:event|unlisten', {
event,
eventId
@@ -132,17 +138,44 @@ async function _unlisten(event, eventId) {
* @since 1.0.0
*/
async function listen(event, handler, options) {
+ return _listen(event, handler, options, false);
+}
+async function _listen(event, handler, options, onlyOnce) {
var _a;
const target = typeof (options === null || options === void 0 ? void 0 : options.target) === 'string'
? { kind: 'AnyLabel', label: options.target }
: ((_a = options === null || options === void 0 ? void 0 : options.target) !== null && _a !== void 0 ? _a : { kind: 'Any' });
- return invoke('plugin:event|listen', {
- event,
- target,
- handler: transformCallback(handler)
- }).then((eventId) => {
- return async () => _unlisten(event, eventId);
+ let disposed = false;
+ const releaseCallback = () => {
+ window.__TAURI_INTERNALS__.unregisterCallback?.(handlerId);
+ };
+ const cleanup = async (eventId) => {
+ if (disposed)
+ return;
+ disposed = true;
+ releaseCallback();
+ await _unlisten(event, eventId);
+ };
+ const handlerId = transformCallback((eventData) => {
+ if (disposed)
+ return;
+ if (onlyOnce)
+ void cleanup(eventData.id).catch(() => { });
+ handler(eventData);
});
+ try {
+ const eventId = await invoke('plugin:event|listen', {
+ event,
+ target,
+ handler: handlerId
+ });
+ return async () => cleanup(eventId);
+ }
+ catch (error) {
+ disposed = true;
+ releaseCallback();
+ throw error;
+ }
}
/**
* Listens once to an emitted event to any {@link EventTarget|target}.
@@ -175,10 +208,7 @@ async function listen(event, handler, options) {
* @since 1.0.0
*/
async function once(event, handler, options) {
- return listen(event, (eventData) => {
- void _unlisten(event, eventData.id);
- handler(eventData);
- }, options);
+ return _listen(event, handler, options, true);
}
/**
* Emits an event to all {@link EventTarget|targets}.
32 changes: 32 additions & 0 deletions patches/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# OverlayScrollbars 2.15.1

The patch selects OverlayScrollbars' existing scroll-event implementation and
percentage-based handle positioning in WebKit. Its native implementation
animates scrollbar handles with
`ScrollTimeline` and `translate*(calc(-100% + 100cq*))`. On macOS 27.2
(26B123), repeatedly updating those timelines retains native WebKit allocations
even when the DOM size and JavaScript live heap stay small.

A system WKWebView reproducer with 24 scrolling elements grew from 293 MiB to
2,324 MiB in 40 seconds. The same timeline with a fixed-pixel transform stayed
near 111 MiB for 90 seconds; the scroll-event control stayed near 116 MiB for
150 seconds. The CSS fallback also uses container-relative transforms by default,
so WebKit scrollbar elements receive a class that keeps the original percentage
rules instead. The patch avoids the problematic interpolation without changing
the page's scrolling or disabling CSS animations. Blink keeps the native path.

All four ESM/CommonJS package entrypoints used by package exports and the
legacy `main`/`module` fields carry the same change. The lockfile records the
patch hash so clean and frozen installs apply it.

`src/lib/overlay-scrollbars-memory.test.ts` runs fresh ESM and CommonJS imports
for desktop/iOS WebKit, Chromium browsers, and browsers without the API. It
checks that WebKit creates handles without native timeline animations and that
Chromium retains the native implementation. Browser functionality and memory
also need a system WebKit check: initialize real scrollbars, repeatedly change
the viewport's scroll position, and verify that the handle moves while
`document.getAnimations()` contains no scrollbar `ScrollTimeline` animations.

Revisit the patch when upgrading OverlayScrollbars or when WebKit fixes this
interpolation path. Test native physical footprint, rather than only a
JavaScript heap snapshot.
135 changes: 135 additions & 0 deletions patches/overlayscrollbars@2.15.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
diff --git a/overlayscrollbars.cjs b/overlayscrollbars.cjs
index 7268fd59a854208ab38233e920a995bf864ef6f1..19e256594fef0a5a535e81aa2da34b7e2d4c7804 100644
--- a/overlayscrollbars.cjs
+++ b/overlayscrollbars.cjs
@@ -62,7 +62,13 @@ const _ = getApi("IntersectionObserver");

const d = getApi("ResizeObserver");

-const p = getApi("ScrollTimeline");
+// WebKit retains native allocations when scroll timelines interpolate the
+// container-relative calc() transforms used by scrollbar handles. Use the
+// existing scroll-event fallback in that engine; keep Blink's native path.
+const isWebKit = /AppleWebKit\//.test(n.navigator && n.navigator.userAgent || "") &&
+ !/(?:Chrome|Chromium|Edg|OPR)\//.test(n.navigator && n.navigator.userAgent || "");
+
+const p = isWebKit ? void 0 : getApi("ScrollTimeline");

const isUndefined = t => t === void 0;

@@ -1967,7 +1973,7 @@ const createScrollbarsSetupElements = (t, n, o, s) => {
const generateScrollbarDOM = t => {
const n = t ? "x" : "y";
const o = t ? ft : _t;
- const e = createDiv(`${at} ${o}`);
+ const e = createDiv(`${at} ${o}${isWebKit ? " os-scrollbar-webkit" : ""}`);
const c = createDiv(dt);
const r = createDiv(pt);
const i = {
diff --git a/overlayscrollbars.cjs.js b/overlayscrollbars.cjs.js
index 010b70bd487226ccd01fb6c0d772ab6bb8638c51..1bd9797196a3079563be955c637fa23b530311d7 100644
--- a/overlayscrollbars.cjs.js
+++ b/overlayscrollbars.cjs.js
@@ -62,7 +62,13 @@ const _ = getApi("IntersectionObserver");

const d = getApi("ResizeObserver");

-const p = getApi("ScrollTimeline");
+// WebKit retains native allocations when scroll timelines interpolate the
+// container-relative calc() transforms used by scrollbar handles. Use the
+// existing scroll-event fallback in that engine; keep Blink's native path.
+const isWebKit = /AppleWebKit\//.test(n.navigator && n.navigator.userAgent || "") &&
+ !/(?:Chrome|Chromium|Edg|OPR)\//.test(n.navigator && n.navigator.userAgent || "");
+
+const p = isWebKit ? void 0 : getApi("ScrollTimeline");

const isUndefined = t => t === void 0;

@@ -1967,7 +1973,7 @@ const createScrollbarsSetupElements = (t, n, o, s) => {
const generateScrollbarDOM = t => {
const n = t ? "x" : "y";
const o = t ? ft : _t;
- const e = createDiv(`${at} ${o}`);
+ const e = createDiv(`${at} ${o}${isWebKit ? " os-scrollbar-webkit" : ""}`);
const c = createDiv(dt);
const r = createDiv(pt);
const i = {
diff --git a/overlayscrollbars.esm.js b/overlayscrollbars.esm.js
index c66166eacc57c3ed6a95f01e1bcc290a58c3d176..e054c67b241249d2bf2059bdc9872b23832e2bf6 100644
--- a/overlayscrollbars.esm.js
+++ b/overlayscrollbars.esm.js
@@ -56,7 +56,13 @@ const _ = getApi("IntersectionObserver");

const d = getApi("ResizeObserver");

-const p = getApi("ScrollTimeline");
+// WebKit retains native allocations when scroll timelines interpolate the
+// container-relative calc() transforms used by scrollbar handles. Use the
+// existing scroll-event fallback in that engine; keep Blink's native path.
+const isWebKit = /AppleWebKit\//.test(n.navigator && n.navigator.userAgent || "") &&
+ !/(?:Chrome|Chromium|Edg|OPR)\//.test(n.navigator && n.navigator.userAgent || "");
+
+const p = isWebKit ? void 0 : getApi("ScrollTimeline");

const isUndefined = t => t === void 0;

@@ -1961,7 +1967,7 @@ const createScrollbarsSetupElements = (t, n, o, s) => {
const generateScrollbarDOM = t => {
const n = t ? "x" : "y";
const o = t ? ft : _t;
- const e = createDiv(`${at} ${o}`);
+ const e = createDiv(`${at} ${o}${isWebKit ? " os-scrollbar-webkit" : ""}`);
const c = createDiv(dt);
const r = createDiv(pt);
const i = {
diff --git a/overlayscrollbars.mjs b/overlayscrollbars.mjs
index 82dc05b44e92c5125aee59691a00c3babee7d934..dd4e6826e75f48fca6d8e2784ed351873f5ce749 100644
--- a/overlayscrollbars.mjs
+++ b/overlayscrollbars.mjs
@@ -56,7 +56,13 @@ const _ = getApi("IntersectionObserver");

const d = getApi("ResizeObserver");

-const p = getApi("ScrollTimeline");
+// WebKit retains native allocations when scroll timelines interpolate the
+// container-relative calc() transforms used by scrollbar handles. Use the
+// existing scroll-event fallback in that engine; keep Blink's native path.
+const isWebKit = /AppleWebKit\//.test(n.navigator && n.navigator.userAgent || "") &&
+ !/(?:Chrome|Chromium|Edg|OPR)\//.test(n.navigator && n.navigator.userAgent || "");
+
+const p = isWebKit ? void 0 : getApi("ScrollTimeline");

const isUndefined = t => t === void 0;

@@ -1961,7 +1967,7 @@ const createScrollbarsSetupElements = (t, n, o, s) => {
const generateScrollbarDOM = t => {
const n = t ? "x" : "y";
const o = t ? ft : _t;
- const e = createDiv(`${at} ${o}`);
+ const e = createDiv(`${at} ${o}${isWebKit ? " os-scrollbar-webkit" : ""}`);
const c = createDiv(dt);
const r = createDiv(pt);
const i = {
diff --git a/styles/overlayscrollbars.css b/styles/overlayscrollbars.css
index 9fd60dce428b62713c7b2f216085402cf4ba8f3e..594d1304fc96a2e6b1c397971de457930ebd0c4a 100644
--- a/styles/overlayscrollbars.css
+++ b/styles/overlayscrollbars.css
@@ -478,15 +478,15 @@ body > .os-scrollbar {
.os-scrollbar-track {
container-type: size;
}
- .os-scrollbar-horizontal .os-scrollbar-handle {
+ .os-scrollbar-horizontal:not(.os-scrollbar-webkit) .os-scrollbar-handle {
left: auto;
transform: translateX(calc(var(--os-scroll-percent-directional) * 100cqw + var(--os-scroll-percent-directional) * -100%));
}
- .os-scrollbar-vertical .os-scrollbar-handle {
+ .os-scrollbar-vertical:not(.os-scrollbar-webkit) .os-scrollbar-handle {
top: auto;
transform: translateY(calc(var(--os-scroll-percent-directional) * 100cqh + var(--os-scroll-percent-directional) * -100%));
}
- .os-scrollbar-rtl.os-scrollbar-horizontal .os-scrollbar-handle {
+ .os-scrollbar-rtl.os-scrollbar-horizontal:not(.os-scrollbar-webkit) .os-scrollbar-handle {
right: auto;
left: 0;
}
Loading