diff --git a/node_modules/react-native-unistyles/lib/commonjs/web/registry.js b/node_modules/react-native-unistyles/lib/commonjs/web/registry.js index 120ff60..d1ff102 100644 --- a/node_modules/react-native-unistyles/lib/commonjs/web/registry.js +++ b/node_modules/react-native-unistyles/lib/commonjs/web/registry.js @@ -10,6 +10,17 @@ class UnistylesRegistry { stylesheets = new Map(); stylesCache = new Set(); stylesCounter = new Map(); + cleanupGeneration = 0; + finalizerTokens = new WeakMap(); + finalizationRegistry = typeof FinalizationRegistry === 'undefined' ? undefined : new FinalizationRegistry(({ + hash, + generation + }) => { + if (generation !== this.cleanupGeneration) { + return; + } + this.decrementAndMaybeCleanup(hash); + }); disposeListenersMap = new Map(); dependenciesMap = new Map(); constructor(services) { @@ -50,26 +61,48 @@ class UnistylesRegistry { this.disposeListenersMap.set(stylesheet, dispose); }; connect = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.add(ref); - this.stylesCounter.set(hash, stylesCounter); + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1); + if (!this.finalizationRegistry) { + return; + } + const token = {}; + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map(); + const tokens = tokensByHash.get(hash) ?? []; + tokens.push(token); + tokensByHash.set(hash, tokens); + this.finalizerTokens.set(ref, tokensByHash); + this.finalizationRegistry.register(ref, { + hash, + generation: this.cleanupGeneration + }, token); }; remove = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.delete(ref); - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false; - } - this.css.remove(hash); - this.stylesCache.delete(hash); - return true; - }); + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop(); + if (token) { + this.finalizationRegistry?.unregister(token); + } + return this.decrementAndMaybeCleanup(hash); + }; + decrementAndMaybeCleanup = hash => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1; + if (next > 0) { + this.stylesCounter.set(hash, next); + return Promise.resolve(false); } - return Promise.resolve(false); + this.stylesCounter.delete(hash); + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // Check if element is still in the DOM + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false; + } + if (document.querySelector(`.${hash}`)) { + return false; + } + this.css.remove(hash); + this.stylesCache.delete(hash); + return true; + }); }; add = (value, forChild) => { const generatedHash = (0, _utils.generateHash)(value); @@ -91,7 +124,10 @@ class UnistylesRegistry { this.css.add(hash, value); }; reset = () => { + this.cleanupGeneration += 1; + this.disposeListenersMap.forEach(dispose => dispose()); this.css.reset(); + this.stylesheets.clear(); this.stylesCache.clear(); this.dependenciesMap.clear(); this.disposeListenersMap.clear(); diff --git a/node_modules/react-native-unistyles/lib/module/web/registry.js b/node_modules/react-native-unistyles/lib/module/web/registry.js index 6362f43..8e769f8 100644 --- a/node_modules/react-native-unistyles/lib/module/web/registry.js +++ b/node_modules/react-native-unistyles/lib/module/web/registry.js @@ -6,6 +6,17 @@ export class UnistylesRegistry { stylesheets = new Map(); stylesCache = new Set(); stylesCounter = new Map(); + cleanupGeneration = 0; + finalizerTokens = new WeakMap(); + finalizationRegistry = typeof FinalizationRegistry === 'undefined' ? undefined : new FinalizationRegistry(({ + hash, + generation + }) => { + if (generation !== this.cleanupGeneration) { + return; + } + this.decrementAndMaybeCleanup(hash); + }); disposeListenersMap = new Map(); dependenciesMap = new Map(); constructor(services) { @@ -46,26 +57,48 @@ export class UnistylesRegistry { this.disposeListenersMap.set(stylesheet, dispose); }; connect = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.add(ref); - this.stylesCounter.set(hash, stylesCounter); + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1); + if (!this.finalizationRegistry) { + return; + } + const token = {}; + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map(); + const tokens = tokensByHash.get(hash) ?? []; + tokens.push(token); + tokensByHash.set(hash, tokens); + this.finalizerTokens.set(ref, tokensByHash); + this.finalizationRegistry.register(ref, { + hash, + generation: this.cleanupGeneration + }, token); }; remove = (ref, hash) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set(); - stylesCounter.delete(ref); - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false; - } - this.css.remove(hash); - this.stylesCache.delete(hash); - return true; - }); + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop(); + if (token) { + this.finalizationRegistry?.unregister(token); + } + return this.decrementAndMaybeCleanup(hash); + }; + decrementAndMaybeCleanup = hash => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1; + if (next > 0) { + this.stylesCounter.set(hash, next); + return Promise.resolve(false); } - return Promise.resolve(false); + this.stylesCounter.delete(hash); + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // Check if element is still in the DOM + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false; + } + if (document.querySelector(`.${hash}`)) { + return false; + } + this.css.remove(hash); + this.stylesCache.delete(hash); + return true; + }); }; add = (value, forChild) => { const generatedHash = generateHash(value); @@ -87,7 +120,10 @@ export class UnistylesRegistry { this.css.add(hash, value); }; reset = () => { + this.cleanupGeneration += 1; + this.disposeListenersMap.forEach(dispose => dispose()); this.css.reset(); + this.stylesheets.clear(); this.stylesCache.clear(); this.dependenciesMap.clear(); this.disposeListenersMap.clear(); diff --git a/node_modules/react-native-unistyles/src/web/registry.ts b/node_modules/react-native-unistyles/src/web/registry.ts index 39e0d9d..a21ba95 100644 --- a/node_modules/react-native-unistyles/src/web/registry.ts +++ b/node_modules/react-native-unistyles/src/web/registry.ts @@ -9,7 +9,24 @@ import { error, extractUnistyleDependencies, generateHash } from './utils' export class UnistylesRegistry { private readonly stylesheets = new Map, StyleSheet>() private readonly stylesCache = new Set() - private readonly stylesCounter = new Map>() + // Use counter instead of Set to avoid retaining detached DOM nodes + // FR is a fallback when remove() is missed + private readonly stylesCounter = new Map() + // Bumped on reset() so stale FR callbacks are ignored + private cleanupGeneration = 0 + // Per-registration unregister token, so remove() cancels only its own registration + private readonly finalizerTokens = new WeakMap>>() + // FR isn't available on every runtime - fall back to counter-only cleanup + private readonly finalizationRegistry = + typeof FinalizationRegistry === 'undefined' + ? undefined + : new FinalizationRegistry<{ hash: string; generation: number }>(({ hash, generation }) => { + if (generation !== this.cleanupGeneration) { + return + } + + this.decrementAndMaybeCleanup(hash) + }) private readonly disposeListenersMap = new Map() private readonly dependenciesMap = new Map, Set>() readonly css: CSSState @@ -78,33 +95,61 @@ export class UnistylesRegistry { } connect = (ref: HTMLElement, hash: string) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set() + this.stylesCounter.set(hash, (this.stylesCounter.get(hash) ?? 0) + 1) - stylesCounter.add(ref) - this.stylesCounter.set(hash, stylesCounter) + if (!this.finalizationRegistry) { + return + } + + const token = {} + const tokensByHash = this.finalizerTokens.get(ref) ?? new Map>() + const tokens = tokensByHash.get(hash) ?? [] + + tokens.push(token) + tokensByHash.set(hash, tokens) + this.finalizerTokens.set(ref, tokensByHash) + this.finalizationRegistry.register(ref, { hash, generation: this.cleanupGeneration }, token) } remove = (ref: HTMLElement, hash: string) => { - const stylesCounter = this.stylesCounter.get(hash) ?? new Set() + const token = this.finalizerTokens.get(ref)?.get(hash)?.pop() - stylesCounter.delete(ref) + if (token) { + this.finalizationRegistry?.unregister(token) + } - if (stylesCounter.size === 0) { - // Move this to the end of the event loop so the element is removed from the DOM - return Promise.resolve().then(() => { - // Check if element is still in the DOM - if (document.querySelector(`.${hash}`)) { - return false - } + return this.decrementAndMaybeCleanup(hash) + } + + private decrementAndMaybeCleanup = (hash: string): Promise => { + const next = (this.stylesCounter.get(hash) ?? 0) - 1 - this.css.remove(hash) - this.stylesCache.delete(hash) + if (next > 0) { + this.stylesCounter.set(hash, next) - return true - }) + return Promise.resolve(false) } - return Promise.resolve(false) + // Drop the empty bucket from the Map + this.stylesCounter.delete(hash) + + // Move this to the end of the event loop so the element is removed from the DOM + return Promise.resolve().then(() => { + // New connect raced in during the microtask + if ((this.stylesCounter.get(hash) ?? 0) > 0) { + return false + } + + // Check if element is still in the DOM + if (document.querySelector(`.${hash}`)) { + return false + } + + this.css.remove(hash) + this.stylesCache.delete(hash) + + return true + }) } add = (value: UnistylesValues, forChild?: boolean) => { @@ -126,7 +171,11 @@ export class UnistylesRegistry { } reset = () => { + // Invalidate pending FR callbacks from this generation + this.cleanupGeneration += 1 + this.disposeListenersMap.forEach((dispose) => dispose()) this.css.reset() + this.stylesheets.clear() this.stylesCache.clear() this.dependenciesMap.clear() this.disposeListenersMap.clear()