diff --git a/packages/core/application/application-common.spec.ts b/packages/core/application/application-common.spec.ts new file mode 100644 index 0000000000..19ba455e2f --- /dev/null +++ b/packages/core/application/application-common.spec.ts @@ -0,0 +1,36 @@ +import { ApplicationCommon } from './application-common'; +import { getAppMainEntry, setAppMainEntry } from './helpers-common'; + +class TestApplication extends ApplicationCommon { + getRootView() { + return null as any; + } +} + +describe('__onApplicationReload', () => { + afterEach(() => { + setAppMainEntry(undefined); + delete global.__onApplicationReload; + }); + + it('remounts the current main entry without constructing a new Application', () => { + const app = new TestApplication(); + const entry = { moduleName: 'app-root' }; + setAppMainEntry(entry); + + const remount = vi.spyOn(app, 'resetRootView'); + global.__onApplicationReload(); + + expect(remount).toHaveBeenCalledWith(entry); + expect(getAppMainEntry()).toBe(entry); + }); + + it('is a no-op when the app has no main entry yet', () => { + const app = new TestApplication(); + const remount = vi.spyOn(app, 'resetRootView'); + + global.__onApplicationReload(); + + expect(remount).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/application/application-common.ts b/packages/core/application/application-common.ts index b9c474c82e..692fdfe52e 100644 --- a/packages/core/application/application-common.ts +++ b/packages/core/application/application-common.ts @@ -227,6 +227,15 @@ export class ApplicationCommon { const rootView = this.getRootView(); this.livesync(rootView, context); }; + + // Isolate-preserving reload remounts the current entry without a new runtime. + global.__onApplicationReload = () => { + const entry = getAppMainEntry(); + if (!entry) { + return; + } + this.resetRootView(entry); + }; } /** diff --git a/packages/core/application/application.ios.ts b/packages/core/application/application.ios.ts index b0b94e2d8b..7980ee856f 100644 --- a/packages/core/application/application.ios.ts +++ b/packages/core/application/application.ios.ts @@ -127,16 +127,6 @@ function supportsMultipleScenes(): boolean { return UIApplication.sharedApplication?.supportsMultipleScenes; } -/** - * Number of times the JS runtime has been soft-rebooted in this process via - * NativeScriptRuntime.reloadApplication / restartWithConfig. 0 on first boot. - * Provided as a global by the iOS runtime (v9+); older runtimes report 0. - */ -function getRuntimeReloadCount(): number { - const runtime = (globalThis as any).NativeScriptRuntime; - return runtime && typeof runtime.reloadCount === 'number' ? runtime.reloadCount : 0; -} - @NativeClass class Responder extends UIResponder implements UIApplicationDelegate { get window(): UIWindow { @@ -279,13 +269,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication private _notificationObservers: NotificationObserver[] = []; - // Strong references to delegates recreated after an in-process soft reboot - // (NativeScriptRuntime.reloadApplication). UIApplication.delegate is an - // `assign` property and UIScene keeps its own reference to the delegate we - // replace, so without these the fresh instances would be deallocated. - private _softRebootAppDelegate: UIApplicationDelegate; - private _softRebootSceneDelegates = new Map(); - displayedOnce = false; displayedLinkTarget: CADisplayLinkTarget; displayedLink: CADisplayLink; @@ -378,8 +361,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication } private runAsEmbeddedApp() { - this._reattachNativeDelegatesAfterSoftReboot(); - // TODO: this rootView should be held alive until rootController dismissViewController is called. const rootView = this.createRootView(this._rootView, true); if (!rootView) { @@ -391,10 +372,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication let window = getWindow() as UIWindow; if (!window) { - // In-process soft reboot with OTAs. - // Original UIWindow is deallocated when the old JS isolate is torn down. - // Recreate a window bound to the active UIWindowScene so the - // root has somewhere to attach. const app = UIApplication.sharedApplication; const all = app && app.connectedScenes ? app.connectedScenes.allObjects : null; let targetScene: UIWindowScene; @@ -414,13 +391,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication window = UIWindow.alloc().initWithWindowScene(targetScene); this._setWindowForScene(window, targetScene); this._setupWindowForScene?.(window, targetScene); - - // If the scene's delegate was recreated after a soft reboot, point it - // at the new window so `scene.delegate.window` queries resolve. - const freshSceneDelegate = this._softRebootSceneDelegates.get(targetScene); - if (freshSceneDelegate) { - freshSceneDelegate.window = window; - } } } @@ -470,67 +440,6 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication this.notifyAppStarted(); } - /** - * After an in-process soft reboot (NativeScriptRuntime.reloadApplication / - * restartWithConfig), the Objective-C delegate classes created by the - * previous JS isolate still exist and UIKit keeps dispatching to their - * now-inert instances: their method callbacks bail out because the isolate - * that implemented them is gone. Notification-center observers are - * re-registered by the new isolate, but delegate-based dispatch (custom - * UIApplicationDelegate methods like push-token/openURL callbacks, and the - * UIScene delegates used by scene-lifecycle apps) stays pinned to the old - * bundle. Recreate those delegates from this bundle's classes and re-point - * UIKit at them. - */ - private _reattachNativeDelegatesAfterSoftReboot(): void { - if (getRuntimeReloadCount() <= 0) { - // First boot: UIApplicationMain (or the host app) set up delegates. - return; - } - - if (isEmbedded()) { - // The host app owns the UIApplication delegate; never touch it. - return; - } - - const app = UIApplication.sharedApplication; - if (!app) { - return; - } - - // Fresh application delegate from the new bundle. Assigning `delegate` - // does not retain (unlike the UIApplicationMain launch path), so keep a - // strong reference ourselves. - this.delegate ??= Responder as any; - const freshDelegate = (this.delegate).new() as UIApplicationDelegate; - this._softRebootAppDelegate = freshDelegate; - app.delegate = freshDelegate; - - // Re-point already-connected scenes at fresh scene delegates so scene - // lifecycle and user-implemented scene delegate methods (shortcuts, - // openURLContexts, userActivity continuation, etc.) reach this isolate. - // Newly connecting scenes are covered by the fresh application delegate's - // applicationConfigurationForConnectingSceneSessionOptions, which returns - // this bundle's SceneDelegate class. - if (this.supportsScenes()) { - this._softRebootSceneDelegates.clear(); - const scenes = app.connectedScenes?.allObjects; - for (let i = 0; scenes && i < scenes.count; i++) { - const scene = scenes.objectAtIndex(i); - if (!(scene instanceof UIWindowScene)) { - continue; - } - const freshSceneDelegate = SceneDelegate.new() as UIWindowSceneDelegate; - scene.delegate = freshSceneDelegate; - this._softRebootSceneDelegates.set(scene, freshSceneDelegate); - } - } - - if (Trace.isEnabled()) { - Trace.write(`Reattached application delegate${this._softRebootSceneDelegates.size ? ` and ${this._softRebootSceneDelegates.size} scene delegate(s)` : ''} after soft reboot (reloadCount: ${getRuntimeReloadCount()})`, Trace.categories.NativeLifecycle); - } - } - private getViewController(rootView: View): UIViewController { let viewController: UIViewController = rootView.viewController || rootView.ios; diff --git a/packages/core/global-types.d.ts b/packages/core/global-types.d.ts index 78ca864489..ab6bb99f3d 100644 --- a/packages/core/global-types.d.ts +++ b/packages/core/global-types.d.ts @@ -111,6 +111,7 @@ declare module globalThis { var __extends: any; var __onLiveSync: (context?: { type: string; path: string }) => void; var __onLiveSyncCore: (context?: { type: string; path: string }) => void; + var __onApplicationReload: () => void; var __onUncaughtError: (error: NativeScriptError) => void; var __onDiscardedError: (error: NativeScriptError) => void; var __snapshot: boolean; @@ -165,39 +166,39 @@ interface NodeModule { } declare enum RequestContext { - 'audio', - 'beacon', - 'cspreport', - 'download', - 'embed', - 'eventsource', - 'favicon', - 'fetch', - 'font', - 'form', - 'frame', - 'hyperlink', - 'iframe', - 'image', - 'imageset', - 'import', - 'internal', - 'location', - 'manifest', - 'object', - 'ping', - 'plugin', - 'prefetch', - 'script', - 'serviceworker', - 'sharedworker', - 'subresource', - 'style', - 'track', - 'video', - 'worker', - 'xmlhttprequest', - 'xslt', + audio, + beacon, + cspreport, + download, + embed, + eventsource, + favicon, + fetch, + font, + form, + frame, + hyperlink, + iframe, + image, + imageset, + import, + internal, + location, + manifest, + object, + ping, + plugin, + prefetch, + script, + serviceworker, + sharedworker, + subresource, + style, + track, + video, + worker, + xmlhttprequest, + xslt, } // Extend the lib.dom.d.ts Body interface with `formData`