Skip to content

Commit 62c4c61

Browse files
Defer terminal IndexedDB close behind storage flush (#886)
1 parent c10b782 commit 62c4c61

4 files changed

Lines changed: 53 additions & 3 deletions

File tree

‎docs/validation/terminal-cancelled-storage-flush-884.md‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ lifecycle event returns. WebScene consequently removed the only task that
3232
could start the IndexedDB write and observed no terminal persistence work, so
3333
`pagehide` and reload handoff proceeded immediately.
3434

35+
After retaining that callback, repeated installed-package runs exposed the
36+
second half of the shutdown race. VS Code also closes its storage database from
37+
a shutdown microtask before the retained timer starts. One run passed in
38+
16.204 seconds, while the next failed in 25.623 seconds with the key present
39+
before reload and absent after reopening; the failed log reported that
40+
`vscode-web-db` was closed.
41+
3542
A focused native fixture reproduces the exact order. Before the fix it reports:
3643

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

59+
If `IDBDatabase.close()` runs from that same lifecycle turn while accepted
60+
terminal persistence remains, the compatibility layer queues the close behind
61+
the retained flush. The database still closes before `pagehide` and host
62+
handoff. Calls made outside this narrow terminal condition keep synchronous
63+
close semantics.
64+
5265
This terminal-only durability rule is intentionally narrower than ordinary
5366
timer cancellation. Timers created outside terminal lifecycle, delayed timers,
5467
intervals, animation frames, and work beyond the 1,024-task budget keep their
@@ -65,7 +78,8 @@ WEBSCENE_NATIVE_ENGINE_TEST_FILTER=terminal-indexeddb-navigation \
6578
```
6679

6780
Result on macOS arm64 Release: pass. The durable transaction-to-handoff gate
68-
completed in 0.0036 seconds. The fixture also covers restart durability,
81+
completed in 0.0037 seconds. The fixture also covers a shutdown-microtask
82+
database close before the retained flush, restart durability,
6983
pre-existing IndexedDB work, exact lifecycle order, ordinary cancellation,
7084
future timers, intervals, native completion descendants, and the bounded
7185
self-refilling-task ceiling.

‎experiments/WebScene.NativeEngine.Probe/native/webscene_indexeddb_compatibility.h‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ inline constexpr std::string_view indexeddb_compatibility_source = R"JS(
99
'use strict';
1010
const nativeStorage = globalThis.__webSceneIndexedDBStorage;
1111
const scheduleTask = globalThis.__webSceneIndexedDBTask;
12-
if (typeof nativeStorage !== 'function' || typeof scheduleTask !== 'function') return;
12+
const shouldDeferClose = globalThis.__webSceneIndexedDBShouldDeferClose;
13+
if (typeof nativeStorage !== 'function' || typeof scheduleTask !== 'function'
14+
|| typeof shouldDeferClose !== 'function') return;
1315
const openConnections = new Map();
1416
const loadedDatabases = new Map();
1517
const writeTails = new Map();
@@ -375,6 +377,7 @@ R"JS( class IDBTransaction extends EventTarget {
375377
this._state = state;
376378
this._revision = revision;
377379
this._closed = false;
380+
this._terminalClosePending = false;
378381
this._upgradeTransaction = null;
379382
this.onabort = null;
380383
this.onerror = null;
@@ -421,6 +424,15 @@ R"JS( class IDBTransaction extends EventTarget {
421424
}
422425
close() {
423426
if (this._closed) return;
427+
if (shouldDeferClose()) {
428+
if (this._terminalClosePending) return;
429+
this._terminalClosePending = true;
430+
scheduleTask(() => {
431+
this._terminalClosePending = false;
432+
this.close();
433+
});
434+
return;
435+
}
424436
this._closed = true;
425437
openConnections.get(this.name)?.delete(this);
426438
}

‎experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_indexeddb.inc‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,17 @@
603603
schedule_timer(info, false, true);
604604
}
605605

606+
static void indexeddb_should_defer_close(
607+
const v8::FunctionCallbackInfo<v8::Value>& info)
608+
{
609+
auto* self = current(info.GetIsolate());
610+
info.GetReturnValue().Set(v8::Boolean::New(
611+
info.GetIsolate(),
612+
self != nullptr
613+
&& self->window_close_lifecycle_dispatching
614+
&& self->has_terminal_handoff_persistence_work()));
615+
}
616+
606617
void install_indexeddb(v8::Local<v8::Context> realm)
607618
{
608619
if (indexeddb == nullptr) return;
@@ -617,6 +628,11 @@
617628
js_string(isolate, "__webSceneIndexedDBTask"),
618629
v8::Function::New(realm, indexeddb_schedule_task).ToLocalChecked(),
619630
v8::PropertyAttribute::DontEnum).Check();
631+
global->DefineOwnProperty(
632+
realm,
633+
js_string(isolate, "__webSceneIndexedDBShouldDeferClose"),
634+
v8::Function::New(realm, indexeddb_should_defer_close).ToLocalChecked(),
635+
v8::PropertyAttribute::DontEnum).Check();
620636
auto script = v8::Script::Compile(
621637
realm,
622638
js_string(isolate, std::string(indexeddb_compatibility_source).c_str()))

‎experiments/WebScene.NativeEngine.Probe/tests/native_v8_runtime_indexeddb_tests.inc‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,12 @@ void test_terminal_navigation_waits_for_indexeddb_commit()
679679
};
680680
}, 0);
681681
clearTimeout(pendingFlushTimer);
682+
// Browser workbench disposal closes its storage database from the
683+
// same shutdown turn before the retained callback starts.
684+
Promise.resolve().then(() => {
685+
terminalIndexedDb.order.push('close-request');
686+
database.close();
687+
});
682688
});
683689
addEventListener('pagehide', () => {
684690
terminalIndexedDb.order.push('pagehide');
@@ -732,8 +738,10 @@ void test_terminal_navigation_waits_for_indexeddb_commit()
732738
"&&terminalIndexedDb.continuation===true"
733739
"&&!terminalIndexedDb.ordinaryCancelFired"
734740
"&&terminalIndexedDb.order[0]==='beforeunload'"
741+
"&&terminalIndexedDb.order.indexOf('close-request')>0"
735742
"&&terminalIndexedDb.order.indexOf('preexisting-commit')>0"
736-
"&&terminalIndexedDb.order.indexOf('flush-timer')>0"
743+
"&&terminalIndexedDb.order.indexOf('flush-timer')>"
744+
"terminalIndexedDb.order.indexOf('close-request')"
737745
"&&terminalIndexedDb.order.indexOf('commit')>"
738746
"terminalIndexedDb.order.indexOf('flush-timer')"
739747
"&&terminalIndexedDb.order.indexOf('continuation')>"

0 commit comments

Comments
 (0)