Skip to content
Merged
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
16 changes: 15 additions & 1 deletion docs/validation/terminal-cancelled-storage-flush-884.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ lifecycle event returns. WebScene consequently removed the only task that
could start the IndexedDB write and observed no terminal persistence work, so
`pagehide` and reload handoff proceeded immediately.

After retaining that callback, repeated installed-package runs exposed the
second half of the shutdown race. VS Code also closes its storage database from
a shutdown microtask before the retained timer starts. One run passed in
16.204 seconds, while the next failed in 25.623 seconds with the key present
before reload and absent after reopening; the failed log reported that
`vscode-web-db` was closed.

A focused native fixture reproduces the exact order. Before the fix it reports:

```text
Expand All @@ -49,6 +56,12 @@ WebScene retains the accepted callback privately until the handoff boundary.
Its microtasks, zero-delay descendants, and IndexedDB operations inherit the
same bounded terminal ancestry.

If `IDBDatabase.close()` runs from that same lifecycle turn while accepted
terminal persistence remains, the compatibility layer queues the close behind
the retained flush. The database still closes before `pagehide` and host
handoff. Calls made outside this narrow terminal condition keep synchronous
close semantics.

This terminal-only durability rule is intentionally narrower than ordinary
timer cancellation. Timers created outside terminal lifecycle, delayed timers,
intervals, animation frames, and work beyond the 1,024-task budget keep their
Expand All @@ -65,7 +78,8 @@ WEBSCENE_NATIVE_ENGINE_TEST_FILTER=terminal-indexeddb-navigation \
```

Result on macOS arm64 Release: pass. The durable transaction-to-handoff gate
completed in 0.0036 seconds. The fixture also covers restart durability,
completed in 0.0037 seconds. The fixture also covers a shutdown-microtask
database close before the retained flush, restart durability,
pre-existing IndexedDB work, exact lifecycle order, ordinary cancellation,
future timers, intervals, native completion descendants, and the bounded
self-refilling-task ceiling.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ inline constexpr std::string_view indexeddb_compatibility_source = R"JS(
'use strict';
const nativeStorage = globalThis.__webSceneIndexedDBStorage;
const scheduleTask = globalThis.__webSceneIndexedDBTask;
if (typeof nativeStorage !== 'function' || typeof scheduleTask !== 'function') return;
const shouldDeferClose = globalThis.__webSceneIndexedDBShouldDeferClose;
if (typeof nativeStorage !== 'function' || typeof scheduleTask !== 'function'
|| typeof shouldDeferClose !== 'function') return;
const openConnections = new Map();
const loadedDatabases = new Map();
const writeTails = new Map();
Expand Down Expand Up @@ -375,6 +377,7 @@ R"JS( class IDBTransaction extends EventTarget {
this._state = state;
this._revision = revision;
this._closed = false;
this._terminalClosePending = false;
this._upgradeTransaction = null;
this.onabort = null;
this.onerror = null;
Expand Down Expand Up @@ -421,6 +424,15 @@ R"JS( class IDBTransaction extends EventTarget {
}
close() {
if (this._closed) return;
if (shouldDeferClose()) {
if (this._terminalClosePending) return;
this._terminalClosePending = true;
scheduleTask(() => {
this._terminalClosePending = false;
this.close();
});
return;
}
this._closed = true;
openConnections.get(this.name)?.delete(this);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,17 @@
schedule_timer(info, false, true);
}

static void indexeddb_should_defer_close(
const v8::FunctionCallbackInfo<v8::Value>& info)
{
auto* self = current(info.GetIsolate());
info.GetReturnValue().Set(v8::Boolean::New(
info.GetIsolate(),
self != nullptr
&& self->window_close_lifecycle_dispatching
&& self->has_terminal_handoff_persistence_work()));
}

void install_indexeddb(v8::Local<v8::Context> realm)
{
if (indexeddb == nullptr) return;
Expand All @@ -617,6 +628,11 @@
js_string(isolate, "__webSceneIndexedDBTask"),
v8::Function::New(realm, indexeddb_schedule_task).ToLocalChecked(),
v8::PropertyAttribute::DontEnum).Check();
global->DefineOwnProperty(
realm,
js_string(isolate, "__webSceneIndexedDBShouldDeferClose"),
v8::Function::New(realm, indexeddb_should_defer_close).ToLocalChecked(),
v8::PropertyAttribute::DontEnum).Check();
auto script = v8::Script::Compile(
realm,
js_string(isolate, std::string(indexeddb_compatibility_source).c_str()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,12 @@ void test_terminal_navigation_waits_for_indexeddb_commit()
};
}, 0);
clearTimeout(pendingFlushTimer);
// Browser workbench disposal closes its storage database from the
// same shutdown turn before the retained callback starts.
Promise.resolve().then(() => {
terminalIndexedDb.order.push('close-request');
database.close();
});
});
addEventListener('pagehide', () => {
terminalIndexedDb.order.push('pagehide');
Expand Down Expand Up @@ -732,8 +738,10 @@ void test_terminal_navigation_waits_for_indexeddb_commit()
"&&terminalIndexedDb.continuation===true"
"&&!terminalIndexedDb.ordinaryCancelFired"
"&&terminalIndexedDb.order[0]==='beforeunload'"
"&&terminalIndexedDb.order.indexOf('close-request')>0"
"&&terminalIndexedDb.order.indexOf('preexisting-commit')>0"
"&&terminalIndexedDb.order.indexOf('flush-timer')>0"
"&&terminalIndexedDb.order.indexOf('flush-timer')>"
"terminalIndexedDb.order.indexOf('close-request')"
"&&terminalIndexedDb.order.indexOf('commit')>"
"terminalIndexedDb.order.indexOf('flush-timer')"
"&&terminalIndexedDb.order.indexOf('continuation')>"
Expand Down
Loading