mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 20:05:23 +00:00
fix: defer reactive root disposal in cache cleanups
Same nested-dispose-in-onCleanup bug as 7f36ac2481 but in three more places: TerminalProvider.disposeAll, PromptProvider.disposeAll, and scoped-cache.clear() (covers viewCache.clear and comments cache.clear). All of them synchronously call createRoot dispose() on cached entries inside onCleanup, which during a server switch nests into the outer cleanNode cascade and throws TypeError at chunk-*.js:992. Snapshot the pending disposers, clear the cache synchronously, and fire the disposers on a microtask so the outer cleanup finishes first.
This commit is contained in:
@@ -232,10 +232,13 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
|
||||
const cache = new Map<string, PromptCacheEntry>()
|
||||
|
||||
const disposeAll = () => {
|
||||
for (const entry of cache.values()) {
|
||||
entry.dispose()
|
||||
}
|
||||
// Defer the dispose calls to a microtask; synchronous nested dispose
|
||||
// inside a parent onCleanup corrupts solid-js's in-flight cleanNode
|
||||
// traversal during mass remounts (see context/terminal.tsx for the
|
||||
// same pattern).
|
||||
const pending = Array.from(cache.values(), (entry) => entry.dispose)
|
||||
cache.clear()
|
||||
if (pending.length) queueMicrotask(() => pending.forEach((d) => d()))
|
||||
}
|
||||
|
||||
onCleanup(disposeAll)
|
||||
|
||||
@@ -364,10 +364,15 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
|
||||
onCleanup(() => caches.delete(cache))
|
||||
|
||||
const disposeAll = () => {
|
||||
for (const entry of cache.values()) {
|
||||
entry.dispose()
|
||||
}
|
||||
// Snapshot disposers, then defer them to a microtask. When this runs
|
||||
// from onCleanup during a parent remount (e.g. switching servers),
|
||||
// calling dispose() synchronously starts a nested cleanNode cascade on
|
||||
// a sibling root while the outer cascade is mid-traversal, corrupting
|
||||
// solid-js's graph walk state and throwing `Cannot read properties of
|
||||
// null (reading '1')` at chunk-*.js:992.
|
||||
const pending = Array.from(cache.values(), (entry) => entry.dispose)
|
||||
cache.clear()
|
||||
if (pending.length) queueMicrotask(() => pending.forEach((d) => d()))
|
||||
}
|
||||
|
||||
onCleanup(disposeAll)
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("createScopedCache", () => {
|
||||
expect(disposed).toEqual(["b"])
|
||||
})
|
||||
|
||||
test("disposes entries on delete and clear", () => {
|
||||
test("disposes entries on delete and clear", async () => {
|
||||
const disposed: string[] = []
|
||||
const cache = createScopedCache((key) => ({ key }), {
|
||||
dispose: (value) => disposed.push(value.key),
|
||||
@@ -39,6 +39,9 @@ describe("createScopedCache", () => {
|
||||
|
||||
cache.clear()
|
||||
expect(cache.peek("b")).toBeUndefined()
|
||||
// clear() defers dispose to a microtask to avoid nested cleanNode cascades
|
||||
// when called from inside an onCleanup; flush the queue before asserting.
|
||||
await Promise.resolve()
|
||||
expect(disposed).toEqual(["a", "b"])
|
||||
})
|
||||
|
||||
|
||||
@@ -89,10 +89,21 @@ export function createScopedCache<T>(createValue: (key: string) => T, options: S
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
for (const [key, entry] of store) {
|
||||
dispose(key, entry)
|
||||
}
|
||||
// Defer dispose() calls to a microtask. When clear() runs inside an
|
||||
// onCleanup during a parent remount (e.g. context/file.tsx and
|
||||
// context/comments.tsx both do this), synchronous dispose on cached
|
||||
// createRoot entries starts a nested cleanNode cascade while the outer
|
||||
// cascade is mid-traversal, corrupting solid-js's graph walk state and
|
||||
// throwing `Cannot read properties of null (reading '1')` at
|
||||
// chunk-*.js:992. Deferring lets the outer cleanup finish first.
|
||||
const pending: Array<[string, Entry<T>]> = []
|
||||
for (const entry of store) pending.push(entry)
|
||||
store.clear()
|
||||
if (pending.length && options.dispose) {
|
||||
queueMicrotask(() => {
|
||||
for (const [key, entry] of pending) dispose(key, entry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user