diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..94a458a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +本文件记录 `ShrinkContext.AppAdapter` 的包内变更。 + +## [0.1.4] - 2026-08-28 + +### Added + +- `ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(CancellationToken)` 支持多个调用方并发等待 ContextLoader 完成启动。 +- `ShrinkAppLoaderBootstrapper.StartError` 暴露启动失败异常。 + +### Changed + +- ContextLoader 启动失败时发布 `ShrinkAppStartFailedEvent`,等待方收到原始异常,不再依赖轮询。 +- 精确依赖 `ShrinkApp.Core 0.1.3`。 diff --git a/CHANGELOG.md.meta b/CHANGELOG.md.meta new file mode 100644 index 0000000..5c0d2af --- /dev/null +++ b/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: b1937438081846a4ae5ca6f747953987 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Development~/UnityProject/Packages/manifest.json b/Development~/UnityProject/Packages/manifest.json index b835c39..24692be 100644 --- a/Development~/UnityProject/Packages/manifest.json +++ b/Development~/UnityProject/Packages/manifest.json @@ -11,12 +11,12 @@ "dependencies": { "com.unity.test-framework": "1.1.33", "com.cneicy.shrink-context-app-adapter": "file:../../..", - "com.cneicy.shrink-app-starter-basic": "0.2.0", + "com.cneicy.shrink-app-starter-basic": "0.2.1", "com.cneicy.shrink-command": "0.2.0", - "com.cneicy.shrink-command-integration-app": "0.1.1", - "com.cneicy.shrink-datasaver-integration-app": "0.1.1", + "com.cneicy.shrink-command-integration-app": "0.1.2", + "com.cneicy.shrink-datasaver-integration-app": "0.1.2", "com.cneicy.shrink-network": "0.2.1", - "com.cneicy.shrink-network-integration-app": "0.1.1", + "com.cneicy.shrink-network-integration-app": "0.1.2", "com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask#7c0f199fe0d3fc528024488ccd671e6c7b27745b" }, "testables": [ diff --git a/README.md b/README.md index c8b480c..5554c03 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,17 @@ ShrinkApp ↔ ShrinkContext 的桥接层:提供当前默认的 ContextLoader `ShrinkAppSettings.hostingMode` 设为 `ContextLoader`(Inspector 或资产字段)。经典项目默认 `ClassicHost`,行为零变化。 ```csharp +// 场景业务等待宿主完成;失败时直接收到启动异常。 +await ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(cancellationToken); + // 运行中按模块开关(宿主与其余模块不重启) var host = ShrinkAppLoaderBootstrapper.Instance!.Host; await host.SetModuleDisabledAsync("shrink.network", true); ``` +`ShrinkAppLoaderBootstrapper.StartError` 保留最近一次启动异常;启动失败同时发布 +`ShrinkAppStartFailedEvent`。调用方不需要轮询 `Host.IsRunning`。 + ## 声明式组合 默认资源名为 `Assets/Resources/ShrinkAppComposition.asset`。组合根先用代码注册安装器或原生组件工厂,Bootstrapper 再应用 Profile;配置文件不能通过类型名反射实例化任意组件。 diff --git a/Runtime/ShrinkAppLoaderBootstrapper.cs b/Runtime/ShrinkAppLoaderBootstrapper.cs index c053b18..9db316b 100644 --- a/Runtime/ShrinkAppLoaderBootstrapper.cs +++ b/Runtime/ShrinkAppLoaderBootstrapper.cs @@ -1,6 +1,9 @@ #nullable enable +using System; +using System.Threading; using Cysharp.Threading.Tasks; using ShrinkApp; +using ShrinkEventBus; using UnityEngine; namespace ShrinkContext.AppAdapter @@ -14,6 +17,7 @@ namespace ShrinkContext.AppAdapter public sealed class ShrinkAppLoaderBootstrapper : MonoBehaviour { private static ShrinkAppLoaderBootstrapper? _instance; + private static UniTaskCompletionSource _startupCompletion = new(); /// /// Starter/组合包在 SubsystemRegistration 阶段设置的默认装配表。 @@ -22,9 +26,43 @@ namespace ShrinkContext.AppAdapter public static System.Action? DefaultComposition { get; set; } public static ShrinkAppLoaderBootstrapper? Instance => _instance; + public static Exception? StartError { get; private set; } public ShrinkAppLoaderHost Host { get; private set; } = null!; + /// + /// 等待 ContextLoader 宿主完成启动。支持多个并发等待者;宿主失败时以原始异常结束。 + /// + public static async UniTask WaitUntilStartedAsync(CancellationToken cancellationToken = default) + { + if (_instance?.Host?.IsRunning == true) + return; + if (StartError != null) + throw StartError; + if (ShrinkAppSettings.Instance.hostingMode != ShrinkAppHostingMode.ContextLoader) + { + throw new InvalidOperationException( + "ShrinkAppLoaderBootstrapper requires ShrinkAppHostingMode.ContextLoader."); + } + + var task = _startupCompletion.Task; + if (cancellationToken.CanBeCanceled) + await task.AttachExternalCancellation(cancellationToken); + else + await task; + + if (StartError != null) + throw StartError; + } + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticState() + { + _instance = null; + StartError = null; + _startupCompletion = new UniTaskCompletionSource(); + } + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void AutoBootstrap() { @@ -45,17 +83,39 @@ namespace ShrinkContext.AppAdapter } _instance = this; - Host = new ShrinkAppLoaderHost(); - DefaultComposition?.Invoke(Host); - var profile = ShrinkAppCompositionProfile.LoadDefault(); - if (profile != null) - Host.ApplyComposition(profile.ResolveDocument()); - global::ShrinkApp.ShrinkApp.InitializeForExternalHost(Host.Services); - Host.StartAsync().Forget(ex => + StartHostAsync().Forget(); + } + + private async UniTask StartHostAsync() + { + try { + Host = new ShrinkAppLoaderHost(); + DefaultComposition?.Invoke(Host); + var profile = ShrinkAppCompositionProfile.LoadDefault(); + if (profile != null) + Host.ApplyComposition(profile.ResolveDocument()); + global::ShrinkApp.ShrinkApp.InitializeForExternalHost(Host.Services); + await Host.StartAsync(); + CompleteStartup(null); + } + catch (Exception ex) + { + CompleteStartup(ex); + EventBus.Post(new ShrinkAppStartFailedEvent + { + ErrorMessage = ex.Message, + FailedModuleId = string.Empty + }); Debug.LogException(ex); Debug.LogError("[ShrinkApp.LoaderHost] 启动失败: " + ex.Message); - }); + } + } + + private static void CompleteStartup(Exception? error) + { + StartError = error; + _startupCompletion.TrySetResult(); } private void OnDestroy() diff --git a/Tests/ShrinkAppLoaderHostTests.cs b/Tests/ShrinkAppLoaderHostTests.cs index 9a61a5f..80c2051 100644 --- a/Tests/ShrinkAppLoaderHostTests.cs +++ b/Tests/ShrinkAppLoaderHostTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Cysharp.Threading.Tasks; using NUnit.Framework; using ShrinkApp; @@ -39,6 +40,8 @@ namespace ShrinkContext.AppAdapter.Tests [TearDown] public void TearDown() { + ResetBootstrapperState(); + SetSharedSettings(null); if (_settings != null) Object.DestroyImmediate(_settings); } @@ -68,6 +71,33 @@ namespace ShrinkContext.AppAdapter.Tests CollectionAssert.AreEquivalent(new[] { "app.a", "app.b" }, probe.Value!.ModuleIds); } + [Test] + public void WaitUntilStartedAsync_AllConcurrentWaitersCompleteTogether() + { + PrepareBootstrapperWaitState(); + + var first = ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(); + var second = ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync(); + CompleteBootstrapperStartup(null); + + TestAwait.Run(UniTask.WhenAll(first, second)); + Assert.IsNull(ShrinkAppLoaderBootstrapper.StartError); + } + + [Test] + public void WaitUntilStartedAsync_PropagatesObservableStartupError() + { + PrepareBootstrapperWaitState(); + var expected = new InvalidOperationException("test startup failure"); + CompleteBootstrapperStartup(expected); + + var actual = Assert.Throws(() => + TestAwait.Run(ShrinkAppLoaderBootstrapper.WaitUntilStartedAsync())); + + Assert.AreSame(expected, actual); + Assert.AreSame(expected, ShrinkAppLoaderBootstrapper.StartError); + } + [Test] public void SetModuleDisabled_RetiresModuleAndDependents_EnableReloadsWithoutRestart() { @@ -210,5 +240,36 @@ namespace ShrinkContext.AppAdapter.Tests public string Value { get; } } + + private void PrepareBootstrapperWaitState() + { + ResetBootstrapperState(); + _settings.hostingMode = ShrinkAppHostingMode.ContextLoader; + SetSharedSettings(_settings); + } + + private static void CompleteBootstrapperStartup(Exception? error) + { + var method = typeof(ShrinkAppLoaderBootstrapper).GetMethod( + "CompleteStartup", BindingFlags.NonPublic | BindingFlags.Static); + Assert.IsNotNull(method); + method!.Invoke(null, new object?[] { error }); + } + + private static void ResetBootstrapperState() + { + var method = typeof(ShrinkAppLoaderBootstrapper).GetMethod( + "ResetStaticState", BindingFlags.NonPublic | BindingFlags.Static); + Assert.IsNotNull(method); + method!.Invoke(null, null); + } + + private static void SetSharedSettings(ShrinkAppSettings? settings) + { + var property = typeof(ShrinkAppSettings).GetProperty( + nameof(ShrinkAppSettings.Instance), BindingFlags.Public | BindingFlags.Static); + Assert.IsNotNull(property); + property!.SetValue(null, settings); + } } } diff --git a/package.json b/package.json index e2fb9ac..7324b0b 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "com.cneicy.shrink-context-app-adapter", - "version": "0.1.3", + "version": "0.1.4", "displayName": "ShrinkContext - ShrinkApp Adapter", "description": "把现有 IShrinkAppModuleInstaller 包装为 ShrinkContext 组件:依赖缺失改为等待而非抛错,模块激活由响应式余效应驱动。", "unity": "2022.3", "dependencies": { "com.cneicy.shrink-context-core": "0.1.0", - "com.cneicy.shrink-app-core": "0.1.2", + "com.cneicy.shrink-app-core": "0.1.3", "com.cneicy.shrink-eventbus": "2.0.0", "com.cysharp.unitask": "2.5.10", "com.unity.modules.jsonserialize": "1.0.0" @@ -22,5 +22,6 @@ "name": "cneicy", "url": "https://git.crash.work/ShrinkSDK" }, - "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter" + "documentationUrl": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter", + "changelogUrl": "https://git.crash.work/ShrinkSDK/ShrinkContext.AppAdapter/src/branch/main/CHANGELOG.md" }