FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(core): scene session discard no longer retires a live window (#11… · NativeScript/NativeScript@399263d · GitHub

Commit 399263d

Browse files
authored
fix(core): scene session discard no longer retires a live window (#11376)
1 parent 6800aef commit 399263d

9 files changed

Lines changed: 210 additions & 14 deletions

‎packages/core/application/application-common.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,10 +540,44 @@ export class ApplicationCommon {
540540
});
541541
}
542542

543+
/**
544+
* @internal - retire the windows behind discarded window-session ids.
545+
*
546+
* A session id can only stand in for a window whose native surface is already gone.
547+
* iOS reports sessions discarded while the app was not running on the next launch,
548+
* and such an id can name the session driving the app now, so an id match alone is
549+
* no evidence that the window is finished with. Retiring an attached window tears
550+
* down the UI in use - its root view unloads and nothing ever reloads it - so only
551+
* detached windows are retired. Ids matching no window are ignored: they routinely
552+
* belong to windows this JS context has never seen.
553+
*/
554+
_retireDiscardedWindows(ids: string[]): void {
555+
for (const id of ids) {
556+
const nativeWindow = id ? this.getWindowById(id) : undefined;
557+
558+
if (!nativeWindow) {
559+
continue;
560+
}
561+
562+
if (nativeWindow.state === 'attached') {
563+
Trace.write(`Ignoring a discarded session for window '${id}': its surface is still attached.`, Trace.categories.NativeLifecycle, Trace.messageType.warn);
564+
565+
continue;
566+
}
567+
568+
nativeWindow._notifyEvent(NativeWindowEvents.close);
569+
this._unregisterWindow(nativeWindow);
570+
}
571+
}
572+
543573
/**
544574
* @internal - Unregister a NativeWindow when its native surface is gone for good.
545575
*/
546576
_unregisterWindow(nativeWindow: NativeWindow): void {
577+
if (!nativeWindow._surfaceGone) {
578+
Trace.write(`Unregistering window '${nativeWindow.id}' while its native surface is still live. A window may only be retired once the platform has reported the surface gone.`, Trace.categories.NativeLifecycle, Trace.messageType.error);
579+
}
580+
547581
const idx = this._windows.indexOf(nativeWindow);
548582
if (idx >= 0) {
549583
this._windows.splice(idx, 1);

‎packages/core/application/application-registry.spec.ts‎

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ describe('ApplicationCommon window registry', () => {
112112
setActiveWindow(undefined);
113113
});
114114

115+
/**
116+
* Retires a window the way a platform does: the disconnect callback records that the
117+
* surface is gone, and only then is the window unregistered.
118+
*/
119+
function retire(window: WindowBase): void {
120+
window._surfaceGone = true;
121+
app._unregisterWindow(asWindow(window));
122+
}
123+
115124
function record(...eventNames: string[]): Array<{ eventName: string; window: WindowBase }> {
116125
const recorded: Array<{ eventName: string; window: WindowBase }> = [];
117126
for (const eventName of eventNames) {
@@ -224,7 +233,7 @@ describe('ApplicationCommon window registry', () => {
224233
app._registerWindow(window);
225234
const recorded = record('windowClose');
226235

227-
app._unregisterWindow(window);
236+
retire(window);
228237

229238
expect(recorded.map((entry) => entry.window)).toEqual([window]);
230239
expect(app.getWindows()).toEqual([]);
@@ -244,7 +253,7 @@ describe('ApplicationCommon window registry', () => {
244253
detached._detach();
245254

246255
const recorded = record('windowClose', 'primaryWindowChanged');
247-
app._unregisterWindow(primary);
256+
retire(primary);
248257

249258
expect(events).toEqual(['windowClose', 'primaryWindowChanged']);
250259
expect(recorded[1].window).toBe(successor);
@@ -261,7 +270,7 @@ describe('ApplicationCommon window registry', () => {
261270
detached._detach();
262271

263272
record('windowClose', 'primaryWindowChanged');
264-
app._unregisterWindow(primary);
273+
retire(primary);
265274

266275
expect(events).toEqual(['windowClose']);
267276
expect(primary.isPrimary).toBe(false);
@@ -275,7 +284,7 @@ describe('ApplicationCommon window registry', () => {
275284
app._registerWindow(secondary);
276285

277286
record('windowClose', 'primaryWindowChanged');
278-
app._unregisterWindow(secondary);
287+
retire(secondary);
279288

280289
expect(events).toEqual(['windowClose']);
281290
expect(app.primaryWindow).toBe(primary);
@@ -308,7 +317,7 @@ describe('ApplicationCommon window registry', () => {
308317
it('falls back to the primary window once the active one closes', () => {
309318
secondary._notifyEvent(NativeWindowEvents.activate);
310319

311-
app._unregisterWindow(secondary);
320+
retire(secondary);
312321

313322
expect(app.activeWindow).toBe(primary);
314323
});
@@ -320,4 +329,80 @@ describe('ApplicationCommon window registry', () => {
320329
expect(app.activeWindow).toBe(primary);
321330
});
322331
});
332+
333+
describe('discarded window sessions', () => {
334+
let attached: TestWindow;
335+
let detached: TestWindow;
336+
337+
beforeEach(() => {
338+
attached = new TestWindow('scene-live', true).withContent();
339+
detached = new TestWindow('scene-gone').withContent();
340+
341+
app._registerWindow(attached);
342+
app._registerWindow(detached);
343+
344+
detached._detach();
345+
});
346+
347+
it('retires a window whose surface is already gone', () => {
348+
record('windowClose');
349+
350+
// The window drops its listeners in `_destroy`, so a listener only sees `close`
351+
// if it is raised before the window is unregistered.
352+
let closed = false;
353+
detached.on(NativeWindowEvents.close, () => {
354+
closed = true;
355+
});
356+
357+
app._retireDiscardedWindows(['scene-gone']);
358+
359+
expect(app.getWindowById('scene-gone')).toBeUndefined();
360+
expect(closed).toBe(true);
361+
expect(events).toEqual(['windowClose']);
362+
});
363+
364+
/**
365+
* iOS reports sessions discarded in an earlier run on the next launch, and such an
366+
* id can name the session driving the app now. Retiring on the id alone would unload
367+
* the live root view and every frame under it, and nothing reloads a root view whose
368+
* window has left the registry - navigation then queues forever behind `Frame.isLoaded`.
369+
*/
370+
it('leaves an attached window alone when a discarded id names it', () => {
371+
const rootView = attached.rootView as any;
372+
let unloaded = false;
373+
374+
rootView.isLoaded = true;
375+
rootView.callUnloaded = () => {
376+
unloaded = true;
377+
};
378+
379+
app._retireDiscardedWindows(['scene-live']);
380+
381+
expect(app.getWindowById('scene-live')).toBe(attached);
382+
expect(attached.state).toBe('attached');
383+
expect(unloaded).toBe(false);
384+
expect(events).toEqual([]);
385+
});
386+
387+
it('ignores ids that match no window', () => {
388+
record('windowClose');
389+
390+
app._retireDiscardedWindows(['scene-never-seen']);
391+
392+
expect(app.getWindows()).toEqual([attached, detached]);
393+
expect(events).toEqual([]);
394+
});
395+
396+
it('retires every detached window named in one discard', () => {
397+
const alsoDetached = new TestWindow('scene-gone-too').withContent();
398+
app._registerWindow(alsoDetached);
399+
alsoDetached._detach();
400+
401+
app._retireDiscardedWindows(['scene-gone', 'scene-live', 'scene-gone-too']);
402+
403+
expect(app.getWindowById('scene-gone')).toBeUndefined();
404+
expect(app.getWindowById('scene-gone-too')).toBeUndefined();
405+
expect(app.getWindowById('scene-live')).toBe(attached);
406+
});
407+
});
323408
});

‎packages/core/application/application.android.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ function initNativeScriptLifecycleCallbacks() {
153153
// there and wedging the app as never-suspended.
154154
Application.android._setWindowActive(nativeWindow, false, activity);
155155

156+
nativeWindow._surfaceGone = true;
157+
156158
// A destroyed activity only ends the window session when it is finishing —
157159
// otherwise Android is recreating it and the same window is reused.
158160
const isClosing = activity.isFinishing();

‎packages/core/application/application.ios.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,8 @@ class SceneDelegate extends UIResponder implements UIWindowSceneDelegate {
469469
Application.ios._setWindowInForeground(nativeWindow, false, windowScene);
470470

471471
if (nativeWindow) {
472+
nativeWindow._surfaceGone = true;
473+
472474
// A disconnect only ends the window session when the app asked for it —
473475
// otherwise iOS may reconnect the same session later. A window with no session
474476
// identity is the exception: a reconnect could never be matched back to it.
@@ -1386,25 +1388,26 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
13861388
// --- NativeWindow registry ---
13871389

13881390
/**
1389-
* @internal - iOS reports discarded sessions for windows this JS context may never
1390-
* have seen (they can arrive on a later launch), so unknown ids are ignored.
1391+
* @internal - hands the discarded sessions' ids to the window registry, which decides
1392+
* which of them name a window that is actually finished with.
13911393
*/
13921394
_onSceneSessionsDiscarded(sessions: NSSet<UISceneSession>): void {
13931395
const all = sessions?.allObjects;
13941396
if (!all) {
13951397
return;
13961398
}
13971399

1400+
const ids: string[] = [];
1401+
13981402
for (let i = 0; i < all.count; i++) {
13991403
const persistentIdentifier = all.objectAtIndex(i)?.persistentIdentifier;
1400-
const nativeWindow = persistentIdentifier ? this.getWindowById(`${persistentIdentifier}`) : undefined;
1401-
if (!nativeWindow) {
1402-
continue;
1403-
}
14041404

1405-
nativeWindow._notifyEvent(NativeWindowEvents.close);
1406-
this._unregisterWindow(nativeWindow);
1405+
if (persistentIdentifier) {
1406+
ids.push(`${persistentIdentifier}`);
1407+
}
14071408
}
1409+
1410+
this._retireDiscardedWindows(ids);
14081411
}
14091412

14101413
/**

‎packages/core/native-window/native-window-common.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,8 @@ export abstract class NativeWindow extends WindowBase {
415415
* this window for the same reason — it is still this window's content.
416416
*/
417417
_detach(): void {
418+
this._surfaceGone = true;
419+
418420
// Take a final reading while the surface can still answer and the root view is
419421
// still up: from here on these are what the window reports.
420422
this.orientation();
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { IOSNativeWindow } from './native-window.ios';
3+
4+
function sceneWithSession(persistentIdentifier?: string): any {
5+
return persistentIdentifier ? { session: { persistentIdentifier } } : { session: {} };
6+
}
7+
8+
const uiWindow: any = {};
9+
10+
/**
11+
* The flag decides whether a disconnect detaches the window for a later reconnect or ends
12+
* it outright, so it has to mean "this id came from the scene's session" and nothing looser.
13+
*/
14+
describe('IOSNativeWindow session identity', () => {
15+
it('recognises an id taken from the scene session', () => {
16+
const window = new IOSNativeWindow(sceneWithSession('ABC-123'), uiWindow, 'ABC-123');
17+
18+
expect(window._hasSessionIdentity).toBe(true);
19+
});
20+
21+
it('rejects a hand-minted id even when the window has a scene', () => {
22+
const window = new IOSNativeWindow(sceneWithSession('ABC-123'), uiWindow, 'embedded-main');
23+
24+
expect(window._hasSessionIdentity).toBe(false);
25+
});
26+
27+
it('rejects a hand-minted id on a window with no scene', () => {
28+
const window = new IOSNativeWindow(undefined, uiWindow, 'main');
29+
30+
expect(window._hasSessionIdentity).toBe(false);
31+
});
32+
33+
it('rejects a scene whose session carries no persistent identifier', () => {
34+
const window = new IOSNativeWindow(sceneWithSession(), uiWindow);
35+
36+
expect(window._hasSessionIdentity).toBe(false);
37+
expect(window.id).toMatch(/^window-\d+$/);
38+
});
39+
});

‎packages/core/native-window/native-window.ios.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ export class IOSNativeWindow extends NativeWindow {
2929

3030
constructor(scene: UIWindowScene | undefined, window: UIWindow, id?: string, isPrimary = false, role: WindowRole = 'application') {
3131
super(id, isPrimary, role);
32-
this._hasSessionIdentity = !!id;
32+
33+
// Only an id taken from the scene's session can be matched back to that session.
34+
// Hand-minted ids ('main', 'embedded-main') are ids all the same, so their mere
35+
// presence says nothing.
36+
const sessionId = scene?.session?.persistentIdentifier;
37+
this._hasSessionIdentity = !!sessionId && id === `${sessionId}`;
3338
this._scene = scene;
3439
this._window = window;
3540
}

‎packages/core/native-window/window-base.spec.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ describe('WindowBase lifecycle', () => {
189189
expect(events).toEqual(['attached', 'detached', 'attached', 'close', 'deactivate']);
190190
});
191191

192+
it('records the surface as gone on detach and clears it on re-attach', () => {
193+
const window = new TestWindow();
194+
expect(window._surfaceGone).toBe(false);
195+
196+
window._detach();
197+
expect(window._surfaceGone).toBe(true);
198+
199+
window._setState('attached');
200+
expect(window._surfaceGone).toBe(false);
201+
});
202+
192203
it('keeps listeners through a detach so a re-attached window still notifies them', () => {
193204
const window = new TestWindow();
194205
const events: string[] = [];

‎packages/core/native-window/window-base.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ export abstract class WindowBase extends Observable {
3535
private _state: WindowState = 'attached';
3636
private _isPrimary: boolean;
3737

38+
/**
39+
* @internal – whether the native surface behind this window is known to be gone.
40+
*
41+
* Only the platform's disconnect callback can establish this, and {@link state} cannot
42+
* stand in for it: a disconnect that ends the session unregisters the window while it is
43+
* still `attached`, so an attached window is not necessarily a live one.
44+
*/
45+
_surfaceGone = false;
46+
3847
constructor(id?: string, isPrimary = false, role: WindowRole = 'application') {
3948
super();
4049
this._id = id || `window-${++_windowIdCounter}`;
@@ -69,6 +78,12 @@ export abstract class WindowBase extends Observable {
6978
* @internal
7079
*/
7180
_setState(value: WindowState): void {
81+
// A window only ever attaches to a surface that exists, so attaching is what clears
82+
// the record of the previous one going away.
83+
if (value === 'attached') {
84+
this._surfaceGone = false;
85+
}
86+
7287
this._state = value;
7388
}
7489

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL