@noego/wood 0.5.1 → 0.6.1

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.
Files changed (151) hide show
  1. package/README.md +31 -0
  2. package/dist/client/index.cjs +30 -11
  3. package/dist/client/index.cjs.map +1 -1
  4. package/dist/client/index.d.cts +33 -1
  5. package/dist/client/index.d.ts +33 -1
  6. package/dist/client/index.js +28 -11
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/codegen/preload_generator.cjs +44 -5
  9. package/dist/codegen/preload_generator.cjs.map +1 -1
  10. package/dist/codegen/preload_generator.js +44 -5
  11. package/dist/codegen/preload_generator.js.map +1 -1
  12. package/dist/codegen/renderer_app_generator.cjs +2 -9
  13. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  14. package/dist/codegen/renderer_app_generator.js +2 -9
  15. package/dist/codegen/renderer_app_generator.js.map +1 -1
  16. package/dist/controller/cleanup.cjs +39 -0
  17. package/dist/controller/cleanup.cjs.map +1 -0
  18. package/dist/controller/cleanup.d.cts +4 -0
  19. package/dist/controller/cleanup.d.ts +4 -0
  20. package/dist/controller/cleanup.js +15 -0
  21. package/dist/controller/cleanup.js.map +1 -0
  22. package/dist/controller/controller_lifecycle.cjs +26 -2
  23. package/dist/controller/controller_lifecycle.cjs.map +1 -1
  24. package/dist/controller/controller_lifecycle.d.cts +3 -1
  25. package/dist/controller/controller_lifecycle.d.ts +3 -1
  26. package/dist/controller/controller_lifecycle.js +24 -1
  27. package/dist/controller/controller_lifecycle.js.map +1 -1
  28. package/dist/frontend/frontend_lifecycle.cjs +5 -7
  29. package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
  30. package/dist/frontend/frontend_lifecycle.d.cts +1 -14
  31. package/dist/frontend/frontend_lifecycle.d.ts +1 -14
  32. package/dist/frontend/frontend_lifecycle.js +5 -7
  33. package/dist/frontend/frontend_lifecycle.js.map +1 -1
  34. package/dist/frontend/frontend_surface.cjs +130 -104
  35. package/dist/frontend/frontend_surface.cjs.map +1 -1
  36. package/dist/frontend/frontend_surface.js +131 -105
  37. package/dist/frontend/frontend_surface.js.map +1 -1
  38. package/dist/frontend/work_registry.cjs +29 -4
  39. package/dist/frontend/work_registry.cjs.map +1 -1
  40. package/dist/frontend/work_registry.d.cts +2 -0
  41. package/dist/frontend/work_registry.d.ts +2 -0
  42. package/dist/frontend/work_registry.js +29 -4
  43. package/dist/frontend/work_registry.js.map +1 -1
  44. package/dist/index.d.cts +2 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/ipc/index.cjs +6 -2
  47. package/dist/ipc/index.cjs.map +1 -1
  48. package/dist/ipc/index.d.cts +9 -0
  49. package/dist/ipc/index.d.ts +9 -0
  50. package/dist/ipc/index.js +6 -2
  51. package/dist/ipc/index.js.map +1 -1
  52. package/dist/main/index.cjs +81 -82
  53. package/dist/main/index.cjs.map +1 -1
  54. package/dist/main/index.d.cts +13 -4
  55. package/dist/main/index.d.ts +13 -4
  56. package/dist/main/index.js +89 -87
  57. package/dist/main/index.js.map +1 -1
  58. package/dist/main/shutdown_coordinator.cjs +153 -0
  59. package/dist/main/shutdown_coordinator.cjs.map +1 -0
  60. package/dist/main/shutdown_coordinator.d.cts +81 -0
  61. package/dist/main/shutdown_coordinator.d.ts +81 -0
  62. package/dist/main/shutdown_coordinator.js +127 -0
  63. package/dist/main/shutdown_coordinator.js.map +1 -0
  64. package/dist/main/startup_failure_handler.cjs +67 -0
  65. package/dist/main/startup_failure_handler.cjs.map +1 -0
  66. package/dist/main/startup_failure_handler.d.cts +16 -0
  67. package/dist/main/startup_failure_handler.d.ts +16 -0
  68. package/dist/main/startup_failure_handler.js +43 -0
  69. package/dist/main/startup_failure_handler.js.map +1 -0
  70. package/dist/router/index.d.cts +3 -0
  71. package/dist/router/index.d.ts +3 -0
  72. package/dist/router/wood_router.cjs +204 -123
  73. package/dist/router/wood_router.cjs.map +1 -1
  74. package/dist/router/wood_router.d.cts +20 -0
  75. package/dist/router/wood_router.d.ts +20 -0
  76. package/dist/router/wood_router.js +206 -125
  77. package/dist/router/wood_router.js.map +1 -1
  78. package/dist/runtime/index.cjs +13 -0
  79. package/dist/runtime/index.cjs.map +1 -1
  80. package/dist/runtime/index.d.cts +8 -4
  81. package/dist/runtime/index.d.ts +8 -4
  82. package/dist/runtime/index.js +8 -0
  83. package/dist/runtime/index.js.map +1 -1
  84. package/dist/runtime/renderer_config_extension.cjs +1 -2
  85. package/dist/runtime/renderer_config_extension.cjs.map +1 -1
  86. package/dist/runtime/renderer_config_extension.d.cts +1 -1
  87. package/dist/runtime/renderer_config_extension.d.ts +1 -1
  88. package/dist/runtime/renderer_config_extension.js +1 -2
  89. package/dist/runtime/renderer_config_extension.js.map +1 -1
  90. package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
  91. package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
  92. package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
  93. package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
  94. package/dist/runtime/renderer_log_dispatcher.js +1 -0
  95. package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
  96. package/dist/runtime/runtime.cjs +107 -57
  97. package/dist/runtime/runtime.cjs.map +1 -1
  98. package/dist/runtime/runtime.d.cts +12 -0
  99. package/dist/runtime/runtime.d.ts +12 -0
  100. package/dist/runtime/runtime.js +110 -59
  101. package/dist/runtime/runtime.js.map +1 -1
  102. package/dist/runtime/runtime_module.cjs +58 -0
  103. package/dist/runtime/runtime_module.cjs.map +1 -0
  104. package/dist/runtime/runtime_module.d.cts +22 -0
  105. package/dist/runtime/runtime_module.d.ts +22 -0
  106. package/dist/runtime/runtime_module.js +30 -0
  107. package/dist/runtime/runtime_module.js.map +1 -0
  108. package/dist/testing/create_controller_harness.d.cts +3 -0
  109. package/dist/testing/create_controller_harness.d.ts +3 -0
  110. package/dist/testing/create_router_bridge.d.cts +3 -0
  111. package/dist/testing/create_router_bridge.d.ts +3 -0
  112. package/dist/testing/create_test_router.d.cts +3 -0
  113. package/dist/testing/create_test_router.d.ts +3 -0
  114. package/dist/testing/index.cjs +3 -0
  115. package/dist/testing/index.cjs.map +1 -1
  116. package/dist/testing/index.d.cts +6 -1
  117. package/dist/testing/index.d.ts +6 -1
  118. package/dist/testing/index.js +2 -0
  119. package/dist/testing/index.js.map +1 -1
  120. package/dist/testing/registration_only_root.cjs +168 -0
  121. package/dist/testing/registration_only_root.cjs.map +1 -0
  122. package/dist/testing/registration_only_root.d.cts +46 -0
  123. package/dist/testing/registration_only_root.d.ts +46 -0
  124. package/dist/testing/registration_only_root.js +141 -0
  125. package/dist/testing/registration_only_root.js.map +1 -0
  126. package/dist/testing/test_wood_config.cjs +382 -70
  127. package/dist/testing/test_wood_config.cjs.map +1 -1
  128. package/dist/testing/test_wood_config.d.cts +25 -1
  129. package/dist/testing/test_wood_config.d.ts +25 -1
  130. package/dist/testing/test_wood_config.js +388 -71
  131. package/dist/testing/test_wood_config.js.map +1 -1
  132. package/dist/testing/wood_dom_animation.cjs +213 -0
  133. package/dist/testing/wood_dom_animation.cjs.map +1 -0
  134. package/dist/testing/wood_dom_animation.d.cts +89 -0
  135. package/dist/testing/wood_dom_animation.d.ts +89 -0
  136. package/dist/testing/wood_dom_animation.js +185 -0
  137. package/dist/testing/wood_dom_animation.js.map +1 -0
  138. package/dist/testing/wood_env_renderer.cjs +81 -32
  139. package/dist/testing/wood_env_renderer.cjs.map +1 -1
  140. package/dist/testing/wood_env_renderer.d.cts +32 -1
  141. package/dist/testing/wood_env_renderer.d.ts +32 -1
  142. package/dist/testing/wood_env_renderer.js +84 -32
  143. package/dist/testing/wood_env_renderer.js.map +1 -1
  144. package/dist/transport/in_process_router_transport.d.cts +3 -0
  145. package/dist/transport/in_process_router_transport.d.ts +3 -0
  146. package/dist/transport/index.d.cts +3 -0
  147. package/dist/transport/index.d.ts +3 -0
  148. package/package.json +7 -6
  149. package/src/components/NavigationShell.svelte +12 -7
  150. package/src/controller/cleanup.ts +8 -0
  151. package/src/controller/controller_lifecycle.ts +24 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n for (const layoutPath of view.layouts) {\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n}\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n if (typeof scopedContainer.registerFunction === 'function') {\n // Explicit Singleton lifetime: these return renderer-root globals, and\n // registerFunction defaults to Transient — rejected by ioc >= 0.5's\n // captive-lifetime validation under singleton dependents.\n const singleton = { loadAs: 0 /* LoadAs.Singleton */ };\n scopedContainer.registerFunction(Navigation, () => getNavigation(), singleton);\n scopedContainer.registerFunction(App, () => getApp(), singleton);\n scopedContainer.registerFunction(Ipc, () => getIpc(), singleton);\n }\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\nasync function disposeScopeQuietly(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n try {\n await scope.dispose();\n } catch (error) {\n console.error('[wood] renderer scope disposal failed:', error);\n }\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: dispose everything this\n // set created before rethrowing.\n for (const scope of createdLayoutScopes) await disposeScopeQuietly(scope as any);\n await disposeScopeQuietly(pageScope as any);\n throw error;\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n if (requestedRoute) {\n const routeEntry = getViewManifestEntry(requestedRoute);\n if (routeEntry) {\n return routeEntry;\n }\n }\n\n const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n return entry;\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport async function app(target: Element) {\n const entry = await resolveInitialEntry();\n\n getNavigation().replace(entry.key, {}, {});\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry);\n\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n}\n`;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/codegen/renderer_app_generator.ts"],"sourcesContent":["import { existsSync } from 'fs';\nimport path from 'path';\nimport type { ViewDef } from '../types/index.js';\nimport { generatedFileNotice } from './generated_file_notice.js';\n\nconst RENDERER_APP_SOURCE_INSTRUCTIONS = [\n 'To change routes, page components, layouts, or renderer controllers, edit the views YAML',\n 'configured by wood.config.yml renderer.openapi and the matching UI source files.',\n] as const;\n\nexport interface RendererAppGeneratorOptions {\n /** Absolute path to renderer component root (e.g. /app/ui) */\n componentDir: string;\n /** Absolute output directory where generated file will be written */\n outDir: string;\n /** Optional explicit default view key (e.g. \"main.chat\") */\n entryViewKey?: string;\n}\n\nfunction computeImportPath(fromDir: string, toFile: string): string {\n let rel = path.relative(fromDir, toFile);\n\n if (!rel.startsWith('.')) {\n rel = './' + rel;\n }\n\n return rel.replace(/\\\\/g, '/');\n}\n\nfunction resolveFromComponentDir(componentDir: string, fileRef: string): string {\n if (path.isAbsolute(fileRef)) {\n return fileRef;\n }\n\n const normalizedRef = fileRef.replace(/\\\\/g, '/');\n const componentDirName = path.basename(componentDir).replace(/\\\\/g, '/');\n\n if (normalizedRef.startsWith(componentDirName + '/')) {\n const stripped = normalizedRef.slice(componentDirName.length + 1);\n return path.resolve(componentDir, stripped);\n }\n\n return path.resolve(componentDir, normalizedRef);\n}\n\nfunction viewKey(view: ViewDef): string {\n return `${view.windowName}.${view.pageName}`;\n}\n\nfunction toIdentifier(prefix: string, value: string): string {\n const core = value\n .replace(/[^a-zA-Z0-9]+/g, ' ')\n .trim()\n .split(/\\s+/)\n .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))\n .join('');\n\n return `${prefix}${core || 'Component'}`;\n}\n\nfunction escapeSingleQuoted(value: string): string {\n return value.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, \"\\\\'\");\n}\n\nconst CONTROLLER_FILE_SUFFIXES = [\n '.ts',\n '.js',\n '.controller.ts',\n '.controller.js',\n] as const;\n\nconst CONTROLLER_INDEX_CANDIDATES = [\n 'index.ts',\n 'index.js',\n] as const;\n\nfunction resolveControllerFile(controllerRef: string, componentDir: string): string | null {\n const normalized = controllerRef.trim();\n const basePath = resolveFromComponentDir(componentDir, normalized);\n\n if (existsSync(basePath)) {\n return basePath;\n }\n\n if (path.extname(basePath)) {\n return null;\n }\n\n for (const suffix of CONTROLLER_FILE_SUFFIXES) {\n const candidate = basePath + suffix;\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n for (const indexFile of CONTROLLER_INDEX_CANDIDATES) {\n const candidate = path.resolve(basePath, indexFile);\n if (existsSync(candidate)) {\n return candidate;\n }\n }\n\n return null;\n}\n\nfunction resolveDefaultViewKey(sortedViews: ViewDef[], entryViewKey?: string): string {\n if (sortedViews.length === 0) return '';\n\n const configuredEntry = entryViewKey?.trim();\n if (!configuredEntry) {\n return viewKey(sortedViews[0]);\n }\n\n const availableKeys = sortedViews.map((view) => viewKey(view));\n if (availableKeys.includes(configuredEntry)) {\n return configuredEntry;\n }\n\n throw new Error(\n `Renderer entry \"${configuredEntry}\" was not found in parsed views. Available view keys: ${availableKeys.join(', ')}`,\n );\n}\n\nexport function generateRendererApp(\n views: ViewDef[],\n options: RendererAppGeneratorOptions,\n): string {\n const sortedViews = [...views].sort((a, b) => viewKey(a).localeCompare(viewKey(b)));\n const defaultViewKey = resolveDefaultViewKey(sortedViews, options.entryViewKey);\n\n const importLines: string[] = [];\n const viewIdentifiers = new Map<string, string>();\n const layoutIdentifiers = new Map<string, string>();\n const controllerIdentifiers = new Map<string, { identifier: string; importPath: string }>();\n const usedNames = new Set<string>();\n\n const allocateName = (prefix: string, filePath: string): string => {\n const base = toIdentifier(prefix, filePath);\n let name = base;\n let suffix = 2;\n while (usedNames.has(name)) {\n name = `${base}${suffix}`;\n suffix += 1;\n }\n usedNames.add(name);\n return name;\n };\n\n const ensureControllerImported = (controllerRef: string, owningViewKey: string): void => {\n if (controllerIdentifiers.has(controllerRef)) return;\n\n const resolvedControllerFile = resolveControllerFile(controllerRef, options.componentDir);\n if (!resolvedControllerFile) {\n throw new Error(\n `Renderer app codegen failed: controller \"${controllerRef}\" for view \"${owningViewKey}\" was not found. ` +\n 'Controller must point to an existing file with a default exported class.',\n );\n }\n\n const identifier = allocateName('PageController', controllerRef);\n const importPath = computeImportPath(options.outDir, resolvedControllerFile);\n controllerIdentifiers.set(controllerRef, { identifier, importPath });\n importLines.push(`import ${identifier} from '${importPath}';`);\n };\n\n for (const view of sortedViews) {\n if (!viewIdentifiers.has(view.viewPath)) {\n const identifier = allocateName('View', view.viewPath);\n viewIdentifiers.set(view.viewPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, view.viewPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n for (const layoutPath of view.layouts) {\n if (layoutIdentifiers.has(layoutPath)) continue;\n const identifier = allocateName('Layout', layoutPath);\n layoutIdentifiers.set(layoutPath, identifier);\n const abs = resolveFromComponentDir(options.componentDir, layoutPath);\n const importPath = computeImportPath(options.outDir, abs);\n importLines.push(`import ${identifier} from '${importPath}';`);\n }\n\n if (view.controller) {\n ensureControllerImported(view.controller, viewKey(view));\n }\n\n for (const layoutController of view.layoutControllers ?? []) {\n if (!layoutController) continue;\n ensureControllerImported(layoutController, viewKey(view));\n }\n }\n\n const manifestEntries = sortedViews.map((view) => {\n const key = viewKey(view);\n const viewIdentifier = viewIdentifiers.get(view.viewPath)!;\n const layoutList = view.layouts.map((layoutPath) => layoutIdentifiers.get(layoutPath)!);\n const controllerPath = view.controller ? `'${escapeSingleQuoted(view.controller)}'` : 'undefined';\n const controllerClass = view.controller\n ? controllerIdentifiers.get(view.controller)!.identifier\n : 'undefined';\n const layoutControllerRefs = view.layoutControllers;\n if (layoutControllerRefs && layoutControllerRefs.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout controller count for view \"${key}\" does not match layout count.`,\n );\n }\n const normalizedLayoutControllerRefs = layoutControllerRefs ?? Array.from(\n { length: view.layouts.length },\n () => undefined,\n );\n const layoutControllerPaths = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? `'${escapeSingleQuoted(controllerRef)}'` : 'undefined'))\n .join(', ')}]`;\n const layoutControllerClasses = `[${normalizedLayoutControllerRefs\n .map((controllerRef) => (controllerRef ? controllerIdentifiers.get(controllerRef)!.identifier : 'undefined'))\n .join(', ')}]`;\n const layoutPreserve = view.layoutPreserve ?? Array.from(\n { length: view.layouts.length },\n () => false,\n );\n if (layoutPreserve.length !== view.layouts.length) {\n throw new Error(\n `Renderer app codegen failed: layout preserve count for view \"${key}\" does not match layout count.`,\n );\n }\n\n return ` '${escapeSingleQuoted(key)}': {\n key: '${escapeSingleQuoted(key)}',\n windowName: '${escapeSingleQuoted(view.windowName)}',\n pageName: '${escapeSingleQuoted(view.pageName)}',\n viewPath: '${escapeSingleQuoted(view.viewPath)}',\n layoutPaths: ${JSON.stringify(view.layouts)},\n layoutControllers: ${layoutControllerPaths},\n layoutPreserve: ${JSON.stringify(layoutPreserve)},\n controller: ${controllerPath},\n layoutControllerClasses: ${layoutControllerClasses},\n controllerClass: ${controllerClass},\n view: ${viewIdentifier},\n layouts: [${layoutList.join(', ')}],\n },`;\n });\n\n return `${generatedFileNotice(RENDERER_APP_SOURCE_INSTRUCTIONS)}\nimport { mount } from 'svelte';\nimport { App, getApp, getContainer } from '@noego/wood/client';\nimport { Navigation, getNavigation, installAutomationIntrospection } from '@noego/wood/navigation';\nimport { Ipc, getIpc } from '@noego/wood/ipc';\nimport NavigationShell from '@noego/wood/navigation-shell';\n${importLines.join('\\n')}\n\ninterface GeneratedAppViewEntry {\n key: string;\n windowName: string;\n pageName: string;\n viewPath: string;\n layoutPaths: string[];\n layoutControllers: Array<string | undefined>;\n layoutPreserve: boolean[];\n controller?: string;\n layoutControllerClasses: Array<any | undefined>;\n controllerClass?: any;\n view: any;\n layouts: any[];\n}\n\nconst VIEW_MANIFEST: Record<string, GeneratedAppViewEntry> = {\n${manifestEntries.join('\\n')}\n};\n\nfunction getViewManifestEntry(viewKey: string): GeneratedAppViewEntry | null {\n return VIEW_MANIFEST[viewKey] ?? null;\n}\n\ninterface WoodScopedContainer {\n get(token: any): unknown | Promise<unknown>;\n registerClass?(token: any): void;\n registerFunction?(\n token: any,\n resolver: (...args: any[]) => unknown,\n options?: Record<string, unknown>,\n ): void;\n}\n\ninterface WoodContainer extends WoodScopedContainer {\n extend?(): WoodScopedContainer;\n}\n\nfunction createScopedContainer(): WoodScopedContainer {\n const container = getContainer() as WoodContainer;\n if (typeof container.extend !== 'function') {\n throw new Error('Wood client container does not support extend().');\n }\n\n const scopedContainer = container.extend();\n\n if (!scopedContainer || typeof scopedContainer.get !== 'function') {\n throw new Error('Wood client container does not support get().');\n }\n\n // Renderer builtins belong to the root created by getContainer().\n // Page scopes inherit them; re-registering resolved ancestor singletons is invalid.\n\n return scopedContainer as WoodScopedContainer;\n}\n\nasync function resolveControllerInScope(\n scope: WoodScopedContainer,\n entry: GeneratedAppViewEntry,\n ControllerClass: any,\n contextLabel: string,\n): Promise<unknown> {\n if (typeof ControllerClass !== 'function') {\n throw new Error(\n \\`Controller for \\${contextLabel} in view \"\\${entry.key}\" is invalid. Expected a default exported class.\\`,\n );\n }\n\n if (typeof scope.registerClass === 'function') {\n scope.registerClass(ControllerClass);\n }\n\n return await scope.get(ControllerClass);\n}\n\nasync function disposeScopeQuietly(scope: { dispose?: () => Promise<void> } | null): Promise<void> {\n if (!scope || typeof scope.dispose !== 'function') return;\n try {\n await scope.dispose();\n } catch (error) {\n console.error('[wood] renderer scope disposal failed:', error);\n }\n}\n\n/**\n * One page scope per view lifecycle: the view controller and every\n * non-preservable layout controller resolve from the SAME scope, so Scoped\n * services/state are shared within the page (the NoEgo renderer model).\n * Layouts marked preservable in the manifest own dedicated scopes that\n * survive page transitions and dispose when the layout finally leaves.\n */\nasync function createControllerSet(\n entry: GeneratedAppViewEntry,\n preservedDepth = 0,\n): Promise<{\n controller: unknown;\n layoutControllers: Array<unknown>;\n pageScope: WoodScopedContainer;\n layoutScopes: Array<WoodScopedContainer | null>;\n}> {\n const pageScope = createScopedContainer();\n const layoutControllers: unknown[] = [];\n const layoutScopes: Array<WoodScopedContainer | null> = [];\n const createdLayoutScopes: WoodScopedContainer[] = [];\n\n try {\n for (let i = 0; i < preservedDepth; i += 1) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n }\n\n for (let i = preservedDepth; i < entry.layoutControllerClasses.length; i += 1) {\n const LayoutController = entry.layoutControllerClasses[i];\n if (!LayoutController) {\n layoutControllers.push(undefined);\n layoutScopes.push(null);\n continue;\n }\n const preservable = entry.layoutPreserve?.[i] === true;\n let scope = pageScope;\n if (preservable) {\n scope = createScopedContainer();\n createdLayoutScopes.push(scope);\n }\n const controller = await resolveControllerInScope(\n scope,\n entry,\n LayoutController,\n \\`layout[\\${i}]\\`,\n );\n layoutControllers.push(controller);\n layoutScopes.push(preservable ? scope : null);\n }\n\n const controller = entry.controllerClass\n ? await resolveControllerInScope(pageScope, entry, entry.controllerClass, 'page')\n : undefined;\n\n return { controller, layoutControllers, pageScope, layoutScopes };\n } catch (error) {\n // Failed transitions must not orphan scopes: dispose everything this\n // set created before rethrowing.\n for (const scope of createdLayoutScopes) await disposeScopeQuietly(scope as any);\n await disposeScopeQuietly(pageScope as any);\n throw error;\n }\n}\n\nconst DEFAULT_VIEW_KEY = '${escapeSingleQuoted(defaultViewKey)}';\n\ntype WoodWindowBootstrapInfo = {\n ok?: boolean;\n windowId?: string;\n defaultRoute?: string;\n};\n\ntype WoodBootstrapBridgeLike = {\n __window?: {\n current?: () => Promise<WoodWindowBootstrapInfo>;\n };\n};\n\nfunction getWoodBridge(): WoodBootstrapBridgeLike | null {\n const globalObj = globalThis as typeof globalThis & {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n window?: {\n __NOEGO_WOOD_BRIDGE__?: unknown;\n };\n };\n\n const bridge = globalObj.__NOEGO_WOOD_BRIDGE__ ?? globalObj.window?.__NOEGO_WOOD_BRIDGE__;\n if (!bridge || typeof bridge !== 'object') {\n return null;\n }\n\n return bridge as WoodBootstrapBridgeLike;\n}\n\nasync function resolveInitialEntry(): Promise<GeneratedAppViewEntry> {\n if (!DEFAULT_VIEW_KEY) {\n throw new Error('No views available in renderer manifest. Define at least one view in renderer OpenAPI YAML.');\n }\n\n const bridge = getWoodBridge();\n const bootstrapWindow = await bridge?.__window?.current?.();\n const requestedRoute = bootstrapWindow?.defaultRoute;\n\n if (requestedRoute) {\n const routeEntry = getViewManifestEntry(requestedRoute);\n if (routeEntry) {\n return routeEntry;\n }\n }\n\n const entry = getViewManifestEntry(DEFAULT_VIEW_KEY);\n if (!entry) {\n throw new Error(\\`Default view \"\\${DEFAULT_VIEW_KEY}\" is missing from generated view manifest.\\`);\n }\n\n return entry;\n}\n\nfunction isAutomationBuild(): boolean {\n try {\n const env = (import.meta as { env?: { DEV?: boolean; MODE?: string } }).env;\n return Boolean(env?.DEV) || env?.MODE === 'test';\n } catch {\n return false;\n }\n}\n\nexport async function app(target: Element) {\n const entry = await resolveInitialEntry();\n\n getNavigation().replace(entry.key, {}, {});\n\n // Dev/test-only Axe introspection surface; never present in production builds.\n if (isAutomationBuild()) {\n const bootstrapWindow = await getWoodBridge()?.__window?.current?.();\n installAutomationIntrospection({ windowId: bootstrapWindow?.windowId ?? null });\n }\n const initialSet = await createControllerSet(entry);\n\n return mount(NavigationShell, {\n target,\n props: {\n manifest: VIEW_MANIFEST,\n initialEntry: entry,\n initialControllerSet: initialSet,\n createControllerSetFn: createControllerSet,\n getViewManifestEntryFn: getViewManifestEntry,\n },\n });\n}\n`;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var cleanup_exports = {};
20
+ __export(cleanup_exports, {
21
+ cleanupAll: () => cleanupAll
22
+ });
23
+ module.exports = __toCommonJS(cleanup_exports);
24
+ async function cleanupAll(steps, message = "Wood cleanup failed") {
25
+ const errors = [];
26
+ for (const step of steps) {
27
+ try {
28
+ await step();
29
+ } catch (error) {
30
+ errors.push(error);
31
+ }
32
+ }
33
+ if (errors.length) throw Object.assign(new Error(message), { errors, cause: errors[0] });
34
+ }
35
+ // Annotate the CommonJS export names for ESM import in node:
36
+ 0 && (module.exports = {
37
+ cleanupAll
38
+ });
39
+ //# sourceMappingURL=cleanup.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controller/cleanup.ts"],"sourcesContent":["/** Attempt every owner in order, preserving all failures (including non-Error throws). */\nexport async function cleanupAll(steps: ReadonlyArray<() => unknown | Promise<unknown>>, message = 'Wood cleanup failed'): Promise<void> {\n const errors: unknown[] = [];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length) throw Object.assign(new Error(message), { errors, cause: errors[0] });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,eAAsB,WAAW,OAAwD,UAAU,uBAAsC;AACvI,QAAM,SAAoB,CAAC;AAC3B,aAAW,QAAQ,OAAO;AACxB,QAAI;AAAE,YAAM,KAAK;AAAA,IAAG,SAAS,OAAO;AAAE,aAAO,KAAK,KAAK;AAAA,IAAG;AAAA,EAC5D;AACA,MAAI,OAAO,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,EAAE,QAAQ,OAAO,OAAO,CAAC,EAAE,CAAC;AACzF;","names":[]}
@@ -0,0 +1,4 @@
1
+ /** Attempt every owner in order, preserving all failures (including non-Error throws). */
2
+ declare function cleanupAll(steps: ReadonlyArray<() => unknown | Promise<unknown>>, message?: string): Promise<void>;
3
+
4
+ export { cleanupAll };
@@ -0,0 +1,4 @@
1
+ /** Attempt every owner in order, preserving all failures (including non-Error throws). */
2
+ declare function cleanupAll(steps: ReadonlyArray<() => unknown | Promise<unknown>>, message?: string): Promise<void>;
3
+
4
+ export { cleanupAll };
@@ -0,0 +1,15 @@
1
+ async function cleanupAll(steps, message = "Wood cleanup failed") {
2
+ const errors = [];
3
+ for (const step of steps) {
4
+ try {
5
+ await step();
6
+ } catch (error) {
7
+ errors.push(error);
8
+ }
9
+ }
10
+ if (errors.length) throw Object.assign(new Error(message), { errors, cause: errors[0] });
11
+ }
12
+ export {
13
+ cleanupAll
14
+ };
15
+ //# sourceMappingURL=cleanup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/controller/cleanup.ts"],"sourcesContent":["/** Attempt every owner in order, preserving all failures (including non-Error throws). */\nexport async function cleanupAll(steps: ReadonlyArray<() => unknown | Promise<unknown>>, message = 'Wood cleanup failed'): Promise<void> {\n const errors: unknown[] = [];\n for (const step of steps) {\n try { await step(); } catch (error) { errors.push(error); }\n }\n if (errors.length) throw Object.assign(new Error(message), { errors, cause: errors[0] });\n}\n"],"mappings":"AACA,eAAsB,WAAW,OAAwD,UAAU,uBAAsC;AACvI,QAAM,SAAoB,CAAC;AAC3B,aAAW,QAAQ,OAAO;AACxB,QAAI;AAAE,YAAM,KAAK;AAAA,IAAG,SAAS,OAAO;AAAE,aAAO,KAAK,KAAK;AAAA,IAAG;AAAA,EAC5D;AACA,MAAI,OAAO,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,OAAO,GAAG,EAAE,QAAQ,OAAO,OAAO,CAAC,EAAE,CAAC;AACzF;","names":[]}
@@ -18,13 +18,15 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var controller_lifecycle_exports = {};
20
20
  __export(controller_lifecycle_exports, {
21
- disposeControllerLifecycle: () => disposeControllerLifecycle
21
+ disposeControllerLifecycle: () => disposeControllerLifecycle,
22
+ guardControllerLifecycleOnce: () => guardControllerLifecycleOnce
22
23
  });
23
24
  module.exports = __toCommonJS(controller_lifecycle_exports);
24
25
  async function disposeControllerLifecycle(target) {
25
26
  if (!target || typeof target !== "object" && typeof target !== "function") {
26
27
  return;
27
28
  }
29
+ guardControllerLifecycleOnce(target);
28
30
  const lifecycle = target;
29
31
  if (typeof lifecycle.dispose === "function") {
30
32
  await lifecycle.dispose();
@@ -34,8 +36,30 @@ async function disposeControllerLifecycle(target) {
34
36
  await lifecycle.destroy();
35
37
  }
36
38
  }
39
+ const guardedControllers = /* @__PURE__ */ new WeakSet();
40
+ function guardControllerLifecycleOnce(target) {
41
+ if (!target || typeof target !== "object" && typeof target !== "function") return;
42
+ if (guardedControllers.has(target)) return;
43
+ guardedControllers.add(target);
44
+ const instance = target;
45
+ for (const name of ["dispose", "destroy"]) {
46
+ const original = instance[name];
47
+ if (typeof original !== "function") continue;
48
+ let called = false;
49
+ Object.defineProperty(instance, name, {
50
+ configurable: true,
51
+ writable: true,
52
+ value: function(...args) {
53
+ if (called) return;
54
+ called = true;
55
+ return original.apply(this, args);
56
+ }
57
+ });
58
+ }
59
+ }
37
60
  // Annotate the CommonJS export names for ESM import in node:
38
61
  0 && (module.exports = {
39
- disposeControllerLifecycle
62
+ disposeControllerLifecycle,
63
+ guardControllerLifecycleOnce
40
64
  });
41
65
  //# sourceMappingURL=controller_lifecycle.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n guardControllerLifecycleOnce(target);\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n\nconst guardedControllers = new WeakSet<object>();\n\n/** Share one teardown attempt between the explicit controller owner and IoC. */\nexport function guardControllerLifecycleOnce(target: unknown): void {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;\n if (guardedControllers.has(target as object)) return;\n guardedControllers.add(target as object);\n const instance = target as Record<string, unknown>;\n for (const name of ['dispose', 'destroy'] as const) {\n const original = instance[name];\n if (typeof original !== 'function') continue;\n let called = false;\n Object.defineProperty(instance, name, {\n configurable: true, writable: true,\n value: function (this: unknown, ...args: unknown[]) {\n if (called) return;\n called = true;\n return original.apply(this, args);\n },\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,+BAA6B,MAAM;AACnC,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;AAEA,MAAM,qBAAqB,oBAAI,QAAgB;AAGxC,SAAS,6BAA6B,QAAuB;AAClE,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,WAAa;AAC7E,MAAI,mBAAmB,IAAI,MAAgB,EAAG;AAC9C,qBAAmB,IAAI,MAAgB;AACvC,QAAM,WAAW;AACjB,aAAW,QAAQ,CAAC,WAAW,SAAS,GAAY;AAClD,UAAM,WAAW,SAAS,IAAI;AAC9B,QAAI,OAAO,aAAa,WAAY;AACpC,QAAI,SAAS;AACb,WAAO,eAAe,UAAU,MAAM;AAAA,MACpC,cAAc;AAAA,MAAM,UAAU;AAAA,MAC9B,OAAO,YAA4B,MAAiB;AAClD,YAAI,OAAQ;AACZ,iBAAS;AACT,eAAO,SAAS,MAAM,MAAM,IAAI;AAAA,MAClC;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -8,5 +8,7 @@ interface ControllerLifecycle {
8
8
  * for existing page controllers.
9
9
  */
10
10
  declare function disposeControllerLifecycle(target: unknown): Promise<void>;
11
+ /** Share one teardown attempt between the explicit controller owner and IoC. */
12
+ declare function guardControllerLifecycleOnce(target: unknown): void;
11
13
 
12
- export { type ControllerLifecycle, disposeControllerLifecycle };
14
+ export { type ControllerLifecycle, disposeControllerLifecycle, guardControllerLifecycleOnce };
@@ -8,5 +8,7 @@ interface ControllerLifecycle {
8
8
  * for existing page controllers.
9
9
  */
10
10
  declare function disposeControllerLifecycle(target: unknown): Promise<void>;
11
+ /** Share one teardown attempt between the explicit controller owner and IoC. */
12
+ declare function guardControllerLifecycleOnce(target: unknown): void;
11
13
 
12
- export { type ControllerLifecycle, disposeControllerLifecycle };
14
+ export { type ControllerLifecycle, disposeControllerLifecycle, guardControllerLifecycleOnce };
@@ -2,6 +2,7 @@ async function disposeControllerLifecycle(target) {
2
2
  if (!target || typeof target !== "object" && typeof target !== "function") {
3
3
  return;
4
4
  }
5
+ guardControllerLifecycleOnce(target);
5
6
  const lifecycle = target;
6
7
  if (typeof lifecycle.dispose === "function") {
7
8
  await lifecycle.dispose();
@@ -11,7 +12,29 @@ async function disposeControllerLifecycle(target) {
11
12
  await lifecycle.destroy();
12
13
  }
13
14
  }
15
+ const guardedControllers = /* @__PURE__ */ new WeakSet();
16
+ function guardControllerLifecycleOnce(target) {
17
+ if (!target || typeof target !== "object" && typeof target !== "function") return;
18
+ if (guardedControllers.has(target)) return;
19
+ guardedControllers.add(target);
20
+ const instance = target;
21
+ for (const name of ["dispose", "destroy"]) {
22
+ const original = instance[name];
23
+ if (typeof original !== "function") continue;
24
+ let called = false;
25
+ Object.defineProperty(instance, name, {
26
+ configurable: true,
27
+ writable: true,
28
+ value: function(...args) {
29
+ if (called) return;
30
+ called = true;
31
+ return original.apply(this, args);
32
+ }
33
+ });
34
+ }
35
+ }
14
36
  export {
15
- disposeControllerLifecycle
37
+ disposeControllerLifecycle,
38
+ guardControllerLifecycleOnce
16
39
  };
17
40
  //# sourceMappingURL=controller_lifecycle.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n"],"mappings":"AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n guardControllerLifecycleOnce(target);\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n\nconst guardedControllers = new WeakSet<object>();\n\n/** Share one teardown attempt between the explicit controller owner and IoC. */\nexport function guardControllerLifecycleOnce(target: unknown): void {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;\n if (guardedControllers.has(target as object)) return;\n guardedControllers.add(target as object);\n const instance = target as Record<string, unknown>;\n for (const name of ['dispose', 'destroy'] as const) {\n const original = instance[name];\n if (typeof original !== 'function') continue;\n let called = false;\n Object.defineProperty(instance, name, {\n configurable: true, writable: true,\n value: function (this: unknown, ...args: unknown[]) {\n if (called) return;\n called = true;\n return original.apply(this, args);\n },\n });\n }\n}\n"],"mappings":"AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,+BAA6B,MAAM;AACnC,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;AAEA,MAAM,qBAAqB,oBAAI,QAAgB;AAGxC,SAAS,6BAA6B,QAAuB;AAClE,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,WAAa;AAC7E,MAAI,mBAAmB,IAAI,MAAgB,EAAG;AAC9C,qBAAmB,IAAI,MAAgB;AACvC,QAAM,WAAW;AACjB,aAAW,QAAQ,CAAC,WAAW,SAAS,GAAY;AAClD,UAAM,WAAW,SAAS,IAAI;AAC9B,QAAI,OAAO,aAAa,WAAY;AACpC,QAAI,SAAS;AACb,WAAO,eAAe,UAAU,MAAM;AAAA,MACpC,cAAc;AAAA,MAAM,UAAU;AAAA,MAC9B,OAAO,YAA4B,MAAiB;AAClD,YAAI,OAAQ;AACZ,iBAAS;AACT,eAAO,SAAS,MAAM,MAAM,IAAI;AAAA,MAClC;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -21,6 +21,7 @@ __export(frontend_lifecycle_exports, {
21
21
  FrontendLifecycleOwner: () => FrontendLifecycleOwner
22
22
  });
23
23
  module.exports = __toCommonJS(frontend_lifecycle_exports);
24
+ var import_cleanup = require("../controller/cleanup.js");
24
25
  var import_work_registry = require("./work_registry.js");
25
26
  class FrontendLifecycleOwner {
26
27
  constructor(registry, owner) {
@@ -56,15 +57,12 @@ class FrontendLifecycleOwner {
56
57
  get isDisposed() {
57
58
  return this.disposed;
58
59
  }
59
- /** Abort owned work and dispose owned resources in reverse order. */
60
- async dispose() {
61
- if (this.disposed) return;
60
+ dispose() {
61
+ if (this.closing) return this.closing;
62
62
  this.disposed = true;
63
63
  this.abortController.abort();
64
- for (const resource of this.resources.reverse()) {
65
- await resource.dispose();
66
- }
67
- this.resources.length = 0;
64
+ const resources = this.resources.splice(0).reverse();
65
+ return this.closing = (0, import_cleanup.cleanupAll)(resources.map((resource) => () => resource.dispose()), "Wood frontend lifecycle cleanup failed");
68
66
  }
69
67
  }
70
68
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n this.abortController.abort();\n for (const resource of this.resources.reverse()) {\n await resource.dispose();\n }\n this.resources.length = 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,2BAAgD;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,QAAI,KAAK,SAAU;AACnB,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,eAAW,YAAY,KAAK,UAAU,QAAQ,GAAG;AAC/C,YAAM,SAAS,QAAQ;AAAA,IACzB;AACA,SAAK,UAAU,SAAS;AAAA,EAC1B;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n private closing?: Promise<void>;\n dispose(): Promise<void> {\n if (this.closing) return this.closing;\n this.disposed = true;\n this.abortController.abort();\n const resources = this.resources.splice(0).reverse();\n return this.closing = cleanupAll(resources.map(resource => () => resource.dispose()), 'Wood frontend lifecycle cleanup failed');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAA2B;AAC3B,2BAAgD;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAIA,UAAyB;AACvB,QAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,UAAM,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,QAAQ;AACnD,WAAO,KAAK,cAAU,2BAAW,UAAU,IAAI,cAAY,MAAM,SAAS,QAAQ,CAAC,GAAG,wCAAwC;AAAA,EAChI;AACF;","names":[]}
@@ -1,19 +1,5 @@
1
1
  import { WorkRegistry } from './work_registry.cjs';
2
2
 
3
- /**
4
- * Production lifecycle owners for deliberately detached frontend work
5
- * (18A D-FAS-01 §2.2). Two natural owners exist per environment:
6
- *
7
- * page lifecycle — default for PageController work; aborted and
8
- * disposed when that page ends
9
- * application lifecycle — explicit for work that intentionally survives
10
- * page navigation; disposed with the environment
11
- *
12
- * PageControllers/client services register their own deliberately detached
13
- * finite work through their injected lifecycle owner — tests do not repair
14
- * unowned product code by wrapping it in track() from the outside.
15
- */
16
-
17
3
  interface FrontendLifecycle {
18
4
  readonly signal: AbortSignal;
19
5
  track<T>(work: Promise<T>, options: {
@@ -44,6 +30,7 @@ declare class FrontendLifecycleOwner implements FrontendLifecycle {
44
30
  }): void;
45
31
  get isDisposed(): boolean;
46
32
  /** Abort owned work and dispose owned resources in reverse order. */
33
+ private closing?;
47
34
  dispose(): Promise<void>;
48
35
  }
49
36
 
@@ -1,19 +1,5 @@
1
1
  import { WorkRegistry } from './work_registry.js';
2
2
 
3
- /**
4
- * Production lifecycle owners for deliberately detached frontend work
5
- * (18A D-FAS-01 §2.2). Two natural owners exist per environment:
6
- *
7
- * page lifecycle — default for PageController work; aborted and
8
- * disposed when that page ends
9
- * application lifecycle — explicit for work that intentionally survives
10
- * page navigation; disposed with the environment
11
- *
12
- * PageControllers/client services register their own deliberately detached
13
- * finite work through their injected lifecycle owner — tests do not repair
14
- * unowned product code by wrapping it in track() from the outside.
15
- */
16
-
17
3
  interface FrontendLifecycle {
18
4
  readonly signal: AbortSignal;
19
5
  track<T>(work: Promise<T>, options: {
@@ -44,6 +30,7 @@ declare class FrontendLifecycleOwner implements FrontendLifecycle {
44
30
  }): void;
45
31
  get isDisposed(): boolean;
46
32
  /** Abort owned work and dispose owned resources in reverse order. */
33
+ private closing?;
47
34
  dispose(): Promise<void>;
48
35
  }
49
36
 
@@ -1,3 +1,4 @@
1
+ import { cleanupAll } from "../controller/cleanup.js";
1
2
  import { FrontendWorkError } from "./work_registry.js";
2
3
  class FrontendLifecycleOwner {
3
4
  constructor(registry, owner) {
@@ -33,15 +34,12 @@ class FrontendLifecycleOwner {
33
34
  get isDisposed() {
34
35
  return this.disposed;
35
36
  }
36
- /** Abort owned work and dispose owned resources in reverse order. */
37
- async dispose() {
38
- if (this.disposed) return;
37
+ dispose() {
38
+ if (this.closing) return this.closing;
39
39
  this.disposed = true;
40
40
  this.abortController.abort();
41
- for (const resource of this.resources.reverse()) {
42
- await resource.dispose();
43
- }
44
- this.resources.length = 0;
41
+ const resources = this.resources.splice(0).reverse();
42
+ return this.closing = cleanupAll(resources.map((resource) => () => resource.dispose()), "Wood frontend lifecycle cleanup failed");
45
43
  }
46
44
  }
47
45
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n async dispose(): Promise<void> {\n if (this.disposed) return;\n this.disposed = true;\n this.abortController.abort();\n for (const resource of this.resources.reverse()) {\n await resource.dispose();\n }\n this.resources.length = 0;\n }\n}\n"],"mappings":"AAaA,SAAuB,yBAAyB;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,MAAM,UAAyB;AAC7B,QAAI,KAAK,SAAU;AACnB,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,eAAW,YAAY,KAAK,UAAU,QAAQ,GAAG;AAC/C,YAAM,SAAS,QAAQ;AAAA,IACzB;AACA,SAAK,UAAU,SAAS;AAAA,EAC1B;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n private closing?: Promise<void>;\n dispose(): Promise<void> {\n if (this.closing) return this.closing;\n this.disposed = true;\n this.abortController.abort();\n const resources = this.resources.splice(0).reverse();\n return this.closing = cleanupAll(resources.map(resource => () => resource.dispose()), 'Wood frontend lifecycle cleanup failed');\n }\n}\n"],"mappings":"AAaA,SAAS,kBAAkB;AAC3B,SAAuB,yBAAyB;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAIA,UAAyB;AACvB,QAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,UAAM,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,QAAQ;AACnD,WAAO,KAAK,UAAU,WAAW,UAAU,IAAI,cAAY,MAAM,SAAS,QAAQ,CAAC,GAAG,wCAAwC;AAAA,EAChI;AACF;","names":[]}