@noego/wood 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dist/browser-test-setup.cjs +29 -0
- package/dist/browser-test-setup.cjs.map +1 -0
- package/dist/browser-test-setup.d.cts +8 -0
- package/dist/browser-test-setup.d.ts +8 -0
- package/dist/browser-test-setup.js +9 -0
- package/dist/browser-test-setup.js.map +1 -0
- package/dist/client/global_polyfill.d.cts +1 -0
- package/dist/client/global_polyfill.d.ts +1 -0
- package/dist/client/index.cjs +7 -0
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +7 -4
- package/dist/client/index.d.ts +7 -4
- package/dist/client/index.js +6 -1
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +97 -24
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +97 -24
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +9 -0
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +9 -0
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/config/config_parser.cjs +10 -0
- package/dist/config/config_parser.cjs.map +1 -1
- package/dist/config/config_parser.js +10 -0
- package/dist/config/config_parser.js.map +1 -1
- package/dist/fetch.cjs +38 -0
- package/dist/fetch.cjs.map +1 -0
- package/dist/fetch.d.cts +7 -0
- package/dist/fetch.d.ts +7 -0
- package/dist/fetch.js +12 -0
- package/dist/fetch.js.map +1 -0
- package/dist/frontend/frontend_page.cjs +3 -3
- package/dist/frontend/frontend_page.cjs.map +1 -1
- package/dist/frontend/frontend_page.d.cts +1 -1
- package/dist/frontend/frontend_page.d.ts +1 -1
- package/dist/frontend/frontend_page.js +3 -3
- package/dist/frontend/frontend_page.js.map +1 -1
- package/dist/frontend/frontend_surface.cjs +31 -17
- package/dist/frontend/frontend_surface.cjs.map +1 -1
- package/dist/frontend/frontend_surface.d.cts +12 -2
- package/dist/frontend/frontend_surface.d.ts +12 -2
- package/dist/frontend/frontend_surface.js +37 -17
- package/dist/frontend/frontend_surface.js.map +1 -1
- package/dist/frontend/index.d.cts +2 -0
- package/dist/frontend/index.d.ts +2 -0
- package/dist/index.cjs +5 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader/index.cjs +19 -2
- package/dist/loader/index.cjs.map +1 -1
- package/dist/loader/index.d.cts +1 -0
- package/dist/loader/index.d.ts +1 -0
- package/dist/loader/index.js +19 -1
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/loader_orchestration.cjs +82 -0
- package/dist/loader/loader_orchestration.cjs.map +1 -0
- package/dist/loader/loader_orchestration.d.cts +66 -0
- package/dist/loader/loader_orchestration.d.ts +66 -0
- package/dist/loader/loader_orchestration.js +51 -0
- package/dist/loader/loader_orchestration.js.map +1 -0
- package/dist/loader/loader_runner.cjs +46 -41
- package/dist/loader/loader_runner.cjs.map +1 -1
- package/dist/loader/loader_runner.d.cts +12 -6
- package/dist/loader/loader_runner.d.ts +12 -6
- package/dist/loader/loader_runner.js +50 -41
- package/dist/loader/loader_runner.js.map +1 -1
- package/dist/native_browser.types-BY2daUOS.d.ts +189 -0
- package/dist/native_browser.types-vWXtE4rM.d.cts +189 -0
- package/dist/navigation/index.cjs +15 -2
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +2 -0
- package/dist/navigation/index.d.ts +2 -0
- package/dist/navigation/index.js +8 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/runtime/get_container.cjs +2 -0
- package/dist/runtime/get_container.cjs.map +1 -1
- package/dist/runtime/get_container.js +2 -0
- package/dist/runtime/get_container.js.map +1 -1
- package/dist/runtime/index.cjs +5 -0
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/runtime_capabilities.cjs +35 -0
- package/dist/runtime/runtime_capabilities.cjs.map +1 -0
- package/dist/runtime/runtime_capabilities.d.cts +6 -0
- package/dist/runtime/runtime_capabilities.d.ts +6 -0
- package/dist/runtime/runtime_capabilities.js +11 -0
- package/dist/runtime/runtime_capabilities.js.map +1 -0
- package/dist/runtime/runtime_module.cjs +2 -0
- package/dist/runtime/runtime_module.cjs.map +1 -1
- package/dist/runtime/runtime_module.js +2 -0
- package/dist/runtime/runtime_module.js.map +1 -1
- package/dist/test-wood.cjs +150 -0
- package/dist/test-wood.cjs.map +1 -0
- package/dist/test-wood.d.cts +62 -0
- package/dist/test-wood.d.ts +62 -0
- package/dist/test-wood.js +115 -0
- package/dist/test-wood.js.map +1 -0
- package/dist/testing/browser.cjs +92 -163
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +14 -13
- package/dist/testing/browser.d.ts +14 -13
- package/dist/testing/browser.js +88 -162
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +2 -1
- package/dist/testing/create_controller_harness.d.ts +2 -1
- package/dist/testing/create_router_bridge.cjs +19 -15
- package/dist/testing/create_router_bridge.cjs.map +1 -1
- package/dist/testing/create_router_bridge.d.cts +8 -1
- package/dist/testing/create_router_bridge.d.ts +8 -1
- package/dist/testing/create_router_bridge.js +18 -15
- package/dist/testing/create_router_bridge.js.map +1 -1
- package/dist/testing/index.cjs +12 -4
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +11 -5
- package/dist/testing/index.d.ts +11 -5
- package/dist/testing/index.js +9 -3
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/native_browser/browser_pool.cjs +31 -0
- package/dist/testing/native_browser/browser_pool.cjs.map +1 -0
- package/dist/testing/native_browser/browser_pool.d.cts +1 -0
- package/dist/testing/native_browser/browser_pool.d.ts +1 -0
- package/dist/testing/native_browser/browser_pool.js +6 -0
- package/dist/testing/native_browser/browser_pool.js.map +1 -0
- package/dist/testing/native_browser/happy_dom_tab.cjs +107 -0
- package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -0
- package/dist/testing/native_browser/happy_dom_tab.d.cts +35 -0
- package/dist/testing/native_browser/happy_dom_tab.d.ts +35 -0
- package/dist/testing/native_browser/happy_dom_tab.js +73 -0
- package/dist/testing/native_browser/happy_dom_tab.js.map +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.cjs +29 -0
- package/dist/testing/native_browser/happy_dom_transfer.cjs.map +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.d.cts +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.d.ts +1 -0
- package/dist/testing/native_browser/happy_dom_transfer.js +5 -0
- package/dist/testing/native_browser/happy_dom_transfer.js.map +1 -0
- package/dist/testing/native_browser/host_transport.cjs +89 -0
- package/dist/testing/native_browser/host_transport.cjs.map +1 -0
- package/dist/testing/native_browser/host_transport.d.cts +44 -0
- package/dist/testing/native_browser/host_transport.d.ts +44 -0
- package/dist/testing/native_browser/host_transport.js +65 -0
- package/dist/testing/native_browser/host_transport.js.map +1 -0
- package/dist/testing/native_browser/native_browser.types.cjs +17 -0
- package/dist/testing/native_browser/native_browser.types.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser.types.d.cts +31 -0
- package/dist/testing/native_browser/native_browser.types.d.ts +31 -0
- package/dist/testing/native_browser/native_browser.types.js +1 -0
- package/dist/testing/native_browser/native_browser.types.js.map +1 -0
- package/dist/testing/native_browser/native_browser_environment.cjs +143 -0
- package/dist/testing/native_browser/native_browser_environment.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser_environment.d.cts +36 -0
- package/dist/testing/native_browser/native_browser_environment.d.ts +36 -0
- package/dist/testing/native_browser/native_browser_environment.js +109 -0
- package/dist/testing/native_browser/native_browser_environment.js.map +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.cjs +31 -0
- package/dist/testing/native_browser/native_browser_lifecycle.cjs.map +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.d.cts +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.d.ts +1 -0
- package/dist/testing/native_browser/native_browser_lifecycle.js +6 -0
- package/dist/testing/native_browser/native_browser_lifecycle.js.map +1 -0
- package/dist/testing/native_browser/playwright_tab.cjs +124 -0
- package/dist/testing/native_browser/playwright_tab.cjs.map +1 -0
- package/dist/testing/native_browser/playwright_tab.d.cts +35 -0
- package/dist/testing/native_browser/playwright_tab.d.ts +35 -0
- package/dist/testing/native_browser/playwright_tab.js +100 -0
- package/dist/testing/native_browser/playwright_tab.js.map +1 -0
- package/dist/testing/native_browser/renderer_compilation.cjs +149 -0
- package/dist/testing/native_browser/renderer_compilation.cjs.map +1 -0
- package/dist/testing/native_browser/renderer_compilation.d.cts +35 -0
- package/dist/testing/native_browser/renderer_compilation.d.ts +35 -0
- package/dist/testing/native_browser/renderer_compilation.js +115 -0
- package/dist/testing/native_browser/renderer_compilation.js.map +1 -0
- package/dist/testing/native_browser/renderer_entry.cjs +148 -0
- package/dist/testing/native_browser/renderer_entry.cjs.map +1 -0
- package/dist/testing/native_browser/renderer_entry.d.cts +13 -0
- package/dist/testing/native_browser/renderer_entry.d.ts +13 -0
- package/dist/testing/native_browser/renderer_entry.js +114 -0
- package/dist/testing/native_browser/renderer_entry.js.map +1 -0
- package/dist/testing/registration_only_root.cjs +61 -5
- package/dist/testing/registration_only_root.cjs.map +1 -1
- package/dist/testing/registration_only_root.d.cts +25 -4
- package/dist/testing/registration_only_root.d.ts +25 -4
- package/dist/testing/registration_only_root.js +60 -5
- package/dist/testing/registration_only_root.js.map +1 -1
- package/dist/testing/runner_importer.cjs +75 -0
- package/dist/testing/runner_importer.cjs.map +1 -0
- package/dist/testing/runner_importer.d.cts +8 -0
- package/dist/testing/runner_importer.d.ts +8 -0
- package/dist/testing/runner_importer.js +41 -0
- package/dist/testing/runner_importer.js.map +1 -0
- package/dist/testing/test_wood_config.cjs +387 -272
- package/dist/testing/test_wood_config.cjs.map +1 -1
- package/dist/testing/test_wood_config.d.cts +18 -123
- package/dist/testing/test_wood_config.d.ts +18 -123
- package/dist/testing/test_wood_config.js +396 -270
- package/dist/testing/test_wood_config.js.map +1 -1
- package/dist/testing/wood_config_source.cjs +12 -0
- package/dist/testing/wood_config_source.cjs.map +1 -1
- package/dist/testing/wood_config_source.d.cts +10 -0
- package/dist/testing/wood_config_source.d.ts +10 -0
- package/dist/testing/wood_config_source.js +12 -0
- package/dist/testing/wood_config_source.js.map +1 -1
- package/dist/testing/wood_env_renderer.cjs +28 -3
- package/dist/testing/wood_env_renderer.cjs.map +1 -1
- package/dist/testing/wood_env_renderer.d.cts +12 -1
- package/dist/testing/wood_env_renderer.d.ts +12 -1
- package/dist/testing/wood_env_renderer.js +28 -3
- package/dist/testing/wood_env_renderer.js.map +1 -1
- package/dist/testing/wood_lazy_runtime.cjs +109 -0
- package/dist/testing/wood_lazy_runtime.cjs.map +1 -0
- package/dist/testing/wood_lazy_runtime.d.cts +36 -0
- package/dist/testing/wood_lazy_runtime.d.ts +36 -0
- package/dist/testing/wood_lazy_runtime.js +84 -0
- package/dist/testing/wood_lazy_runtime.js.map +1 -0
- package/dist/testing/wood_test_selection.cjs +34 -55
- package/dist/testing/wood_test_selection.cjs.map +1 -1
- package/dist/testing/wood_test_selection.d.cts +18 -24
- package/dist/testing/wood_test_selection.d.ts +18 -24
- package/dist/testing/wood_test_selection.js +30 -54
- package/dist/testing/wood_test_selection.js.map +1 -1
- package/dist/testing-api.cjs +17 -0
- package/dist/testing-api.cjs.map +1 -0
- package/dist/testing-api.d.cts +18 -0
- package/dist/testing-api.d.ts +18 -0
- package/dist/testing-api.js +1 -0
- package/dist/testing-api.js.map +1 -0
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +4 -0
- package/dist/types/config.d.ts +4 -0
- package/docs/native-browser-testing.md +164 -0
- package/package.json +31 -5
- package/src/components/NavigationShell.browser.test.ts +10 -10
- package/src/components/NavigationShell.cleanup.browser.test.ts +57 -0
- package/src/components/NavigationShell.scopes.browser.test.ts +5 -5
- package/src/components/NavigationShell.svelte +50 -24
|
@@ -42,8 +42,8 @@ function bridgeSurface(surface, scope) {
|
|
|
42
42
|
function createFrontendPage(options) {
|
|
43
43
|
const { identity, controller, scope } = options;
|
|
44
44
|
let closed = false;
|
|
45
|
-
const bridgedInput = bridgeSurface(controller
|
|
46
|
-
const bridgedEvents = bridgeSurface(controller
|
|
45
|
+
const bridgedInput = bridgeSurface(controller?.input, scope);
|
|
46
|
+
const bridgedEvents = bridgeSurface(controller?.events, scope);
|
|
47
47
|
const layouts = (options.layouts ?? []).map((mounted) => {
|
|
48
48
|
const layoutInput = bridgeSurface(mounted.instance.input, mounted.scope);
|
|
49
49
|
const layoutEvents = bridgeSurface(mounted.instance.events, mounted.scope);
|
|
@@ -76,7 +76,7 @@ function createFrontendPage(options) {
|
|
|
76
76
|
identity,
|
|
77
77
|
get data() {
|
|
78
78
|
guard();
|
|
79
|
-
return controller
|
|
79
|
+
return controller?.data;
|
|
80
80
|
},
|
|
81
81
|
get input() {
|
|
82
82
|
guard();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/frontend/frontend_page.ts"],"sourcesContent":["/**\n * The public frontend page projection (18A D-FAS-05).\n *\n * `FrontendPage` exposes the production `data / input / events` contract of\n * the selected PageController — never the controller instance itself, never\n * private or IoC-only members. References are not cloned (reactivity is\n * preserved); `input`/`events` handlers are bridged so they execute under\n * the page scope's IoC execution context, exactly as the production\n * renderer runtime invokes them (they are plain data properties holding\n * arrow functions, which would otherwise run context-less from a test).\n */\nimport { ExecutionContext } from '@noego/ioc';\nimport { FrontendWorkError } from './work_registry.js';\n\n/**\n * The public projection of one mounted LAYOUT controller (18A D-FAS-05).\n *\n * Layout controllers are production page members — the views YAML binds\n * them, the renderer mounts them alongside the view controller, and a\n * `preserve: true` layout outlives page transitions. They therefore expose\n * the same public `data / input / events` contract as the page controller,\n * never their instance.\n */\nexport interface FrontendLayout<Data = unknown, Input = unknown, Events = unknown> {\n /** The layout view path as declared in the views source. */\n readonly path: string;\n /** The layout controller identity as declared in the views source. */\n readonly controller: string;\n /** Whether the views source declares this layout preservable. */\n readonly preserve: boolean;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n}\n\nexport interface FrontendPage<Data = unknown, Input = unknown, Events = unknown> {\n /** Canonical `window.page` production identity. */\n readonly identity: string;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n /** Every mounted layout controller of this page, outermost first. */\n readonly layouts: readonly FrontendLayout[];\n /**\n * The page's sole layout controller. Throws when the page declares none,\n * or more than one — use `layouts` and name the layout explicitly there.\n */\n readonly layout: FrontendLayout;\n close(): Promise<void>;\n}\n\ninterface PageControllerLike {\n data?: unknown;\n input?: unknown;\n events?: unknown;\n}\n\nfunction bridgeSurface(surface: unknown, scope: unknown): unknown {\n if (!surface || typeof surface !== 'object') return surface;\n // Proxy, not a copy: input/events surfaces are often class instances\n // (methods on the prototype) or reactive objects (accessor-backed state);\n // a proxy preserves both while wrapping function reads so handlers run\n // under the page scope's execution context.\n const wrapped = new WeakMap<object, unknown>();\n return new Proxy(surface as object, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver === undefined ? target : target);\n if (typeof value !== 'function') return value;\n const fn = value as (...args: unknown[]) => unknown;\n const cached = wrapped.get(fn);\n if (cached) return cached;\n const bridged = (...args: unknown[]) =>\n ExecutionContext.current() !== undefined\n ? fn.apply(target, args)\n : ExecutionContext.run(scope as never, () => fn.apply(target, args));\n wrapped.set(fn, bridged);\n return bridged;\n },\n });\n}\n\nexport interface FrontendPageHandle<Data = unknown, Input = unknown, Events = unknown> {\n page: FrontendPage<Data, Input, Events>;\n /** Mark the projection stale without disposing (navigation already disposed the scope). */\n invalidate: () => void;\n}\n\n/** One mounted layout controller with the scope its handlers run under. */\nexport interface MountedLayout {\n path: string;\n controller: string;\n preserve: boolean;\n instance: PageControllerLike;\n scope: unknown;\n}\n\nexport function createFrontendPage<Data, Input, Events>(options: {\n identity: string;\n controller
|
|
1
|
+
{"version":3,"sources":["../../src/frontend/frontend_page.ts"],"sourcesContent":["/**\n * The public frontend page projection (18A D-FAS-05).\n *\n * `FrontendPage` exposes the production `data / input / events` contract of\n * the selected PageController — never the controller instance itself, never\n * private or IoC-only members. References are not cloned (reactivity is\n * preserved); `input`/`events` handlers are bridged so they execute under\n * the page scope's IoC execution context, exactly as the production\n * renderer runtime invokes them (they are plain data properties holding\n * arrow functions, which would otherwise run context-less from a test).\n */\nimport { ExecutionContext } from '@noego/ioc';\nimport { FrontendWorkError } from './work_registry.js';\n\n/**\n * The public projection of one mounted LAYOUT controller (18A D-FAS-05).\n *\n * Layout controllers are production page members — the views YAML binds\n * them, the renderer mounts them alongside the view controller, and a\n * `preserve: true` layout outlives page transitions. They therefore expose\n * the same public `data / input / events` contract as the page controller,\n * never their instance.\n */\nexport interface FrontendLayout<Data = unknown, Input = unknown, Events = unknown> {\n /** The layout view path as declared in the views source. */\n readonly path: string;\n /** The layout controller identity as declared in the views source. */\n readonly controller: string;\n /** Whether the views source declares this layout preservable. */\n readonly preserve: boolean;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n}\n\nexport interface FrontendPage<Data = unknown, Input = unknown, Events = unknown> {\n /** Canonical `window.page` production identity. */\n readonly identity: string;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n /** Every mounted layout controller of this page, outermost first. */\n readonly layouts: readonly FrontendLayout[];\n /**\n * The page's sole layout controller. Throws when the page declares none,\n * or more than one — use `layouts` and name the layout explicitly there.\n */\n readonly layout: FrontendLayout;\n close(): Promise<void>;\n}\n\ninterface PageControllerLike {\n data?: unknown;\n input?: unknown;\n events?: unknown;\n}\n\nfunction bridgeSurface(surface: unknown, scope: unknown): unknown {\n if (!surface || typeof surface !== 'object') return surface;\n // Proxy, not a copy: input/events surfaces are often class instances\n // (methods on the prototype) or reactive objects (accessor-backed state);\n // a proxy preserves both while wrapping function reads so handlers run\n // under the page scope's execution context.\n const wrapped = new WeakMap<object, unknown>();\n return new Proxy(surface as object, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver === undefined ? target : target);\n if (typeof value !== 'function') return value;\n const fn = value as (...args: unknown[]) => unknown;\n const cached = wrapped.get(fn);\n if (cached) return cached;\n const bridged = (...args: unknown[]) =>\n ExecutionContext.current() !== undefined\n ? fn.apply(target, args)\n : ExecutionContext.run(scope as never, () => fn.apply(target, args));\n wrapped.set(fn, bridged);\n return bridged;\n },\n });\n}\n\nexport interface FrontendPageHandle<Data = unknown, Input = unknown, Events = unknown> {\n page: FrontendPage<Data, Input, Events>;\n /** Mark the projection stale without disposing (navigation already disposed the scope). */\n invalidate: () => void;\n}\n\n/** One mounted layout controller with the scope its handlers run under. */\nexport interface MountedLayout {\n path: string;\n controller: string;\n preserve: boolean;\n instance: PageControllerLike;\n scope: unknown;\n}\n\nexport function createFrontendPage<Data, Input, Events>(options: {\n identity: string;\n controller?: PageControllerLike;\n scope: unknown;\n layouts?: readonly MountedLayout[];\n onClose: () => Promise<void>;\n}): FrontendPageHandle<Data, Input, Events> {\n const { identity, controller, scope } = options;\n let closed = false;\n\n const bridgedInput = bridgeSurface(controller?.input, scope);\n const bridgedEvents = bridgeSurface(controller?.events, scope);\n\n const layouts: FrontendLayout[] = (options.layouts ?? []).map((mounted) => {\n const layoutInput = bridgeSurface(mounted.instance.input, mounted.scope);\n const layoutEvents = bridgeSurface(mounted.instance.events, mounted.scope);\n return {\n path: mounted.path,\n controller: mounted.controller,\n preserve: mounted.preserve,\n get data() {\n guard();\n return mounted.instance.data;\n },\n get input() {\n guard();\n return layoutInput;\n },\n get events() {\n guard();\n return layoutEvents;\n },\n };\n });\n\n const guard = (): void => {\n if (closed) {\n throw new FrontendWorkError(\n `page \"${identity}\" is closed; a closed/navigated-away page handle must not be used. ` +\n 'Re-open the page or use frontend.current().',\n );\n }\n };\n\n const page: FrontendPage<Data, Input, Events> = {\n identity,\n get data(): Data {\n guard();\n return controller?.data as Data;\n },\n get input(): Input {\n guard();\n return bridgedInput as Input;\n },\n get events(): Events | undefined {\n guard();\n return bridgedEvents as Events | undefined;\n },\n get layouts(): readonly FrontendLayout[] {\n guard();\n return layouts;\n },\n get layout(): FrontendLayout {\n guard();\n if (layouts.length === 1) return layouts[0];\n if (!layouts.length) {\n throw new FrontendWorkError(\n `page \"${identity}\" mounts no layout controller; its views entry binds none.`,\n );\n }\n throw new FrontendWorkError(\n `page \"${identity}\" mounts ${layouts.length} layout controllers ` +\n `(${layouts.map((entry) => entry.controller).join(', ')}); ` +\n 'use page.layouts and name the one you mean.',\n );\n },\n close: async () => {\n if (closed) return;\n closed = true;\n await options.onClose();\n },\n };\n\n return {\n page,\n invalidate: () => {\n closed = true;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAWA,iBAAiC;AACjC,2BAAkC;AA6ClC,SAAS,cAAc,SAAkB,OAAyB;AAChE,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AAKpD,QAAM,UAAU,oBAAI,QAAyB;AAC7C,SAAO,IAAI,MAAM,SAAmB;AAAA,IAClC,IAAI,QAAQ,UAAU,UAAU;AAC9B,YAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,aAAa,SAAY,SAAS,MAAM;AACpF,UAAI,OAAO,UAAU,WAAY,QAAO;AACxC,YAAM,KAAK;AACX,YAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,UAAI,OAAQ,QAAO;AACnB,YAAM,UAAU,IAAI,SAClB,4BAAiB,QAAQ,MAAM,SAC3B,GAAG,MAAM,QAAQ,IAAI,IACrB,4BAAiB,IAAI,OAAgB,MAAM,GAAG,MAAM,QAAQ,IAAI,CAAC;AACvE,cAAQ,IAAI,IAAI,OAAO;AACvB,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAiBO,SAAS,mBAAwC,SAMZ;AAC1C,QAAM,EAAE,UAAU,YAAY,MAAM,IAAI;AACxC,MAAI,SAAS;AAEb,QAAM,eAAe,cAAc,YAAY,OAAO,KAAK;AAC3D,QAAM,gBAAgB,cAAc,YAAY,QAAQ,KAAK;AAE7D,QAAM,WAA6B,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,YAAY;AACzE,UAAM,cAAc,cAAc,QAAQ,SAAS,OAAO,QAAQ,KAAK;AACvE,UAAM,eAAe,cAAc,QAAQ,SAAS,QAAQ,QAAQ,KAAK;AACzE,WAAO;AAAA,MACL,MAAM,QAAQ;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,UAAU,QAAQ;AAAA,MAClB,IAAI,OAAO;AACT,cAAM;AACN,eAAO,QAAQ,SAAS;AAAA,MAC1B;AAAA,MACA,IAAI,QAAQ;AACV,cAAM;AACN,eAAO;AAAA,MACT;AAAA,MACA,IAAI,SAAS;AACX,cAAM;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,MAAY;AACxB,QAAI,QAAQ;AACV,YAAM,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,MAEnB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAA0C;AAAA,IAC9C;AAAA,IACA,IAAI,OAAa;AACf,YAAM;AACN,aAAO,YAAY;AAAA,IACrB;AAAA,IACA,IAAI,QAAe;AACjB,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAA6B;AAC/B,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,UAAqC;AACvC,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAAyB;AAC3B,YAAM;AACN,UAAI,QAAQ,WAAW,EAAG,QAAO,QAAQ,CAAC;AAC1C,UAAI,CAAC,QAAQ,QAAQ;AACnB,cAAM,IAAI;AAAA,UACR,SAAS,QAAQ;AAAA,QACnB;AAAA,MACF;AACA,YAAM,IAAI;AAAA,QACR,SAAS,QAAQ,YAAY,QAAQ,MAAM,wBACvC,QAAQ,IAAI,CAAC,UAAU,MAAM,UAAU,EAAE,KAAK,IAAI,CAAC;AAAA,MAEzD;AAAA,IACF;AAAA,IACA,OAAO,YAAY;AACjB,UAAI,OAAQ;AACZ,eAAS;AACT,YAAM,QAAQ,QAAQ;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,YAAY,MAAM;AAChB,eAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
|
|
@@ -53,7 +53,7 @@ interface MountedLayout {
|
|
|
53
53
|
}
|
|
54
54
|
declare function createFrontendPage<Data, Input, Events>(options: {
|
|
55
55
|
identity: string;
|
|
56
|
-
controller
|
|
56
|
+
controller?: PageControllerLike;
|
|
57
57
|
scope: unknown;
|
|
58
58
|
layouts?: readonly MountedLayout[];
|
|
59
59
|
onClose: () => Promise<void>;
|
|
@@ -53,7 +53,7 @@ interface MountedLayout {
|
|
|
53
53
|
}
|
|
54
54
|
declare function createFrontendPage<Data, Input, Events>(options: {
|
|
55
55
|
identity: string;
|
|
56
|
-
controller
|
|
56
|
+
controller?: PageControllerLike;
|
|
57
57
|
scope: unknown;
|
|
58
58
|
layouts?: readonly MountedLayout[];
|
|
59
59
|
onClose: () => Promise<void>;
|
|
@@ -19,8 +19,8 @@ function bridgeSurface(surface, scope) {
|
|
|
19
19
|
function createFrontendPage(options) {
|
|
20
20
|
const { identity, controller, scope } = options;
|
|
21
21
|
let closed = false;
|
|
22
|
-
const bridgedInput = bridgeSurface(controller
|
|
23
|
-
const bridgedEvents = bridgeSurface(controller
|
|
22
|
+
const bridgedInput = bridgeSurface(controller?.input, scope);
|
|
23
|
+
const bridgedEvents = bridgeSurface(controller?.events, scope);
|
|
24
24
|
const layouts = (options.layouts ?? []).map((mounted) => {
|
|
25
25
|
const layoutInput = bridgeSurface(mounted.instance.input, mounted.scope);
|
|
26
26
|
const layoutEvents = bridgeSurface(mounted.instance.events, mounted.scope);
|
|
@@ -53,7 +53,7 @@ function createFrontendPage(options) {
|
|
|
53
53
|
identity,
|
|
54
54
|
get data() {
|
|
55
55
|
guard();
|
|
56
|
-
return controller
|
|
56
|
+
return controller?.data;
|
|
57
57
|
},
|
|
58
58
|
get input() {
|
|
59
59
|
guard();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/frontend/frontend_page.ts"],"sourcesContent":["/**\n * The public frontend page projection (18A D-FAS-05).\n *\n * `FrontendPage` exposes the production `data / input / events` contract of\n * the selected PageController — never the controller instance itself, never\n * private or IoC-only members. References are not cloned (reactivity is\n * preserved); `input`/`events` handlers are bridged so they execute under\n * the page scope's IoC execution context, exactly as the production\n * renderer runtime invokes them (they are plain data properties holding\n * arrow functions, which would otherwise run context-less from a test).\n */\nimport { ExecutionContext } from '@noego/ioc';\nimport { FrontendWorkError } from './work_registry.js';\n\n/**\n * The public projection of one mounted LAYOUT controller (18A D-FAS-05).\n *\n * Layout controllers are production page members — the views YAML binds\n * them, the renderer mounts them alongside the view controller, and a\n * `preserve: true` layout outlives page transitions. They therefore expose\n * the same public `data / input / events` contract as the page controller,\n * never their instance.\n */\nexport interface FrontendLayout<Data = unknown, Input = unknown, Events = unknown> {\n /** The layout view path as declared in the views source. */\n readonly path: string;\n /** The layout controller identity as declared in the views source. */\n readonly controller: string;\n /** Whether the views source declares this layout preservable. */\n readonly preserve: boolean;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n}\n\nexport interface FrontendPage<Data = unknown, Input = unknown, Events = unknown> {\n /** Canonical `window.page` production identity. */\n readonly identity: string;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n /** Every mounted layout controller of this page, outermost first. */\n readonly layouts: readonly FrontendLayout[];\n /**\n * The page's sole layout controller. Throws when the page declares none,\n * or more than one — use `layouts` and name the layout explicitly there.\n */\n readonly layout: FrontendLayout;\n close(): Promise<void>;\n}\n\ninterface PageControllerLike {\n data?: unknown;\n input?: unknown;\n events?: unknown;\n}\n\nfunction bridgeSurface(surface: unknown, scope: unknown): unknown {\n if (!surface || typeof surface !== 'object') return surface;\n // Proxy, not a copy: input/events surfaces are often class instances\n // (methods on the prototype) or reactive objects (accessor-backed state);\n // a proxy preserves both while wrapping function reads so handlers run\n // under the page scope's execution context.\n const wrapped = new WeakMap<object, unknown>();\n return new Proxy(surface as object, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver === undefined ? target : target);\n if (typeof value !== 'function') return value;\n const fn = value as (...args: unknown[]) => unknown;\n const cached = wrapped.get(fn);\n if (cached) return cached;\n const bridged = (...args: unknown[]) =>\n ExecutionContext.current() !== undefined\n ? fn.apply(target, args)\n : ExecutionContext.run(scope as never, () => fn.apply(target, args));\n wrapped.set(fn, bridged);\n return bridged;\n },\n });\n}\n\nexport interface FrontendPageHandle<Data = unknown, Input = unknown, Events = unknown> {\n page: FrontendPage<Data, Input, Events>;\n /** Mark the projection stale without disposing (navigation already disposed the scope). */\n invalidate: () => void;\n}\n\n/** One mounted layout controller with the scope its handlers run under. */\nexport interface MountedLayout {\n path: string;\n controller: string;\n preserve: boolean;\n instance: PageControllerLike;\n scope: unknown;\n}\n\nexport function createFrontendPage<Data, Input, Events>(options: {\n identity: string;\n controller
|
|
1
|
+
{"version":3,"sources":["../../src/frontend/frontend_page.ts"],"sourcesContent":["/**\n * The public frontend page projection (18A D-FAS-05).\n *\n * `FrontendPage` exposes the production `data / input / events` contract of\n * the selected PageController — never the controller instance itself, never\n * private or IoC-only members. References are not cloned (reactivity is\n * preserved); `input`/`events` handlers are bridged so they execute under\n * the page scope's IoC execution context, exactly as the production\n * renderer runtime invokes them (they are plain data properties holding\n * arrow functions, which would otherwise run context-less from a test).\n */\nimport { ExecutionContext } from '@noego/ioc';\nimport { FrontendWorkError } from './work_registry.js';\n\n/**\n * The public projection of one mounted LAYOUT controller (18A D-FAS-05).\n *\n * Layout controllers are production page members — the views YAML binds\n * them, the renderer mounts them alongside the view controller, and a\n * `preserve: true` layout outlives page transitions. They therefore expose\n * the same public `data / input / events` contract as the page controller,\n * never their instance.\n */\nexport interface FrontendLayout<Data = unknown, Input = unknown, Events = unknown> {\n /** The layout view path as declared in the views source. */\n readonly path: string;\n /** The layout controller identity as declared in the views source. */\n readonly controller: string;\n /** Whether the views source declares this layout preservable. */\n readonly preserve: boolean;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n}\n\nexport interface FrontendPage<Data = unknown, Input = unknown, Events = unknown> {\n /** Canonical `window.page` production identity. */\n readonly identity: string;\n readonly data: Data;\n readonly input: Input;\n readonly events: Events | undefined;\n /** Every mounted layout controller of this page, outermost first. */\n readonly layouts: readonly FrontendLayout[];\n /**\n * The page's sole layout controller. Throws when the page declares none,\n * or more than one — use `layouts` and name the layout explicitly there.\n */\n readonly layout: FrontendLayout;\n close(): Promise<void>;\n}\n\ninterface PageControllerLike {\n data?: unknown;\n input?: unknown;\n events?: unknown;\n}\n\nfunction bridgeSurface(surface: unknown, scope: unknown): unknown {\n if (!surface || typeof surface !== 'object') return surface;\n // Proxy, not a copy: input/events surfaces are often class instances\n // (methods on the prototype) or reactive objects (accessor-backed state);\n // a proxy preserves both while wrapping function reads so handlers run\n // under the page scope's execution context.\n const wrapped = new WeakMap<object, unknown>();\n return new Proxy(surface as object, {\n get(target, property, receiver) {\n const value = Reflect.get(target, property, receiver === undefined ? target : target);\n if (typeof value !== 'function') return value;\n const fn = value as (...args: unknown[]) => unknown;\n const cached = wrapped.get(fn);\n if (cached) return cached;\n const bridged = (...args: unknown[]) =>\n ExecutionContext.current() !== undefined\n ? fn.apply(target, args)\n : ExecutionContext.run(scope as never, () => fn.apply(target, args));\n wrapped.set(fn, bridged);\n return bridged;\n },\n });\n}\n\nexport interface FrontendPageHandle<Data = unknown, Input = unknown, Events = unknown> {\n page: FrontendPage<Data, Input, Events>;\n /** Mark the projection stale without disposing (navigation already disposed the scope). */\n invalidate: () => void;\n}\n\n/** One mounted layout controller with the scope its handlers run under. */\nexport interface MountedLayout {\n path: string;\n controller: string;\n preserve: boolean;\n instance: PageControllerLike;\n scope: unknown;\n}\n\nexport function createFrontendPage<Data, Input, Events>(options: {\n identity: string;\n controller?: PageControllerLike;\n scope: unknown;\n layouts?: readonly MountedLayout[];\n onClose: () => Promise<void>;\n}): FrontendPageHandle<Data, Input, Events> {\n const { identity, controller, scope } = options;\n let closed = false;\n\n const bridgedInput = bridgeSurface(controller?.input, scope);\n const bridgedEvents = bridgeSurface(controller?.events, scope);\n\n const layouts: FrontendLayout[] = (options.layouts ?? []).map((mounted) => {\n const layoutInput = bridgeSurface(mounted.instance.input, mounted.scope);\n const layoutEvents = bridgeSurface(mounted.instance.events, mounted.scope);\n return {\n path: mounted.path,\n controller: mounted.controller,\n preserve: mounted.preserve,\n get data() {\n guard();\n return mounted.instance.data;\n },\n get input() {\n guard();\n return layoutInput;\n },\n get events() {\n guard();\n return layoutEvents;\n },\n };\n });\n\n const guard = (): void => {\n if (closed) {\n throw new FrontendWorkError(\n `page \"${identity}\" is closed; a closed/navigated-away page handle must not be used. ` +\n 'Re-open the page or use frontend.current().',\n );\n }\n };\n\n const page: FrontendPage<Data, Input, Events> = {\n identity,\n get data(): Data {\n guard();\n return controller?.data as Data;\n },\n get input(): Input {\n guard();\n return bridgedInput as Input;\n },\n get events(): Events | undefined {\n guard();\n return bridgedEvents as Events | undefined;\n },\n get layouts(): readonly FrontendLayout[] {\n guard();\n return layouts;\n },\n get layout(): FrontendLayout {\n guard();\n if (layouts.length === 1) return layouts[0];\n if (!layouts.length) {\n throw new FrontendWorkError(\n `page \"${identity}\" mounts no layout controller; its views entry binds none.`,\n );\n }\n throw new FrontendWorkError(\n `page \"${identity}\" mounts ${layouts.length} layout controllers ` +\n `(${layouts.map((entry) => entry.controller).join(', ')}); ` +\n 'use page.layouts and name the one you mean.',\n );\n },\n close: async () => {\n if (closed) return;\n closed = true;\n await options.onClose();\n },\n };\n\n return {\n page,\n invalidate: () => {\n closed = true;\n },\n };\n}\n"],"mappings":"AAWA,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AA6ClC,SAAS,cAAc,SAAkB,OAAyB;AAChE,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AAKpD,QAAM,UAAU,oBAAI,QAAyB;AAC7C,SAAO,IAAI,MAAM,SAAmB;AAAA,IAClC,IAAI,QAAQ,UAAU,UAAU;AAC9B,YAAM,QAAQ,QAAQ,IAAI,QAAQ,UAAU,aAAa,SAAY,SAAS,MAAM;AACpF,UAAI,OAAO,UAAU,WAAY,QAAO;AACxC,YAAM,KAAK;AACX,YAAM,SAAS,QAAQ,IAAI,EAAE;AAC7B,UAAI,OAAQ,QAAO;AACnB,YAAM,UAAU,IAAI,SAClB,iBAAiB,QAAQ,MAAM,SAC3B,GAAG,MAAM,QAAQ,IAAI,IACrB,iBAAiB,IAAI,OAAgB,MAAM,GAAG,MAAM,QAAQ,IAAI,CAAC;AACvE,cAAQ,IAAI,IAAI,OAAO;AACvB,aAAO;AAAA,IACT;AAAA,EACF,CAAC;AACH;AAiBO,SAAS,mBAAwC,SAMZ;AAC1C,QAAM,EAAE,UAAU,YAAY,MAAM,IAAI;AACxC,MAAI,SAAS;AAEb,QAAM,eAAe,cAAc,YAAY,OAAO,KAAK;AAC3D,QAAM,gBAAgB,cAAc,YAAY,QAAQ,KAAK;AAE7D,QAAM,WAA6B,QAAQ,WAAW,CAAC,GAAG,IAAI,CAAC,YAAY;AACzE,UAAM,cAAc,cAAc,QAAQ,SAAS,OAAO,QAAQ,KAAK;AACvE,UAAM,eAAe,cAAc,QAAQ,SAAS,QAAQ,QAAQ,KAAK;AACzE,WAAO;AAAA,MACL,MAAM,QAAQ;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,UAAU,QAAQ;AAAA,MAClB,IAAI,OAAO;AACT,cAAM;AACN,eAAO,QAAQ,SAAS;AAAA,MAC1B;AAAA,MACA,IAAI,QAAQ;AACV,cAAM;AACN,eAAO;AAAA,MACT;AAAA,MACA,IAAI,SAAS;AACX,cAAM;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,QAAQ,MAAY;AACxB,QAAI,QAAQ;AACV,YAAM,IAAI;AAAA,QACR,SAAS,QAAQ;AAAA,MAEnB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAA0C;AAAA,IAC9C;AAAA,IACA,IAAI,OAAa;AACf,YAAM;AACN,aAAO,YAAY;AAAA,IACrB;AAAA,IACA,IAAI,QAAe;AACjB,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAA6B;AAC/B,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,UAAqC;AACvC,YAAM;AACN,aAAO;AAAA,IACT;AAAA,IACA,IAAI,SAAyB;AAC3B,YAAM;AACN,UAAI,QAAQ,WAAW,EAAG,QAAO,QAAQ,CAAC;AAC1C,UAAI,CAAC,QAAQ,QAAQ;AACnB,cAAM,IAAI;AAAA,UACR,SAAS,QAAQ;AAAA,QACnB;AAAA,MACF;AACA,YAAM,IAAI;AAAA,QACR,SAAS,QAAQ,YAAY,QAAQ,MAAM,wBACvC,QAAQ,IAAI,CAAC,UAAU,MAAM,UAAU,EAAE,KAAK,IAAI,CAAC;AAAA,MAEzD;AAAA,IACF;AAAA,IACA,OAAO,YAAY;AACjB,UAAI,OAAQ;AACZ,eAAS;AACT,YAAM,QAAQ,QAAQ;AAAA,IACxB;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,YAAY,MAAM;AAChB,eAAS;AAAA,IACX;AAAA,EACF;AACF;","names":[]}
|
|
@@ -24,8 +24,10 @@ __export(frontend_surface_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(frontend_surface_exports);
|
|
25
25
|
var import_ioc = require("@noego/ioc");
|
|
26
26
|
var import_layout_preservation = require("../navigation/layout_preservation.js");
|
|
27
|
+
var import_navigation_identity = require("../navigation/navigation_identity.js");
|
|
27
28
|
var import_cleanup = require("../controller/cleanup.js");
|
|
28
29
|
var import_controller_lifecycle = require("../controller/controller_lifecycle.js");
|
|
30
|
+
var import_loader_orchestration = require("../loader/loader_orchestration.js");
|
|
29
31
|
var import_page_catalog = require("./page_catalog.js");
|
|
30
32
|
var import_work_registry = require("./work_registry.js");
|
|
31
33
|
var import_frontend_lifecycle = require("./frontend_lifecycle.js");
|
|
@@ -53,14 +55,9 @@ function createFrontendSurface(options) {
|
|
|
53
55
|
};
|
|
54
56
|
const disposeLocalController = (instance) => instance && typeof instance === "object" && locallyOwnedControllers.has(instance) ? (0, import_controller_lifecycle.disposeControllerLifecycle)(instance) : Promise.resolve();
|
|
55
57
|
const currentOwner = () => active ? `page ${active.identity}` : "application";
|
|
56
|
-
async function constructPage(view, params, query, preserved = []) {
|
|
58
|
+
async function constructPage(view, params, query, preserved = [], preservedDepth = 0) {
|
|
57
59
|
const identity = (0, import_page_catalog.pageIdentity)(view);
|
|
58
|
-
|
|
59
|
-
throw new import_page_catalog.PageCatalogError(
|
|
60
|
-
`page "${identity}" declares no controller in its view definition; the Node application slice drives public PageController input and needs one.`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const ControllerClass = await options.resolveController(view.controller, view);
|
|
60
|
+
const ControllerClass = view.controller ? await options.resolveController(view.controller, view) : void 0;
|
|
64
61
|
const scope = rendererRoot.extend();
|
|
65
62
|
const lifecycle = new import_frontend_lifecycle.FrontendLifecycleOwner(registry, `page ${identity}`);
|
|
66
63
|
const rollback = [
|
|
@@ -70,9 +67,15 @@ function createFrontendSurface(options) {
|
|
|
70
67
|
try {
|
|
71
68
|
scope.registerFunction?.(import_frontend_lifecycle.FrontendLifecycleOwner, () => lifecycle, { loadAs: import_ioc.LoadAs.Scoped });
|
|
72
69
|
await import_ioc.ExecutionContext.run(scope, () => options.configurePageScope?.(scope, view, { kind: "page", controller: view.controller }));
|
|
73
|
-
registerControllerUnlessBound(scope, ControllerClass);
|
|
70
|
+
if (ControllerClass) registerControllerUnlessBound(scope, ControllerClass);
|
|
71
|
+
const route = { params, query };
|
|
72
|
+
const loaderSet = options.resolveLoaders ? await options.resolveLoaders(view) : import_loader_orchestration.NO_LOADERS;
|
|
73
|
+
const loaded = await import_ioc.ExecutionContext.run(
|
|
74
|
+
scope,
|
|
75
|
+
() => (0, import_loader_orchestration.runViewLoaders)(loaderSet, (0, import_loader_orchestration.createLoaderRequest)(identity, route), preservedDepth)
|
|
76
|
+
);
|
|
74
77
|
const layouts = [...preserved];
|
|
75
|
-
for (let index =
|
|
78
|
+
for (let index = preservedDepth; index < view.layouts.length; index += 1) {
|
|
76
79
|
const layoutIdentity = view.layoutControllers?.[index];
|
|
77
80
|
if (!layoutIdentity) continue;
|
|
78
81
|
const preserve = view.layoutPreserve?.[index] === true;
|
|
@@ -97,6 +100,17 @@ function createFrontendSurface(options) {
|
|
|
97
100
|
);
|
|
98
101
|
markController(instance, LayoutClass);
|
|
99
102
|
rollback.push(() => import_ioc.ExecutionContext.run(layoutScope, () => disposeLocalController(instance)));
|
|
103
|
+
if (typeof instance.initialize === "function") {
|
|
104
|
+
await registry.track(
|
|
105
|
+
Promise.resolve(
|
|
106
|
+
import_ioc.ExecutionContext.run(
|
|
107
|
+
layoutScope,
|
|
108
|
+
() => (0, import_loader_orchestration.initializeController)(instance, (0, import_loader_orchestration.initializeArgument)(route, loaded.layouts[index] ?? null))
|
|
109
|
+
)
|
|
110
|
+
).then(() => void 0),
|
|
111
|
+
{ label: `initialize layout ${layoutIdentity}`, kind: "initialize", owner: `page ${identity}` }
|
|
112
|
+
);
|
|
113
|
+
}
|
|
100
114
|
layouts.push({
|
|
101
115
|
index,
|
|
102
116
|
path: view.layouts[index],
|
|
@@ -108,18 +122,18 @@ function createFrontendSurface(options) {
|
|
|
108
122
|
ownLifecycle
|
|
109
123
|
});
|
|
110
124
|
}
|
|
111
|
-
const controller = await import_ioc.ExecutionContext.run(
|
|
125
|
+
const controller = ControllerClass ? await import_ioc.ExecutionContext.run(
|
|
112
126
|
scope,
|
|
113
127
|
() => scope.get(ControllerClass)
|
|
114
|
-
);
|
|
115
|
-
markController(controller, ControllerClass);
|
|
128
|
+
) : void 0;
|
|
129
|
+
if (controller) markController(controller, ControllerClass);
|
|
116
130
|
rollback.push(() => import_ioc.ExecutionContext.run(scope, () => disposeLocalController(controller)));
|
|
117
|
-
if (typeof controller
|
|
131
|
+
if (typeof controller?.initialize === "function") {
|
|
118
132
|
await registry.track(
|
|
119
133
|
Promise.resolve(
|
|
120
134
|
import_ioc.ExecutionContext.run(
|
|
121
135
|
scope,
|
|
122
|
-
() =>
|
|
136
|
+
() => (0, import_loader_orchestration.initializeController)(controller, (0, import_loader_orchestration.initializeArgument)(route, loaded.view))
|
|
123
137
|
)
|
|
124
138
|
).then(() => void 0),
|
|
125
139
|
{ label: `initialize ${identity}`, kind: "initialize", owner: `page ${identity}` }
|
|
@@ -134,7 +148,7 @@ function createFrontendSurface(options) {
|
|
|
134
148
|
await disposePage(identity);
|
|
135
149
|
}
|
|
136
150
|
});
|
|
137
|
-
return { identity, view, scope, lifecycle, handle, controller, layouts };
|
|
151
|
+
return { identity, mountKey: (0, import_navigation_identity.createNavigationMountKey)(identity, params), view, scope, lifecycle, handle, controller, layouts };
|
|
138
152
|
} catch (error) {
|
|
139
153
|
try {
|
|
140
154
|
await (0, import_cleanup.cleanupAll)(rollback.reverse(), "Wood page rollback failed");
|
|
@@ -185,7 +199,7 @@ function createFrontendSurface(options) {
|
|
|
185
199
|
entry.options ?? {}
|
|
186
200
|
) : 0;
|
|
187
201
|
const carried = previous ? previous.layouts.filter((layout) => layout.index < depth) : [];
|
|
188
|
-
const next = await constructPage(view, entry.params, entry.query, carried);
|
|
202
|
+
const next = await constructPage(view, entry.params, entry.query, carried, depth);
|
|
189
203
|
if (disposed) {
|
|
190
204
|
await endPage(next, depth);
|
|
191
205
|
return;
|
|
@@ -197,7 +211,7 @@ function createFrontendSurface(options) {
|
|
|
197
211
|
}
|
|
198
212
|
const unsubscribe = navigation.subscribe((entry) => {
|
|
199
213
|
if (!entry || transitioning || disposed) return;
|
|
200
|
-
if (active && entry.page === active.
|
|
214
|
+
if (active && (0, import_navigation_identity.createNavigationMountKey)(entry.page, entry.params) === active.mountKey) return;
|
|
201
215
|
if (!active) return;
|
|
202
216
|
const transition = navigationQueue.then(() => transitionTo(entry));
|
|
203
217
|
navigationQueue = transition.catch(() => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/frontend/frontend_surface.ts"],"sourcesContent":["/**\n * The Node frontend application surface (spec 18 §12–13, 18A D-FAS-01/02).\n *\n * frontend.open({ page, params, query }) — explicit initial page\n * frontend.current() — production navigation result\n * frontend.act(fn) / frontend.settle() — owned-work fixed-point drains\n * frontend.errors — observed owned-work failures\n *\n * Runtime honesty (D-FAS-02): this is the plain-Node tier. It proves\n * explicit PageController behavior, $state/$derived reachable through it,\n * backend calls, events, and navigation identity/lifecycle. It does not\n * create a Svelte component/root context and therefore proves no $effect,\n * mount/unmount, or DOM behavior — those belong to the happy-dom tier.\n *\n * Isolation: every environment owns its own Navigation instance, renderer\n * root, transport, and work registry. Nothing here writes globalThis and\n * nothing calls the production client getters — two environments run\n * concurrently without cross-talk. A controller that calls the module-level\n * `go()`/`getNavigation()` free functions bypasses its environment; this\n * tier requires constructor-injected `Navigation`.\n */\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport { Navigation, type NavigationEntry } from '../navigation/index.js';\nimport { computePreservedLayoutDepth } from '../navigation/layout_preservation.js';\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { disposeControllerLifecycle, guardControllerLifecycleOnce } from '../controller/controller_lifecycle.js';\nimport type { ViewDef } from '../types/index.cjs';\nimport { PageCatalog, PageCatalogError, pageIdentity } from './page_catalog.js';\nimport { WorkRegistry, FrontendWorkError, type WorkFailure } from './work_registry.js';\nimport { FrontendLifecycleOwner, type FrontendLifecycle } from './frontend_lifecycle.js';\nimport { createFrontendPage, type FrontendPage, type FrontendPageHandle, type MountedLayout } from './frontend_page.js';\n\nexport interface FrontendOpenOptions {\n /** Required production page identity (`window.page`, or unambiguous leaf). */\n page: string;\n params?: Record<string, string>;\n query?: Record<string, string>;\n}\n\nexport interface FrontendApplicationSurface {\n open(options: FrontendOpenOptions): Promise<FrontendPage>;\n current(): FrontendPage | null;\n act(action: () => unknown | Promise<unknown>): Promise<void>;\n settle(): Promise<void>;\n readonly errors: readonly WorkFailure[];\n /** The environment-local production Navigation instance. */\n readonly navigation: Navigation;\n /** Internal: dispose the active page and application lifecycles. */\n dispose(): Promise<void>;\n}\n\ninterface ScopeLike {\n extend(): ScopeLike;\n get(token: unknown): Promise<unknown>;\n registerClass?(target: unknown, options?: unknown): void;\n registerFunction?(token: unknown, factory: () => unknown, options?: unknown): void;\n registerValue?(token: unknown, value: unknown): void;\n dispose?(): Promise<void>;\n}\n\n/**\n * A mounted layout controller and the scope that owns it. A layout declared\n * `preserve: true` owns a DEDICATED scope that survives page transitions\n * (production parity: `createControllerSet`); every other layout shares the\n * page scope and dies with the page.\n */\ninterface ActiveLayout extends MountedLayout {\n index: number;\n /** Non-null only for a preserved layout that owns its scope. */\n ownScope: ScopeLike | null;\n /**\n * A preserved layout outlives the page that built it, so it cannot borrow\n * that page's lifecycle owner — it owns one, disposed when the layout is\n * finally dropped.\n */\n ownLifecycle: FrontendLifecycleOwner | null;\n}\n\ninterface ActivePage {\n identity: string;\n view: ViewDef;\n scope: ScopeLike;\n lifecycle: FrontendLifecycleOwner;\n handle: FrontendPageHandle;\n controller: unknown;\n layouts: ActiveLayout[];\n}\n\nexport interface PageScopeContext {\n kind: 'page' | 'layout';\n /** Layout index within the view's layout stack (layout scopes only). */\n layoutIndex?: number;\n /** Controller identity bound to this scope. */\n controller: string;\n}\n\nexport interface CreateFrontendSurfaceOptions {\n /** The selected renderer aperture. */\n catalog: PageCatalog;\n /** Fresh renderer root for this environment (separate from the main root). */\n rendererRoot: ScopeLike;\n registry: WorkRegistry;\n /** Environment-local Navigation (also registered on the renderer root). */\n navigation: Navigation;\n /** Resolve a production controller identity to its class. */\n resolveController: (identity: string, view: ViewDef) => Promise<new (...args: never[]) => unknown>;\n /** Per-pass reactive flush hooks from the active runtime adapter (Node: none). */\n flushHooks?: ReadonlyArray<() => void | Promise<void>>;\n /**\n * Register per-scope services on a newly created scope (bridge client,\n * etc.). Called for the page scope, and once for each PRESERVED layout\n * that owns a dedicated scope — `context.kind` says which, so a caller\n * that tracks \"the page scope\" does not mistake a layout scope for it.\n */\n configurePageScope?: (\n scope: ScopeLike,\n view: ViewDef,\n context: PageScopeContext,\n ) => void | Promise<void>;\n}\n\n/**\n * Plain (undecorated) controller classes are registered on their scope so\n * they resolve — unless the hierarchy already decided how the token resolves\n * (a renderer-root substitution of the controller class), which keeps\n * winning inside the page scope.\n */\nfunction registerControllerUnlessBound(scope: ScopeLike, ControllerClass: unknown): void {\n const registrable = scope as { isRegistered?(token: unknown): boolean };\n if (typeof registrable.isRegistered === 'function' && registrable.isRegistered(ControllerClass)) return;\n if (typeof scope.registerClass === 'function') scope.registerClass(ControllerClass);\n}\n\nexport function createFrontendSurface(options: CreateFrontendSurfaceOptions): FrontendApplicationSurface {\n const { catalog, rendererRoot, registry, navigation } = options;\n const flushHooks = options.flushHooks ?? [];\n const application = new FrontendLifecycleOwner(registry, 'application');\n\n let active: ActivePage | null = null;\n let transitioning = false;\n let disposed = false;\n let navigationQueue: Promise<void> = Promise.resolve();\n let closing: Promise<void> | undefined;\n const retainedPageScopes = new Map<ScopeLike, FrontendLifecycleOwner>();\n const locallyOwnedControllers = new WeakSet<object>();\n const markController = (instance: unknown, token: unknown) => {\n const root = rendererRoot as ScopeLike & { isRegistered?(token: unknown): boolean };\n if (!root.isRegistered?.(token) && instance && typeof instance === 'object') locallyOwnedControllers.add(instance);\n guardControllerLifecycleOnce(instance);\n };\n const disposeLocalController = (instance: unknown) => instance && typeof instance === 'object' && locallyOwnedControllers.has(instance)\n ? disposeControllerLifecycle(instance) : Promise.resolve();\n\n const currentOwner = (): string => (active ? `page ${active.identity}` : 'application');\n\n async function constructPage(\n view: ViewDef,\n params: Record<string, string>,\n query: Record<string, string>,\n preserved: ActiveLayout[] = [],\n ): Promise<ActivePage> {\n const identity = pageIdentity(view);\n if (!view.controller) {\n throw new PageCatalogError(\n `page \"${identity}\" declares no controller in its view definition; ` +\n 'the Node application slice drives public PageController input and needs one.',\n );\n }\n\n const ControllerClass = await options.resolveController(view.controller, view);\n const scope = rendererRoot.extend();\n const lifecycle = new FrontendLifecycleOwner(registry, `page ${identity}`);\n\n const rollback: Array<() => unknown | Promise<unknown>> = [\n () => ExecutionContext.run(scope as never, () => scope.dispose?.()),\n () => lifecycle.dispose(),\n ];\n try {\n // The page lifecycle is injectable so controllers can register\n // deliberately detached finite work (D-FAS-01 §2.2). Scoped, not\n // Singleton: the lifecycle belongs to this page scope alone — a\n // Singleton would store at the hierarchy root and leak the first\n // page's lifecycle into every later page.\n scope.registerFunction?.(FrontendLifecycleOwner, () => lifecycle, { loadAs: LoadAs.Scoped });\n await ExecutionContext.run(scope as never, () => options.configurePageScope?.(scope, view, { kind: 'page', controller: view.controller! }));\n registerControllerUnlessBound(scope, ControllerClass);\n\n // Layout controllers mount BEFORE the view controller (production\n // ordering in `createControllerSet`): a page controller may legitimately\n // read state a layout controller published during construction.\n const layouts: ActiveLayout[] = [...preserved];\n for (let index = preserved.length; index < view.layouts.length; index += 1) {\n const layoutIdentity = view.layoutControllers?.[index];\n if (!layoutIdentity) continue;\n const preserve = view.layoutPreserve?.[index] === true;\n const LayoutClass = await options.resolveController(layoutIdentity, view);\n const ownScope = preserve ? rendererRoot.extend() : null;\n const ownLifecycle = ownScope\n ? new FrontendLifecycleOwner(registry, `layout ${layoutIdentity}`)\n : null;\n const layoutScope = ownScope ?? scope;\n if (ownScope) {\n rollback.push(() => ExecutionContext.run(ownScope as never, () => ownScope.dispose?.()));\n rollback.push(() => ownLifecycle!.dispose());\n ownScope.registerFunction?.(FrontendLifecycleOwner, () => ownLifecycle!, { loadAs: LoadAs.Scoped });\n await ExecutionContext.run(ownScope as never, () => options.configurePageScope?.(ownScope, view, {\n kind: 'layout',\n layoutIndex: index,\n controller: layoutIdentity,\n }));\n }\n registerControllerUnlessBound(layoutScope, LayoutClass);\n const instance = (await ExecutionContext.run(layoutScope as never, () =>\n layoutScope.get(LayoutClass),\n )) as Record<string, unknown>;\n markController(instance, LayoutClass);\n rollback.push(() => ExecutionContext.run(layoutScope as never, () => disposeLocalController(instance)));\n layouts.push({\n index,\n path: view.layouts[index],\n controller: layoutIdentity,\n preserve,\n instance,\n scope: layoutScope,\n ownScope,\n ownLifecycle,\n });\n }\n\n const controller = (await ExecutionContext.run(scope as never, () =>\n scope.get(ControllerClass),\n )) as Record<string, unknown>;\n\n markController(controller, ControllerClass);\n rollback.push(() => ExecutionContext.run(scope as never, () => disposeLocalController(controller)));\n\n if (typeof controller.initialize === 'function') {\n await registry.track(\n Promise.resolve(\n ExecutionContext.run(scope as never, () =>\n (controller.initialize as (load: unknown) => unknown)({ params, query }),\n ),\n ).then(() => undefined),\n { label: `initialize ${identity}`, kind: 'initialize', owner: `page ${identity}` },\n );\n }\n\n const handle = createFrontendPage({\n identity,\n controller,\n scope,\n layouts,\n onClose: async () => {\n await disposePage(identity);\n },\n });\n\n return { identity, view, scope, lifecycle, handle, controller, layouts };\n } catch (error) {\n try { await cleanupAll(rollback.reverse(), 'Wood page rollback failed'); }\n catch (cleanupError) { throw Object.assign(new Error('Wood page construction and cleanup failed'), { cause: error, errors: [error, cleanupError] }); }\n throw error;\n }\n }\n\n /**\n * End a page: the page lifecycle owner first (abort signal, owned\n * resources), then the IoC scopes — scope disposal is what runs each\n * controller's production `dispose()`/`destroy()` hook, so nothing here\n * calls those directly. Layouts in the preserved prefix keep running:\n * they belong to the next page now.\n */\n async function endPage(page: ActivePage, preservedDepth = 0): Promise<void> {\n page.handle.invalidate();\n // Production teardown order (NavigationShell.disposeControllerSet): the\n // page controller's hook, then each ending layout's, then the lifecycle\n // owner and the IoC scopes. The hooks are once-guarded, so the scope\n // disposal below does not run them a second time.\n const steps: Array<() => unknown | Promise<unknown>> = [\n () => ExecutionContext.run(page.scope as never, () => disposeLocalController(page.controller)),\n ];\n for (const layout of page.layouts) {\n if (layout.index < preservedDepth) continue;\n steps.push(() => ExecutionContext.run(layout.scope as never, () => disposeLocalController(layout.instance)));\n steps.push(() => layout.ownLifecycle?.dispose());\n if (layout.ownScope) steps.push(() => ExecutionContext.run(layout.ownScope as never, () => layout.ownScope!.dispose?.()));\n }\n const carriesPageScope = page.layouts.some(layout => layout.index < preservedDepth && layout.scope === page.scope);\n if (carriesPageScope) retainedPageScopes.set(page.scope, page.lifecycle);\n else {\n steps.push(() => page.lifecycle.dispose());\n steps.push(() => ExecutionContext.run(page.scope as never, () => page.scope.dispose?.()));\n }\n for (const [scope, lifecycle] of retainedPageScopes) {\n if (active?.layouts.some(layout => layout.scope === scope)) continue;\n retainedPageScopes.delete(scope);\n steps.push(() => lifecycle.dispose());\n steps.push(() => ExecutionContext.run(scope as never, () => scope.dispose?.()));\n }\n await cleanupAll(steps, 'Wood page cleanup failed');\n }\n\n async function disposePage(identity: string): Promise<void> {\n if (!active || active.identity !== identity) return;\n const ended = active;\n active = null;\n await endPage(ended);\n }\n\n async function transitionTo(entry: NavigationEntry): Promise<void> {\n if (disposed) return;\n // Resolve the destination through the selected aperture — navigation\n // outside it fails without widening the environment.\n const view = catalog.resolve(entry.page);\n const previous = active;\n // Preserved layouts carry over as live instances — the transition never\n // reconstructs them (production `preserve: true` semantics).\n // Same preservation rule the renderer's NavigationShell applies.\n const depth = previous\n ? computePreservedLayoutDepth(\n { layoutPaths: previous.view.layouts, layoutControllers: previous.view.layoutControllers, layoutPreserve: previous.view.layoutPreserve },\n { layoutPaths: view.layouts, layoutControllers: view.layoutControllers, layoutPreserve: view.layoutPreserve },\n entry.options ?? {},\n )\n : 0;\n const carried = previous ? previous.layouts.filter((layout) => layout.index < depth) : [];\n const next = await constructPage(view, entry.params, entry.query, carried);\n if (disposed) {\n await endPage(next, depth);\n return;\n }\n active = next;\n if (previous) {\n await endPage(previous, depth);\n }\n }\n\n // Production navigation drives page transitions: a controller calling its\n // injected Navigation.go()/replace() inside the aperture triggers one\n // tracked finite navigation transition (18A D-FAS-01 §2.2).\n const unsubscribe = navigation.subscribe((entry) => {\n if (!entry || transitioning || disposed) return;\n if (active && entry.page === active.identity) return;\n if (!active) return; // initial open() seeds navigation itself\n const transition = navigationQueue.then(() => transitionTo(entry));\n navigationQueue = transition.catch(() => {});\n registry.track(transition, {\n label: `navigate ${active.identity} -> ${entry.page}`,\n kind: 'navigation',\n owner: currentOwner(),\n });\n });\n\n return {\n navigation,\n\n async open(openOptions: FrontendOpenOptions): Promise<FrontendPage> {\n if (disposed || transitioning) throw new PageCatalogError('Wood frontend is closed or opening a page');\n if (!openOptions || typeof openOptions.page !== 'string' || !openOptions.page.length) {\n throw new PageCatalogError('frontend.open() requires an explicit { page } identity');\n }\n if (active) {\n throw new PageCatalogError(\n `frontend.open() is test bootstrap; page \"${active.identity}\" is already open. ` +\n 'Drive production navigation for subsequent route changes, or close() the page first.',\n );\n }\n const view = catalog.resolve(openOptions.page);\n const params = openOptions.params ?? {};\n const query = openOptions.query ?? {};\n\n transitioning = true;\n try {\n // Seed production navigation state BEFORE construction: controllers\n // and their services legitimately read the current route (params,\n // query) during construction/initialize, exactly as in production\n // where the route exists before the page loads.\n navigation.replace(pageIdentity(view), params, query);\n const opened = await constructPage(view, params, query);\n if (disposed) { await endPage(opened); throw new PageCatalogError('Wood frontend was disposed during page construction'); }\n active = opened;\n } finally {\n transitioning = false;\n }\n return active.handle.page;\n },\n\n current(): FrontendPage | null {\n return active ? active.handle.page : null;\n },\n\n async act(action: () => unknown | Promise<unknown>): Promise<void> {\n void registry.track(\n Promise.resolve().then(() => action()).then(() => undefined),\n { label: 'act()', kind: 'tracked', owner: currentOwner() },\n ).catch(() => {\n // The failure is recorded by the registry; drain reports it with\n // its owner rather than losing the aggregate view.\n });\n await registry.drain(flushHooks);\n },\n\n async settle(): Promise<void> {\n await registry.drain(flushHooks);\n },\n\n get errors(): readonly WorkFailure[] {\n return registry.errors();\n },\n\n dispose(): Promise<void> {\n if (closing) return closing;\n disposed = true;\n unsubscribe();\n const ended = active;\n active = null;\n return closing = cleanupAll([\n () => ended ? endPage(ended) : undefined,\n () => application.dispose(),\n ], 'Wood frontend cleanup failed');\n },\n };\n}\n\nexport type { FrontendLifecycle };\nexport { FrontendWorkError };\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/frontend/frontend_surface.ts"],"sourcesContent":["/**\n * The Node frontend application surface (spec 18 §12–13, 18A D-FAS-01/02).\n *\n * frontend.open({ page, params, query }) — explicit initial page\n * frontend.current() — production navigation result\n * frontend.act(fn) / frontend.settle() — owned-work fixed-point drains\n * frontend.errors — observed owned-work failures\n *\n * Runtime honesty (D-FAS-02): this is the plain-Node tier. It proves\n * explicit PageController behavior, $state/$derived reachable through it,\n * backend calls, events, and navigation identity/lifecycle. It does not\n * create a Svelte component/root context and therefore proves no $effect,\n * mount/unmount, or DOM behavior — those belong to the happy-dom tier.\n *\n * Isolation: every environment owns its own Navigation instance, renderer\n * root, transport, and work registry. Nothing here writes globalThis and\n * nothing calls the production client getters — two environments run\n * concurrently without cross-talk. A controller that calls the module-level\n * `go()`/`getNavigation()` free functions bypasses its environment; this\n * tier requires constructor-injected `Navigation`.\n */\nimport { ExecutionContext, LoadAs } from '@noego/ioc';\nimport { Navigation, type NavigationEntry } from '../navigation/index.js';\nimport { computePreservedLayoutDepth } from '../navigation/layout_preservation.js';\nimport { createNavigationMountKey } from '../navigation/navigation_identity.js';\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { disposeControllerLifecycle, guardControllerLifecycleOnce } from '../controller/controller_lifecycle.js';\nimport {\n createLoaderRequest,\n initializeArgument,\n initializeController,\n NO_LOADERS,\n runViewLoaders,\n type ViewLoaderSet,\n} from '../loader/loader_orchestration.js';\nimport type { ViewDef } from '../types/index.cjs';\nimport { PageCatalog, PageCatalogError, pageIdentity } from './page_catalog.js';\nimport { WorkRegistry, FrontendWorkError, type WorkFailure } from './work_registry.js';\nimport { FrontendLifecycleOwner, type FrontendLifecycle } from './frontend_lifecycle.js';\nimport { createFrontendPage, type FrontendPage, type FrontendPageHandle, type MountedLayout } from './frontend_page.js';\n\nexport interface FrontendOpenOptions {\n /** Required production page identity (`window.page`, or unambiguous leaf). */\n page: string;\n params?: Record<string, string>;\n query?: Record<string, string>;\n}\n\nexport interface FrontendApplicationSurface {\n open(options: FrontendOpenOptions): Promise<FrontendPage>;\n current(): FrontendPage | null;\n act(action: () => unknown | Promise<unknown>): Promise<void>;\n settle(): Promise<void>;\n readonly errors: readonly WorkFailure[];\n /** The environment-local production Navigation instance. */\n readonly navigation: Navigation;\n /** Internal: dispose the active page and application lifecycles. */\n dispose(): Promise<void>;\n}\n\ninterface ScopeLike {\n extend(): ScopeLike;\n get(token: unknown): Promise<unknown>;\n registerClass?(target: unknown, options?: unknown): void;\n registerFunction?(token: unknown, factory: () => unknown, options?: unknown): void;\n registerValue?(token: unknown, value: unknown): void;\n dispose?(): Promise<void>;\n}\n\n/**\n * A mounted layout controller and the scope that owns it. A layout declared\n * `preserve: true` owns a DEDICATED scope that survives page transitions\n * (production parity: `createControllerSet`); every other layout shares the\n * page scope and dies with the page.\n */\ninterface ActiveLayout extends MountedLayout {\n index: number;\n /** Non-null only for a preserved layout that owns its scope. */\n ownScope: ScopeLike | null;\n /**\n * A preserved layout outlives the page that built it, so it cannot borrow\n * that page's lifecycle owner — it owns one, disposed when the layout is\n * finally dropped.\n */\n ownLifecycle: FrontendLifecycleOwner | null;\n}\n\ninterface ActivePage {\n identity: string;\n mountKey: string;\n view: ViewDef;\n scope: ScopeLike;\n lifecycle: FrontendLifecycleOwner;\n handle: FrontendPageHandle;\n controller: unknown;\n layouts: ActiveLayout[];\n}\n\nexport interface PageScopeContext {\n kind: 'page' | 'layout';\n /** Layout index within the view's layout stack (layout scopes only). */\n layoutIndex?: number;\n /** Controller identity when the source binds one (static pages omit it). */\n controller?: string;\n}\n\nexport interface CreateFrontendSurfaceOptions {\n /** The selected renderer aperture. */\n catalog: PageCatalog;\n /** Fresh renderer root for this environment (separate from the main root). */\n rendererRoot: ScopeLike;\n registry: WorkRegistry;\n /** Environment-local Navigation (also registered on the renderer root). */\n navigation: Navigation;\n /** Resolve a production controller identity to its class. */\n resolveController: (identity: string, view: ViewDef) => Promise<new (...args: never[]) => unknown>;\n /**\n * Resolve the page/layout data loaders of a view (companion `.load`\n * sources, already discovered and imported by the caller). Omitted: no\n * loaders — controllers initialize with `{ params, query }` exactly as\n * before. Loaders run under the page scope on every open/navigation,\n * layouts (non-preserved) first, then the view, BEFORE controllers exist.\n */\n resolveLoaders?: (view: ViewDef) => ViewLoaderSet | Promise<ViewLoaderSet>;\n /** Per-pass reactive flush hooks from the active runtime adapter (Node: none). */\n flushHooks?: ReadonlyArray<() => void | Promise<void>>;\n /**\n * Register per-scope services on a newly created scope (bridge client,\n * etc.). Called for the page scope, and once for each PRESERVED layout\n * that owns a dedicated scope — `context.kind` says which, so a caller\n * that tracks \"the page scope\" does not mistake a layout scope for it.\n */\n configurePageScope?: (\n scope: ScopeLike,\n view: ViewDef,\n context: PageScopeContext,\n ) => void | Promise<void>;\n}\n\n/**\n * Plain (undecorated) controller classes are registered on their scope so\n * they resolve — unless the hierarchy already decided how the token resolves\n * (a renderer-root substitution of the controller class), which keeps\n * winning inside the page scope.\n */\nfunction registerControllerUnlessBound(scope: ScopeLike, ControllerClass: unknown): void {\n const registrable = scope as { isRegistered?(token: unknown): boolean };\n if (typeof registrable.isRegistered === 'function' && registrable.isRegistered(ControllerClass)) return;\n if (typeof scope.registerClass === 'function') scope.registerClass(ControllerClass);\n}\n\nexport function createFrontendSurface(options: CreateFrontendSurfaceOptions): FrontendApplicationSurface {\n const { catalog, rendererRoot, registry, navigation } = options;\n const flushHooks = options.flushHooks ?? [];\n const application = new FrontendLifecycleOwner(registry, 'application');\n\n let active: ActivePage | null = null;\n let transitioning = false;\n let disposed = false;\n let navigationQueue: Promise<void> = Promise.resolve();\n let closing: Promise<void> | undefined;\n const retainedPageScopes = new Map<ScopeLike, FrontendLifecycleOwner>();\n const locallyOwnedControllers = new WeakSet<object>();\n const markController = (instance: unknown, token: unknown) => {\n const root = rendererRoot as ScopeLike & { isRegistered?(token: unknown): boolean };\n if (!root.isRegistered?.(token) && instance && typeof instance === 'object') locallyOwnedControllers.add(instance);\n guardControllerLifecycleOnce(instance);\n };\n const disposeLocalController = (instance: unknown) => instance && typeof instance === 'object' && locallyOwnedControllers.has(instance)\n ? disposeControllerLifecycle(instance) : Promise.resolve();\n\n const currentOwner = (): string => (active ? `page ${active.identity}` : 'application');\n\n async function constructPage(\n view: ViewDef,\n params: Record<string, string>,\n query: Record<string, string>,\n preserved: ActiveLayout[] = [],\n preservedDepth = 0,\n ): Promise<ActivePage> {\n const identity = pageIdentity(view);\n const ControllerClass = view.controller\n ? await options.resolveController(view.controller, view) : undefined;\n const scope = rendererRoot.extend();\n const lifecycle = new FrontendLifecycleOwner(registry, `page ${identity}`);\n\n const rollback: Array<() => unknown | Promise<unknown>> = [\n () => ExecutionContext.run(scope as never, () => scope.dispose?.()),\n () => lifecycle.dispose(),\n ];\n try {\n // The page lifecycle is injectable so controllers can register\n // deliberately detached finite work (D-FAS-01 §2.2). Scoped, not\n // Singleton: the lifecycle belongs to this page scope alone — a\n // Singleton would store at the hierarchy root and leak the first\n // page's lifecycle into every later page.\n scope.registerFunction?.(FrontendLifecycleOwner, () => lifecycle, { loadAs: LoadAs.Scoped });\n await ExecutionContext.run(scope as never, () => options.configurePageScope?.(scope, view, { kind: 'page', controller: view.controller }));\n if (ControllerClass) registerControllerUnlessBound(scope, ControllerClass);\n\n // Data loaders run first (shared orchestration): the route is already\n // seeded, no controller exists yet, and a failure rolls the page back.\n const route = { params, query };\n const loaderSet = options.resolveLoaders ? await options.resolveLoaders(view) : NO_LOADERS;\n const loaded = await ExecutionContext.run(scope as never, () =>\n runViewLoaders(loaderSet, createLoaderRequest(identity, route), preservedDepth),\n );\n\n // Layout controllers mount BEFORE the view controller (production\n // ordering in `createControllerSet`): a page controller may legitimately\n // read state a layout controller published during construction.\n const layouts: ActiveLayout[] = [...preserved];\n for (let index = preservedDepth; index < view.layouts.length; index += 1) {\n const layoutIdentity = view.layoutControllers?.[index];\n if (!layoutIdentity) continue;\n const preserve = view.layoutPreserve?.[index] === true;\n const LayoutClass = await options.resolveController(layoutIdentity, view);\n const ownScope = preserve ? rendererRoot.extend() : null;\n const ownLifecycle = ownScope\n ? new FrontendLifecycleOwner(registry, `layout ${layoutIdentity}`)\n : null;\n const layoutScope = ownScope ?? scope;\n if (ownScope) {\n rollback.push(() => ExecutionContext.run(ownScope as never, () => ownScope.dispose?.()));\n rollback.push(() => ownLifecycle!.dispose());\n ownScope.registerFunction?.(FrontendLifecycleOwner, () => ownLifecycle!, { loadAs: LoadAs.Scoped });\n await ExecutionContext.run(ownScope as never, () => options.configurePageScope?.(ownScope, view, {\n kind: 'layout',\n layoutIndex: index,\n controller: layoutIdentity,\n }));\n }\n registerControllerUnlessBound(layoutScope, LayoutClass);\n const instance = (await ExecutionContext.run(layoutScope as never, () =>\n layoutScope.get(LayoutClass),\n )) as Record<string, unknown>;\n markController(instance, LayoutClass);\n rollback.push(() => ExecutionContext.run(layoutScope as never, () => disposeLocalController(instance)));\n if (typeof instance.initialize === 'function') {\n await registry.track(\n Promise.resolve(\n ExecutionContext.run(layoutScope as never, () =>\n initializeController(instance, initializeArgument(route, loaded.layouts[index] ?? null)),\n ),\n ).then(() => undefined),\n { label: `initialize layout ${layoutIdentity}`, kind: 'initialize', owner: `page ${identity}` },\n );\n }\n layouts.push({\n index,\n path: view.layouts[index],\n controller: layoutIdentity,\n preserve,\n instance,\n scope: layoutScope,\n ownScope,\n ownLifecycle,\n });\n }\n\n const controller = ControllerClass ? (await ExecutionContext.run(scope as never, () =>\n scope.get(ControllerClass),\n )) as Record<string, unknown> : undefined;\n\n if (controller) markController(controller, ControllerClass);\n rollback.push(() => ExecutionContext.run(scope as never, () => disposeLocalController(controller)));\n\n if (typeof controller?.initialize === 'function') {\n await registry.track(\n Promise.resolve(\n ExecutionContext.run(scope as never, () =>\n initializeController(controller, initializeArgument(route, loaded.view)),\n ),\n ).then(() => undefined),\n { label: `initialize ${identity}`, kind: 'initialize', owner: `page ${identity}` },\n );\n }\n\n const handle = createFrontendPage({\n identity,\n controller,\n scope,\n layouts,\n onClose: async () => {\n await disposePage(identity);\n },\n });\n\n return { identity, mountKey: createNavigationMountKey(identity, params), view, scope, lifecycle, handle, controller, layouts };\n } catch (error) {\n try { await cleanupAll(rollback.reverse(), 'Wood page rollback failed'); }\n catch (cleanupError) { throw Object.assign(new Error('Wood page construction and cleanup failed'), { cause: error, errors: [error, cleanupError] }); }\n throw error;\n }\n }\n\n /**\n * End a page: the page lifecycle owner first (abort signal, owned\n * resources), then the IoC scopes — scope disposal is what runs each\n * controller's production `dispose()`/`destroy()` hook, so nothing here\n * calls those directly. Layouts in the preserved prefix keep running:\n * they belong to the next page now.\n */\n async function endPage(page: ActivePage, preservedDepth = 0): Promise<void> {\n page.handle.invalidate();\n // Production teardown order (NavigationShell.disposeControllerSet): the\n // page controller's hook, then each ending layout's, then the lifecycle\n // owner and the IoC scopes. The hooks are once-guarded, so the scope\n // disposal below does not run them a second time.\n const steps: Array<() => unknown | Promise<unknown>> = [\n () => ExecutionContext.run(page.scope as never, () => disposeLocalController(page.controller)),\n ];\n for (const layout of page.layouts) {\n if (layout.index < preservedDepth) continue;\n steps.push(() => ExecutionContext.run(layout.scope as never, () => disposeLocalController(layout.instance)));\n steps.push(() => layout.ownLifecycle?.dispose());\n if (layout.ownScope) steps.push(() => ExecutionContext.run(layout.ownScope as never, () => layout.ownScope!.dispose?.()));\n }\n const carriesPageScope = page.layouts.some(layout => layout.index < preservedDepth && layout.scope === page.scope);\n if (carriesPageScope) retainedPageScopes.set(page.scope, page.lifecycle);\n else {\n steps.push(() => page.lifecycle.dispose());\n steps.push(() => ExecutionContext.run(page.scope as never, () => page.scope.dispose?.()));\n }\n for (const [scope, lifecycle] of retainedPageScopes) {\n if (active?.layouts.some(layout => layout.scope === scope)) continue;\n retainedPageScopes.delete(scope);\n steps.push(() => lifecycle.dispose());\n steps.push(() => ExecutionContext.run(scope as never, () => scope.dispose?.()));\n }\n await cleanupAll(steps, 'Wood page cleanup failed');\n }\n\n async function disposePage(identity: string): Promise<void> {\n if (!active || active.identity !== identity) return;\n const ended = active;\n active = null;\n await endPage(ended);\n }\n\n async function transitionTo(entry: NavigationEntry): Promise<void> {\n if (disposed) return;\n // Resolve the destination through the selected aperture — navigation\n // outside it fails without widening the environment.\n const view = catalog.resolve(entry.page);\n const previous = active;\n // Preserved layouts carry over as live instances — the transition never\n // reconstructs them (production `preserve: true` semantics).\n // Same preservation rule the renderer's NavigationShell applies.\n const depth = previous\n ? computePreservedLayoutDepth(\n { layoutPaths: previous.view.layouts, layoutControllers: previous.view.layoutControllers, layoutPreserve: previous.view.layoutPreserve },\n { layoutPaths: view.layouts, layoutControllers: view.layoutControllers, layoutPreserve: view.layoutPreserve },\n entry.options ?? {},\n )\n : 0;\n const carried = previous ? previous.layouts.filter((layout) => layout.index < depth) : [];\n const next = await constructPage(view, entry.params, entry.query, carried, depth);\n if (disposed) {\n await endPage(next, depth);\n return;\n }\n active = next;\n if (previous) {\n await endPage(previous, depth);\n }\n }\n\n // Production navigation drives page transitions: a controller calling its\n // injected Navigation.go()/replace() inside the aperture triggers one\n // tracked finite navigation transition (18A D-FAS-01 §2.2).\n const unsubscribe = navigation.subscribe((entry) => {\n if (!entry || transitioning || disposed) return;\n if (active && createNavigationMountKey(entry.page, entry.params) === active.mountKey) return;\n if (!active) return; // initial open() seeds navigation itself\n const transition = navigationQueue.then(() => transitionTo(entry));\n navigationQueue = transition.catch(() => {});\n registry.track(transition, {\n label: `navigate ${active.identity} -> ${entry.page}`,\n kind: 'navigation',\n owner: currentOwner(),\n });\n });\n\n return {\n navigation,\n\n async open(openOptions: FrontendOpenOptions): Promise<FrontendPage> {\n if (disposed || transitioning) throw new PageCatalogError('Wood frontend is closed or opening a page');\n if (!openOptions || typeof openOptions.page !== 'string' || !openOptions.page.length) {\n throw new PageCatalogError('frontend.open() requires an explicit { page } identity');\n }\n if (active) {\n throw new PageCatalogError(\n `frontend.open() is test bootstrap; page \"${active.identity}\" is already open. ` +\n 'Drive production navigation for subsequent route changes, or close() the page first.',\n );\n }\n const view = catalog.resolve(openOptions.page);\n const params = openOptions.params ?? {};\n const query = openOptions.query ?? {};\n\n transitioning = true;\n try {\n // Seed production navigation state BEFORE construction: controllers\n // and their services legitimately read the current route (params,\n // query) during construction/initialize, exactly as in production\n // where the route exists before the page loads.\n navigation.replace(pageIdentity(view), params, query);\n const opened = await constructPage(view, params, query);\n if (disposed) { await endPage(opened); throw new PageCatalogError('Wood frontend was disposed during page construction'); }\n active = opened;\n } finally {\n transitioning = false;\n }\n return active.handle.page;\n },\n\n current(): FrontendPage | null {\n return active ? active.handle.page : null;\n },\n\n async act(action: () => unknown | Promise<unknown>): Promise<void> {\n void registry.track(\n Promise.resolve().then(() => action()).then(() => undefined),\n { label: 'act()', kind: 'tracked', owner: currentOwner() },\n ).catch(() => {\n // The failure is recorded by the registry; drain reports it with\n // its owner rather than losing the aggregate view.\n });\n await registry.drain(flushHooks);\n },\n\n async settle(): Promise<void> {\n await registry.drain(flushHooks);\n },\n\n get errors(): readonly WorkFailure[] {\n return registry.errors();\n },\n\n dispose(): Promise<void> {\n if (closing) return closing;\n disposed = true;\n unsubscribe();\n const ended = active;\n active = null;\n return closing = cleanupAll([\n () => ended ? endPage(ended) : undefined,\n () => application.dispose(),\n ], 'Wood frontend cleanup failed');\n },\n };\n}\n\nexport type { FrontendLifecycle };\nexport { FrontendWorkError };\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Navigation } from '../navigation/index.cjs';
|
|
2
|
+
import { ViewLoaderSet } from '../loader/loader_orchestration.cjs';
|
|
2
3
|
import { ViewDef } from '../types/views.cjs';
|
|
3
4
|
import { PageCatalog } from './page_catalog.cjs';
|
|
4
5
|
import { WorkRegistry, WorkFailure } from './work_registry.cjs';
|
|
@@ -7,6 +8,7 @@ export { FrontendLifecycle } from './frontend_lifecycle.cjs';
|
|
|
7
8
|
import { FrontendPage } from './frontend_page.cjs';
|
|
8
9
|
import '../navigation/layout_preservation.cjs';
|
|
9
10
|
import '../navigation/automation_introspection.cjs';
|
|
11
|
+
import '../types/context.cjs';
|
|
10
12
|
|
|
11
13
|
interface FrontendOpenOptions {
|
|
12
14
|
/** Required production page identity (`window.page`, or unambiguous leaf). */
|
|
@@ -37,8 +39,8 @@ interface PageScopeContext {
|
|
|
37
39
|
kind: 'page' | 'layout';
|
|
38
40
|
/** Layout index within the view's layout stack (layout scopes only). */
|
|
39
41
|
layoutIndex?: number;
|
|
40
|
-
/** Controller identity
|
|
41
|
-
controller
|
|
42
|
+
/** Controller identity when the source binds one (static pages omit it). */
|
|
43
|
+
controller?: string;
|
|
42
44
|
}
|
|
43
45
|
interface CreateFrontendSurfaceOptions {
|
|
44
46
|
/** The selected renderer aperture. */
|
|
@@ -50,6 +52,14 @@ interface CreateFrontendSurfaceOptions {
|
|
|
50
52
|
navigation: Navigation;
|
|
51
53
|
/** Resolve a production controller identity to its class. */
|
|
52
54
|
resolveController: (identity: string, view: ViewDef) => Promise<new (...args: never[]) => unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the page/layout data loaders of a view (companion `.load`
|
|
57
|
+
* sources, already discovered and imported by the caller). Omitted: no
|
|
58
|
+
* loaders — controllers initialize with `{ params, query }` exactly as
|
|
59
|
+
* before. Loaders run under the page scope on every open/navigation,
|
|
60
|
+
* layouts (non-preserved) first, then the view, BEFORE controllers exist.
|
|
61
|
+
*/
|
|
62
|
+
resolveLoaders?: (view: ViewDef) => ViewLoaderSet | Promise<ViewLoaderSet>;
|
|
53
63
|
/** Per-pass reactive flush hooks from the active runtime adapter (Node: none). */
|
|
54
64
|
flushHooks?: ReadonlyArray<() => void | Promise<void>>;
|
|
55
65
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Navigation } from '../navigation/index.js';
|
|
2
|
+
import { ViewLoaderSet } from '../loader/loader_orchestration.js';
|
|
2
3
|
import { ViewDef } from '../types/views.js';
|
|
3
4
|
import { PageCatalog } from './page_catalog.js';
|
|
4
5
|
import { WorkRegistry, WorkFailure } from './work_registry.js';
|
|
@@ -7,6 +8,7 @@ export { FrontendLifecycle } from './frontend_lifecycle.js';
|
|
|
7
8
|
import { FrontendPage } from './frontend_page.js';
|
|
8
9
|
import '../navigation/layout_preservation.js';
|
|
9
10
|
import '../navigation/automation_introspection.js';
|
|
11
|
+
import '../types/context.js';
|
|
10
12
|
|
|
11
13
|
interface FrontendOpenOptions {
|
|
12
14
|
/** Required production page identity (`window.page`, or unambiguous leaf). */
|
|
@@ -37,8 +39,8 @@ interface PageScopeContext {
|
|
|
37
39
|
kind: 'page' | 'layout';
|
|
38
40
|
/** Layout index within the view's layout stack (layout scopes only). */
|
|
39
41
|
layoutIndex?: number;
|
|
40
|
-
/** Controller identity
|
|
41
|
-
controller
|
|
42
|
+
/** Controller identity when the source binds one (static pages omit it). */
|
|
43
|
+
controller?: string;
|
|
42
44
|
}
|
|
43
45
|
interface CreateFrontendSurfaceOptions {
|
|
44
46
|
/** The selected renderer aperture. */
|
|
@@ -50,6 +52,14 @@ interface CreateFrontendSurfaceOptions {
|
|
|
50
52
|
navigation: Navigation;
|
|
51
53
|
/** Resolve a production controller identity to its class. */
|
|
52
54
|
resolveController: (identity: string, view: ViewDef) => Promise<new (...args: never[]) => unknown>;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the page/layout data loaders of a view (companion `.load`
|
|
57
|
+
* sources, already discovered and imported by the caller). Omitted: no
|
|
58
|
+
* loaders — controllers initialize with `{ params, query }` exactly as
|
|
59
|
+
* before. Loaders run under the page scope on every open/navigation,
|
|
60
|
+
* layouts (non-preserved) first, then the view, BEFORE controllers exist.
|
|
61
|
+
*/
|
|
62
|
+
resolveLoaders?: (view: ViewDef) => ViewLoaderSet | Promise<ViewLoaderSet>;
|
|
53
63
|
/** Per-pass reactive flush hooks from the active runtime adapter (Node: none). */
|
|
54
64
|
flushHooks?: ReadonlyArray<() => void | Promise<void>>;
|
|
55
65
|
/**
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { ExecutionContext, LoadAs } from "@noego/ioc";
|
|
2
2
|
import { computePreservedLayoutDepth } from "../navigation/layout_preservation.js";
|
|
3
|
+
import { createNavigationMountKey } from "../navigation/navigation_identity.js";
|
|
3
4
|
import { cleanupAll } from "../controller/cleanup.js";
|
|
4
5
|
import { disposeControllerLifecycle, guardControllerLifecycleOnce } from "../controller/controller_lifecycle.js";
|
|
6
|
+
import {
|
|
7
|
+
createLoaderRequest,
|
|
8
|
+
initializeArgument,
|
|
9
|
+
initializeController,
|
|
10
|
+
NO_LOADERS,
|
|
11
|
+
runViewLoaders
|
|
12
|
+
} from "../loader/loader_orchestration.js";
|
|
5
13
|
import { PageCatalogError, pageIdentity } from "./page_catalog.js";
|
|
6
14
|
import { FrontendWorkError } from "./work_registry.js";
|
|
7
15
|
import { FrontendLifecycleOwner } from "./frontend_lifecycle.js";
|
|
@@ -29,14 +37,9 @@ function createFrontendSurface(options) {
|
|
|
29
37
|
};
|
|
30
38
|
const disposeLocalController = (instance) => instance && typeof instance === "object" && locallyOwnedControllers.has(instance) ? disposeControllerLifecycle(instance) : Promise.resolve();
|
|
31
39
|
const currentOwner = () => active ? `page ${active.identity}` : "application";
|
|
32
|
-
async function constructPage(view, params, query, preserved = []) {
|
|
40
|
+
async function constructPage(view, params, query, preserved = [], preservedDepth = 0) {
|
|
33
41
|
const identity = pageIdentity(view);
|
|
34
|
-
|
|
35
|
-
throw new PageCatalogError(
|
|
36
|
-
`page "${identity}" declares no controller in its view definition; the Node application slice drives public PageController input and needs one.`
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
const ControllerClass = await options.resolveController(view.controller, view);
|
|
42
|
+
const ControllerClass = view.controller ? await options.resolveController(view.controller, view) : void 0;
|
|
40
43
|
const scope = rendererRoot.extend();
|
|
41
44
|
const lifecycle = new FrontendLifecycleOwner(registry, `page ${identity}`);
|
|
42
45
|
const rollback = [
|
|
@@ -46,9 +49,15 @@ function createFrontendSurface(options) {
|
|
|
46
49
|
try {
|
|
47
50
|
scope.registerFunction?.(FrontendLifecycleOwner, () => lifecycle, { loadAs: LoadAs.Scoped });
|
|
48
51
|
await ExecutionContext.run(scope, () => options.configurePageScope?.(scope, view, { kind: "page", controller: view.controller }));
|
|
49
|
-
registerControllerUnlessBound(scope, ControllerClass);
|
|
52
|
+
if (ControllerClass) registerControllerUnlessBound(scope, ControllerClass);
|
|
53
|
+
const route = { params, query };
|
|
54
|
+
const loaderSet = options.resolveLoaders ? await options.resolveLoaders(view) : NO_LOADERS;
|
|
55
|
+
const loaded = await ExecutionContext.run(
|
|
56
|
+
scope,
|
|
57
|
+
() => runViewLoaders(loaderSet, createLoaderRequest(identity, route), preservedDepth)
|
|
58
|
+
);
|
|
50
59
|
const layouts = [...preserved];
|
|
51
|
-
for (let index =
|
|
60
|
+
for (let index = preservedDepth; index < view.layouts.length; index += 1) {
|
|
52
61
|
const layoutIdentity = view.layoutControllers?.[index];
|
|
53
62
|
if (!layoutIdentity) continue;
|
|
54
63
|
const preserve = view.layoutPreserve?.[index] === true;
|
|
@@ -73,6 +82,17 @@ function createFrontendSurface(options) {
|
|
|
73
82
|
);
|
|
74
83
|
markController(instance, LayoutClass);
|
|
75
84
|
rollback.push(() => ExecutionContext.run(layoutScope, () => disposeLocalController(instance)));
|
|
85
|
+
if (typeof instance.initialize === "function") {
|
|
86
|
+
await registry.track(
|
|
87
|
+
Promise.resolve(
|
|
88
|
+
ExecutionContext.run(
|
|
89
|
+
layoutScope,
|
|
90
|
+
() => initializeController(instance, initializeArgument(route, loaded.layouts[index] ?? null))
|
|
91
|
+
)
|
|
92
|
+
).then(() => void 0),
|
|
93
|
+
{ label: `initialize layout ${layoutIdentity}`, kind: "initialize", owner: `page ${identity}` }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
76
96
|
layouts.push({
|
|
77
97
|
index,
|
|
78
98
|
path: view.layouts[index],
|
|
@@ -84,18 +104,18 @@ function createFrontendSurface(options) {
|
|
|
84
104
|
ownLifecycle
|
|
85
105
|
});
|
|
86
106
|
}
|
|
87
|
-
const controller = await ExecutionContext.run(
|
|
107
|
+
const controller = ControllerClass ? await ExecutionContext.run(
|
|
88
108
|
scope,
|
|
89
109
|
() => scope.get(ControllerClass)
|
|
90
|
-
);
|
|
91
|
-
markController(controller, ControllerClass);
|
|
110
|
+
) : void 0;
|
|
111
|
+
if (controller) markController(controller, ControllerClass);
|
|
92
112
|
rollback.push(() => ExecutionContext.run(scope, () => disposeLocalController(controller)));
|
|
93
|
-
if (typeof controller
|
|
113
|
+
if (typeof controller?.initialize === "function") {
|
|
94
114
|
await registry.track(
|
|
95
115
|
Promise.resolve(
|
|
96
116
|
ExecutionContext.run(
|
|
97
117
|
scope,
|
|
98
|
-
() => controller
|
|
118
|
+
() => initializeController(controller, initializeArgument(route, loaded.view))
|
|
99
119
|
)
|
|
100
120
|
).then(() => void 0),
|
|
101
121
|
{ label: `initialize ${identity}`, kind: "initialize", owner: `page ${identity}` }
|
|
@@ -110,7 +130,7 @@ function createFrontendSurface(options) {
|
|
|
110
130
|
await disposePage(identity);
|
|
111
131
|
}
|
|
112
132
|
});
|
|
113
|
-
return { identity, view, scope, lifecycle, handle, controller, layouts };
|
|
133
|
+
return { identity, mountKey: createNavigationMountKey(identity, params), view, scope, lifecycle, handle, controller, layouts };
|
|
114
134
|
} catch (error) {
|
|
115
135
|
try {
|
|
116
136
|
await cleanupAll(rollback.reverse(), "Wood page rollback failed");
|
|
@@ -161,7 +181,7 @@ function createFrontendSurface(options) {
|
|
|
161
181
|
entry.options ?? {}
|
|
162
182
|
) : 0;
|
|
163
183
|
const carried = previous ? previous.layouts.filter((layout) => layout.index < depth) : [];
|
|
164
|
-
const next = await constructPage(view, entry.params, entry.query, carried);
|
|
184
|
+
const next = await constructPage(view, entry.params, entry.query, carried, depth);
|
|
165
185
|
if (disposed) {
|
|
166
186
|
await endPage(next, depth);
|
|
167
187
|
return;
|
|
@@ -173,7 +193,7 @@ function createFrontendSurface(options) {
|
|
|
173
193
|
}
|
|
174
194
|
const unsubscribe = navigation.subscribe((entry) => {
|
|
175
195
|
if (!entry || transitioning || disposed) return;
|
|
176
|
-
if (active && entry.page === active.
|
|
196
|
+
if (active && createNavigationMountKey(entry.page, entry.params) === active.mountKey) return;
|
|
177
197
|
if (!active) return;
|
|
178
198
|
const transition = navigationQueue.then(() => transitionTo(entry));
|
|
179
199
|
navigationQueue = transition.catch(() => {
|