@noego/wood 0.7.3 → 0.7.4
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/bin/wood.js +9 -1
- package/dist/codegen/renderer_app_generator.cjs +14 -13
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +14 -13
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/navigation/index.cjs +14 -0
- package/dist/navigation/index.cjs.map +1 -1
- package/dist/navigation/index.d.cts +3 -1
- package/dist/navigation/index.d.ts +3 -1
- package/dist/navigation/index.js +13 -0
- package/dist/navigation/index.js.map +1 -1
- package/dist/testing/native_browser/happy_dom_tab.cjs +3 -2
- package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -1
- package/dist/testing/native_browser/happy_dom_tab.js +3 -2
- package/dist/testing/native_browser/happy_dom_tab.js.map +1 -1
- package/dist/testing/native_browser/host_transport.cjs +18 -8
- package/dist/testing/native_browser/host_transport.cjs.map +1 -1
- package/dist/testing/native_browser/host_transport.d.cts +3 -3
- package/dist/testing/native_browser/host_transport.d.ts +3 -3
- package/dist/testing/native_browser/host_transport.js +18 -8
- package/dist/testing/native_browser/host_transport.js.map +1 -1
- package/dist/testing/native_browser/playwright_tab.cjs +4 -6
- package/dist/testing/native_browser/playwright_tab.cjs.map +1 -1
- package/dist/testing/native_browser/playwright_tab.js +4 -6
- package/dist/testing/native_browser/playwright_tab.js.map +1 -1
- package/dist/testing/native_browser/renderer_entry.cjs +5 -2
- package/dist/testing/native_browser/renderer_entry.cjs.map +1 -1
- package/dist/testing/native_browser/renderer_entry.js +5 -2
- package/dist/testing/native_browser/renderer_entry.js.map +1 -1
- package/docs/native-browser-testing.md +11 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nimport type { NavigationOptions } from './layout_preservation.js';\nexport type { NavigationOptions } from './layout_preservation.js';\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.js';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.js';\nexport { computePreservedLayoutDepth } from './layout_preservation.js';\nexport type { LayoutStack } from './layout_preservation.js';\n// Shared page/layout data-loader orchestration for the generated renderer app.\nexport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders, assertDataLoader, NO_LOADERS } from '../loader/loader_orchestration.js';\nexport type { DataLoader, LoadedData, RouteInput, ViewLoaderResult, ViewLoaderSet } from '../loader/loader_orchestration.js';\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/navigation/index.ts"],"sourcesContent":["import { emitRendererTrace } from '../tracing/index.js';\n\nconst NAVIGATION_SYMBOL = Symbol.for('@noego/wood.navigation');\nconst HISTORY_STATE_MARKER = '__wood_navigation_state__';\n\nexport interface NavigationEntry {\n page: string;\n params: Record<string, string>;\n query: Record<string, string>;\n options: NavigationOptions;\n}\n\nimport type { NavigationOptions } from './layout_preservation.js';\nexport type { NavigationOptions } from './layout_preservation.js';\n\ntype NavigationHistoryState = NavigationEntry & {\n [HISTORY_STATE_MARKER]: true;\n};\n\n/** Validate persisted route data before a new document uses it for bootstrap. */\nexport function readNavigationHistory(state: unknown): Pick<NavigationEntry, 'page' | 'params' | 'query'> | null {\n if (!state || typeof state !== 'object' || Array.isArray(state)) return null;\n const value = state as Record<string, unknown>;\n if (value[HISTORY_STATE_MARKER] !== true || typeof value.page !== 'string' || !value.page.trim()) return null;\n const record = (input: unknown): input is Record<string, string> => {\n if (!input || typeof input !== 'object' || Array.isArray(input)) return false;\n const prototype = Object.getPrototypeOf(input);\n return (prototype === Object.prototype || prototype === null) && Object.values(input).every(item => typeof item === 'string');\n };\n if (!record(value.params) || !record(value.query)) return null;\n return {page: value.page, params: {...value.params}, query: {...value.query}};\n}\n\ntype NavigationListener = (entry: NavigationEntry | null) => void;\n\nexport class Navigation {\n private entries: NavigationEntry[] = [];\n private index = -1;\n private listeners = new Set<NavigationListener>();\n\n getCurrent(): NavigationEntry | null {\n if (this.index < 0 || this.index >= this.entries.length) {\n return null;\n }\n return this.cloneEntry(this.entries[this.index]);\n }\n\n getPages(): NavigationEntry[] {\n return this.entries.map((entry) => this.cloneEntry(entry));\n }\n\n getParams(): Record<string, string> {\n const current = this.getCurrent();\n if (!current) {\n return {};\n }\n return { ...current.params };\n }\n\n replace(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n\n if (this.index < 0) {\n this.entries.push(entry);\n this.index = 0;\n } else {\n this.entries[this.index] = entry;\n }\n\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.replace',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n ): NavigationEntry {\n const entry = this.createEntry(page, params, query, options);\n if (this.index < this.entries.length - 1) {\n this.entries = this.entries.slice(0, this.index + 1);\n }\n this.entries.push(entry);\n this.index = this.entries.length - 1;\n\n this.writeWindowHistory('pushState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.go',\n payload: {\n page: entry.page,\n params: entry.params,\n query: entry.query,\n options: entry.options,\n },\n });\n return this.cloneEntry(entry);\n }\n\n goto(delta: number): NavigationEntry | null {\n return this.moveCursor(delta);\n }\n\n goBack(): NavigationEntry | null {\n return this.goto(-1);\n }\n\n goForward(): NavigationEntry | null {\n return this.goto(1);\n }\n\n subscribe(listener: NavigationListener): () => void {\n this.listeners.add(listener);\n listener(this.getCurrent());\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n private moveCursor(delta: number): NavigationEntry | null {\n if (delta === 0) {\n return this.getCurrent();\n }\n\n const nextIndex = this.index + delta;\n if (nextIndex < 0 || nextIndex >= this.entries.length) {\n return this.getCurrent();\n }\n\n this.index = nextIndex;\n const entry = this.entries[this.index];\n this.writeWindowHistory('replaceState', entry);\n this.notify();\n emitRendererTrace('info', {\n source: 'wood.navigation',\n type: 'navigation.goto',\n payload: {\n delta,\n page: entry.page,\n },\n });\n return this.cloneEntry(entry);\n }\n\n private notify(): void {\n const current = this.getCurrent();\n for (const listener of this.listeners) {\n listener(current);\n }\n }\n\n private createEntry(\n page: string,\n params: Record<string, string>,\n query: Record<string, string>,\n options: NavigationOptions,\n ): NavigationEntry {\n return {\n page,\n params: this.normalizeRecord(params),\n query: this.normalizeRecord(query),\n options: { ...options },\n };\n }\n\n private cloneEntry(entry: NavigationEntry): NavigationEntry {\n return {\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n }\n\n private normalizeRecord(input: Record<string, string>): Record<string, string> {\n const output: Record<string, string> = {};\n for (const [key, value] of Object.entries(input ?? {})) {\n output[String(key)] = String(value);\n }\n return output;\n }\n\n private writeWindowHistory(\n mode: 'pushState' | 'replaceState',\n entry: NavigationEntry,\n ): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n const state: NavigationHistoryState = {\n [HISTORY_STATE_MARKER]: true,\n page: entry.page,\n params: { ...entry.params },\n query: { ...entry.query },\n options: { ...entry.options },\n };\n\n window.history[mode](state, '');\n }\n}\n\ntype GlobalWithNavigation = typeof globalThis & {\n [NAVIGATION_SYMBOL]?: Navigation;\n};\n\nfunction createNavigationInstance(): Navigation {\n return new Navigation();\n}\n\nexport function getNavigation(): Navigation {\n const globalObj = globalThis as GlobalWithNavigation;\n if (!globalObj[NAVIGATION_SYMBOL]) {\n globalObj[NAVIGATION_SYMBOL] = createNavigationInstance();\n }\n return globalObj[NAVIGATION_SYMBOL];\n}\n\nexport function goto(\n delta: number,\n): NavigationEntry | null {\n return getNavigation().goto(delta);\n}\n\nexport function go(\n page: string,\n params: Record<string, string> = {},\n query: Record<string, string> = {},\n options: NavigationOptions = {},\n): NavigationEntry {\n return getNavigation().go(page, params, query, options);\n}\n\nexport function goback(): NavigationEntry | null {\n return goto(-1);\n}\n\n/** Test-only helper to clear the navigation singleton. */\nexport function clearNavigationForTests(): void {\n const globalObj = globalThis as GlobalWithNavigation;\n delete globalObj[NAVIGATION_SYMBOL];\n}\n\n// Dev/test-only automation introspection surface (Axe AXE-WP-02).\n// Benign import cycle: automation_introspection uses getNavigation lazily.\n// NOTE: keep these re-exports single-line — the tsup extension-rewrite plugin\n// only rewrites single-line import/export specifiers.\nexport { installAutomationIntrospection, WOOD_AUTOMATION_GLOBAL_KEY } from './automation_introspection.js';\nexport type { WoodAutomationSnapshot, WoodAutomationEvent, WoodAutomationIntrospection, InstallAutomationIntrospectionOptions } from './automation_introspection.js';\nexport { computePreservedLayoutDepth } from './layout_preservation.js';\nexport type { LayoutStack } from './layout_preservation.js';\n// Shared page/layout data-loader orchestration for the generated renderer app.\nexport { createLoaderRequest, initializeArgument, initializeController, runViewLoaders, assertDataLoader, NO_LOADERS } from '../loader/loader_orchestration.js';\nexport type { DataLoader, LoadedData, RouteInput, ViewLoaderResult, ViewLoaderSet } from '../loader/loader_orchestration.js';\n"],"mappings":"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","names":[]}
|
|
@@ -47,8 +47,8 @@ async function openHappyDomTab(inputs, bundle, route) {
|
|
|
47
47
|
const networkHost = (0, import_browser.installHappyDomFetch)(window, (request) => inputs.network.dispatch(request));
|
|
48
48
|
const intoWindow = (0, import_happy_dom_transfer.createHappyDomTransfer)(window);
|
|
49
49
|
const globals = window;
|
|
50
|
-
const transport = (0, import_host_transport.nativeHostTransport)(inputs, async (channel, payload) => {
|
|
51
|
-
globals.__woodNativeEmit(channel, intoWindow(payload));
|
|
50
|
+
const transport = (0, import_host_transport.nativeHostTransport)(inputs, async (channel, payload, documentId) => {
|
|
51
|
+
if (globals.__woodNativeDocumentId === documentId) globals.__woodNativeEmit(channel, intoWindow(payload));
|
|
52
52
|
});
|
|
53
53
|
Object.assign(window, {
|
|
54
54
|
global: window,
|
|
@@ -66,6 +66,7 @@ async function openHappyDomTab(inputs, bundle, route) {
|
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
__woodNativeSubscribe: transport.subscribe,
|
|
69
|
+
__woodNativeResetSubscriptions: transport.resetSubscriptions,
|
|
69
70
|
__woodNativeTrace: (payload) => transport.trace(structuredClone(payload)),
|
|
70
71
|
__woodNativeLog: (payload) => transport.log(structuredClone(payload))
|
|
71
72
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/happy_dom_tab.ts"],"sourcesContent":["import { installHappyDomFetch } from '@noego/testing/browser';\nimport { createHappyDomTransfer } from './happy_dom_transfer.js';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\nexport async function openHappyDomTab(\n inputs: NativeBrowserInputs, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const { Window } = await import('happy-dom');\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n const window = new Window({\n url: `https://wood.test/${encodeURIComponent(route.page)}`,\n width: windowConfig?.width ?? 1024, height: windowConfig?.height ?? 768,\n settings: {enableJavaScriptEvaluation: true},\n });\n // The Window owns its VM, timers, DOM and storage. Never install it on Node\n // globalThis or replace Vitest's globals; simultaneous cases remain isolated.\n const networkHost = installHappyDomFetch(window, request => inputs.network.dispatch(request));\n const intoWindow = createHappyDomTransfer(window);\n const globals = window as unknown as Record<string, any>;\n const transport = nativeHostTransport(inputs, async (channel, payload) => {\n globals.__woodNativeEmit(channel, intoWindow(payload));\n });\n Object.assign(window, {\n global: window,\n // happy-dom's Window lacks the structuredClone global renderer code and\n // the bridge client rely on; lend the host's implementation.\n structuredClone: globalThis.structuredClone,\n __woodNativeRoute: intoWindow(route),\n __woodNativeInvoke: async (channel: string, payload: unknown) => {\n try { return intoWindow(await transport.invoke(channel, structuredClone(payload))); }\n catch (error) {\n const nativeError = new window.Error(error instanceof Error ? error.message : String(error));\n if (error instanceof Error) nativeError.name = error.name;\n throw nativeError;\n }\n },\n __woodNativeSubscribe: transport.subscribe,\n __woodNativeTrace: (payload: unknown) => transport.trace(structuredClone(payload)),\n __woodNativeLog: (payload: unknown) => transport.log(structuredClone(payload)),\n });\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood happy-dom window cleanup failed', [\n () => hostDeadline(Promise.resolve(globals.__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => hostDeadline(transport.flush(), 'event drain'),\n () => networkHost.dispose(),\n () => window.happyDOM.close(),\n ]);\n try {\n window.document.body.innerHTML = '<div id=\"app\"></div>';\n if (bundle.css) {\n const style = window.document.createElement('style');\n style.textContent = bundle.css;\n window.document.head.appendChild(style);\n }\n window.eval(bundle.code);\n await hostDeadline(Promise.resolve(globals.__woodNativeReady), 'initial mount');\n return {\n native: window,\n async flush() {\n await hostDeadline(networkHost.settle(), 'external fetch drain');\n await hostDeadline(Promise.resolve(globals.__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await globals.__woodNativeFlush();\n },\n dispose,\n };\n } catch (error) {\n await disposeNativeResources('Wood happy-dom open failed', [() => {throw error;}, dispose]);\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAqC;AACrC,gCAAuC;AAEvC,4BAAoC;AACpC,sCAAqD;AAErD,eAAsB,gBACpB,QAA6B,QAA8B,OAChC;AAC3B,QAAM,EAAE,OAAO,IAAI,MAAM,OAAO,WAAW;AAC3C,QAAM,eAAe,OAAO,OAAO,OAAO,QAAQ,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,CAAE;AAC3E,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,KAAK,qBAAqB,mBAAmB,MAAM,IAAI,CAAC;AAAA,IACxD,OAAO,cAAc,SAAS;AAAA,IAAM,QAAQ,cAAc,UAAU;AAAA,IACpE,UAAU,EAAC,4BAA4B,KAAI;AAAA,EAC7C,CAAC;AAGD,QAAM,kBAAc,qCAAqB,QAAQ,aAAW,OAAO,QAAQ,SAAS,OAAO,CAAC;AAC5F,QAAM,iBAAa,kDAAuB,MAAM;AAChD,QAAM,UAAU;AAChB,QAAM,gBAAY,2CAAoB,QAAQ,OAAO,SAAS,
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/happy_dom_tab.ts"],"sourcesContent":["import { installHappyDomFetch } from '@noego/testing/browser';\nimport { createHappyDomTransfer } from './happy_dom_transfer.js';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\nexport async function openHappyDomTab(\n inputs: NativeBrowserInputs, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const { Window } = await import('happy-dom');\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n const window = new Window({\n url: `https://wood.test/${encodeURIComponent(route.page)}`,\n width: windowConfig?.width ?? 1024, height: windowConfig?.height ?? 768,\n settings: {enableJavaScriptEvaluation: true},\n });\n // The Window owns its VM, timers, DOM and storage. Never install it on Node\n // globalThis or replace Vitest's globals; simultaneous cases remain isolated.\n const networkHost = installHappyDomFetch(window, request => inputs.network.dispatch(request));\n const intoWindow = createHappyDomTransfer(window);\n const globals = window as unknown as Record<string, any>;\n const transport = nativeHostTransport(inputs, async (channel, payload, documentId) => {\n if (globals.__woodNativeDocumentId === documentId) globals.__woodNativeEmit(channel, intoWindow(payload));\n });\n Object.assign(window, {\n global: window,\n // happy-dom's Window lacks the structuredClone global renderer code and\n // the bridge client rely on; lend the host's implementation.\n structuredClone: globalThis.structuredClone,\n __woodNativeRoute: intoWindow(route),\n __woodNativeInvoke: async (channel: string, payload: unknown) => {\n try { return intoWindow(await transport.invoke(channel, structuredClone(payload))); }\n catch (error) {\n const nativeError = new window.Error(error instanceof Error ? error.message : String(error));\n if (error instanceof Error) nativeError.name = error.name;\n throw nativeError;\n }\n },\n __woodNativeSubscribe: transport.subscribe,\n __woodNativeResetSubscriptions: transport.resetSubscriptions,\n __woodNativeTrace: (payload: unknown) => transport.trace(structuredClone(payload)),\n __woodNativeLog: (payload: unknown) => transport.log(structuredClone(payload)),\n });\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood happy-dom window cleanup failed', [\n () => hostDeadline(Promise.resolve(globals.__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => hostDeadline(transport.flush(), 'event drain'),\n () => networkHost.dispose(),\n () => window.happyDOM.close(),\n ]);\n try {\n window.document.body.innerHTML = '<div id=\"app\"></div>';\n if (bundle.css) {\n const style = window.document.createElement('style');\n style.textContent = bundle.css;\n window.document.head.appendChild(style);\n }\n window.eval(bundle.code);\n await hostDeadline(Promise.resolve(globals.__woodNativeReady), 'initial mount');\n return {\n native: window,\n async flush() {\n await hostDeadline(networkHost.settle(), 'external fetch drain');\n await hostDeadline(Promise.resolve(globals.__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await globals.__woodNativeFlush();\n },\n dispose,\n };\n } catch (error) {\n await disposeNativeResources('Wood happy-dom open failed', [() => {throw error;}, dispose]);\n throw error;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -14,8 +14,8 @@ async function openHappyDomTab(inputs, bundle, route) {
|
|
|
14
14
|
const networkHost = installHappyDomFetch(window, (request) => inputs.network.dispatch(request));
|
|
15
15
|
const intoWindow = createHappyDomTransfer(window);
|
|
16
16
|
const globals = window;
|
|
17
|
-
const transport = nativeHostTransport(inputs, async (channel, payload) => {
|
|
18
|
-
globals.__woodNativeEmit(channel, intoWindow(payload));
|
|
17
|
+
const transport = nativeHostTransport(inputs, async (channel, payload, documentId) => {
|
|
18
|
+
if (globals.__woodNativeDocumentId === documentId) globals.__woodNativeEmit(channel, intoWindow(payload));
|
|
19
19
|
});
|
|
20
20
|
Object.assign(window, {
|
|
21
21
|
global: window,
|
|
@@ -33,6 +33,7 @@ async function openHappyDomTab(inputs, bundle, route) {
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
__woodNativeSubscribe: transport.subscribe,
|
|
36
|
+
__woodNativeResetSubscriptions: transport.resetSubscriptions,
|
|
36
37
|
__woodNativeTrace: (payload) => transport.trace(structuredClone(payload)),
|
|
37
38
|
__woodNativeLog: (payload) => transport.log(structuredClone(payload))
|
|
38
39
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/happy_dom_tab.ts"],"sourcesContent":["import { installHappyDomFetch } from '@noego/testing/browser';\nimport { createHappyDomTransfer } from './happy_dom_transfer.js';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\nexport async function openHappyDomTab(\n inputs: NativeBrowserInputs, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const { Window } = await import('happy-dom');\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n const window = new Window({\n url: `https://wood.test/${encodeURIComponent(route.page)}`,\n width: windowConfig?.width ?? 1024, height: windowConfig?.height ?? 768,\n settings: {enableJavaScriptEvaluation: true},\n });\n // The Window owns its VM, timers, DOM and storage. Never install it on Node\n // globalThis or replace Vitest's globals; simultaneous cases remain isolated.\n const networkHost = installHappyDomFetch(window, request => inputs.network.dispatch(request));\n const intoWindow = createHappyDomTransfer(window);\n const globals = window as unknown as Record<string, any>;\n const transport = nativeHostTransport(inputs, async (channel, payload) => {\n globals.__woodNativeEmit(channel, intoWindow(payload));\n });\n Object.assign(window, {\n global: window,\n // happy-dom's Window lacks the structuredClone global renderer code and\n // the bridge client rely on; lend the host's implementation.\n structuredClone: globalThis.structuredClone,\n __woodNativeRoute: intoWindow(route),\n __woodNativeInvoke: async (channel: string, payload: unknown) => {\n try { return intoWindow(await transport.invoke(channel, structuredClone(payload))); }\n catch (error) {\n const nativeError = new window.Error(error instanceof Error ? error.message : String(error));\n if (error instanceof Error) nativeError.name = error.name;\n throw nativeError;\n }\n },\n __woodNativeSubscribe: transport.subscribe,\n __woodNativeTrace: (payload: unknown) => transport.trace(structuredClone(payload)),\n __woodNativeLog: (payload: unknown) => transport.log(structuredClone(payload)),\n });\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood happy-dom window cleanup failed', [\n () => hostDeadline(Promise.resolve(globals.__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => hostDeadline(transport.flush(), 'event drain'),\n () => networkHost.dispose(),\n () => window.happyDOM.close(),\n ]);\n try {\n window.document.body.innerHTML = '<div id=\"app\"></div>';\n if (bundle.css) {\n const style = window.document.createElement('style');\n style.textContent = bundle.css;\n window.document.head.appendChild(style);\n }\n window.eval(bundle.code);\n await hostDeadline(Promise.resolve(globals.__woodNativeReady), 'initial mount');\n return {\n native: window,\n async flush() {\n await hostDeadline(networkHost.settle(), 'external fetch drain');\n await hostDeadline(Promise.resolve(globals.__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await globals.__woodNativeFlush();\n },\n dispose,\n };\n } catch (error) {\n await disposeNativeResources('Wood happy-dom open failed', [() => {throw error;}, dispose]);\n throw error;\n }\n}\n"],"mappings":"AAAA,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB,oBAAoB;AAErD,eAAsB,gBACpB,QAA6B,QAA8B,OAChC;AAC3B,QAAM,EAAE,OAAO,IAAI,MAAM,OAAO,WAAW;AAC3C,QAAM,eAAe,OAAO,OAAO,OAAO,QAAQ,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,CAAE;AAC3E,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,KAAK,qBAAqB,mBAAmB,MAAM,IAAI,CAAC;AAAA,IACxD,OAAO,cAAc,SAAS;AAAA,IAAM,QAAQ,cAAc,UAAU;AAAA,IACpE,UAAU,EAAC,4BAA4B,KAAI;AAAA,EAC7C,CAAC;AAGD,QAAM,cAAc,qBAAqB,QAAQ,aAAW,OAAO,QAAQ,SAAS,OAAO,CAAC;AAC5F,QAAM,aAAa,uBAAuB,MAAM;AAChD,QAAM,UAAU;AAChB,QAAM,YAAY,oBAAoB,QAAQ,OAAO,SAAS,
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/happy_dom_tab.ts"],"sourcesContent":["import { installHappyDomFetch } from '@noego/testing/browser';\nimport { createHappyDomTransfer } from './happy_dom_transfer.js';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\nexport async function openHappyDomTab(\n inputs: NativeBrowserInputs, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const { Window } = await import('happy-dom');\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n const window = new Window({\n url: `https://wood.test/${encodeURIComponent(route.page)}`,\n width: windowConfig?.width ?? 1024, height: windowConfig?.height ?? 768,\n settings: {enableJavaScriptEvaluation: true},\n });\n // The Window owns its VM, timers, DOM and storage. Never install it on Node\n // globalThis or replace Vitest's globals; simultaneous cases remain isolated.\n const networkHost = installHappyDomFetch(window, request => inputs.network.dispatch(request));\n const intoWindow = createHappyDomTransfer(window);\n const globals = window as unknown as Record<string, any>;\n const transport = nativeHostTransport(inputs, async (channel, payload, documentId) => {\n if (globals.__woodNativeDocumentId === documentId) globals.__woodNativeEmit(channel, intoWindow(payload));\n });\n Object.assign(window, {\n global: window,\n // happy-dom's Window lacks the structuredClone global renderer code and\n // the bridge client rely on; lend the host's implementation.\n structuredClone: globalThis.structuredClone,\n __woodNativeRoute: intoWindow(route),\n __woodNativeInvoke: async (channel: string, payload: unknown) => {\n try { return intoWindow(await transport.invoke(channel, structuredClone(payload))); }\n catch (error) {\n const nativeError = new window.Error(error instanceof Error ? error.message : String(error));\n if (error instanceof Error) nativeError.name = error.name;\n throw nativeError;\n }\n },\n __woodNativeSubscribe: transport.subscribe,\n __woodNativeResetSubscriptions: transport.resetSubscriptions,\n __woodNativeTrace: (payload: unknown) => transport.trace(structuredClone(payload)),\n __woodNativeLog: (payload: unknown) => transport.log(structuredClone(payload)),\n });\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood happy-dom window cleanup failed', [\n () => hostDeadline(Promise.resolve(globals.__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => hostDeadline(transport.flush(), 'event drain'),\n () => networkHost.dispose(),\n () => window.happyDOM.close(),\n ]);\n try {\n window.document.body.innerHTML = '<div id=\"app\"></div>';\n if (bundle.css) {\n const style = window.document.createElement('style');\n style.textContent = bundle.css;\n window.document.head.appendChild(style);\n }\n window.eval(bundle.code);\n await hostDeadline(Promise.resolve(globals.__woodNativeReady), 'initial mount');\n return {\n native: window,\n async flush() {\n await hostDeadline(networkHost.settle(), 'external fetch drain');\n await hostDeadline(Promise.resolve(globals.__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await globals.__woodNativeFlush();\n },\n dispose,\n };\n } catch (error) {\n await disposeNativeResources('Wood happy-dom open failed', [() => {throw error;}, dispose]);\n throw error;\n }\n}\n"],"mappings":"AAAA,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,wBAAwB,oBAAoB;AAErD,eAAsB,gBACpB,QAA6B,QAA8B,OAChC;AAC3B,QAAM,EAAE,OAAO,IAAI,MAAM,OAAO,WAAW;AAC3C,QAAM,eAAe,OAAO,OAAO,OAAO,QAAQ,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,CAAE;AAC3E,QAAM,SAAS,IAAI,OAAO;AAAA,IACxB,KAAK,qBAAqB,mBAAmB,MAAM,IAAI,CAAC;AAAA,IACxD,OAAO,cAAc,SAAS;AAAA,IAAM,QAAQ,cAAc,UAAU;AAAA,IACpE,UAAU,EAAC,4BAA4B,KAAI;AAAA,EAC7C,CAAC;AAGD,QAAM,cAAc,qBAAqB,QAAQ,aAAW,OAAO,QAAQ,SAAS,OAAO,CAAC;AAC5F,QAAM,aAAa,uBAAuB,MAAM;AAChD,QAAM,UAAU;AAChB,QAAM,YAAY,oBAAoB,QAAQ,OAAO,SAAS,SAAS,eAAe;AACpF,QAAI,QAAQ,2BAA2B,WAAY,SAAQ,iBAAiB,SAAS,WAAW,OAAO,CAAC;AAAA,EAC1G,CAAC;AACD,SAAO,OAAO,QAAQ;AAAA,IACpB,QAAQ;AAAA;AAAA;AAAA,IAGR,iBAAiB,WAAW;AAAA,IAC5B,mBAAmB,WAAW,KAAK;AAAA,IACnC,oBAAoB,OAAO,SAAiB,YAAqB;AAC/D,UAAI;AAAE,eAAO,WAAW,MAAM,UAAU,OAAO,SAAS,gBAAgB,OAAO,CAAC,CAAC;AAAA,MAAG,SAC7E,OAAO;AACZ,cAAM,cAAc,IAAI,OAAO,MAAM,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAC3F,YAAI,iBAAiB,MAAO,aAAY,OAAO,MAAM;AACrD,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,uBAAuB,UAAU;AAAA,IACjC,gCAAgC,UAAU;AAAA,IAC1C,mBAAmB,CAAC,YAAqB,UAAU,MAAM,gBAAgB,OAAO,CAAC;AAAA,IACjF,iBAAiB,CAAC,YAAqB,UAAU,IAAI,gBAAgB,OAAO,CAAC;AAAA,EAC/E,CAAC;AACD,MAAI,UAAgC;AACpC,QAAM,UAAU,MAAM,sBAAY,uBAAuB,wCAAwC;AAAA,IAC/F,MAAM,aAAa,QAAQ,QAAQ,QAAQ,sBAAsB,CAAC,GAAG,mBAAmB;AAAA,IACxF,MAAM,UAAU,QAAQ;AAAA,IACxB,MAAM,aAAa,UAAU,MAAM,GAAG,aAAa;AAAA,IACnD,MAAM,YAAY,QAAQ;AAAA,IAC1B,MAAM,OAAO,SAAS,MAAM;AAAA,EAC9B,CAAC;AACD,MAAI;AACF,WAAO,SAAS,KAAK,YAAY;AACjC,QAAI,OAAO,KAAK;AACd,YAAM,QAAQ,OAAO,SAAS,cAAc,OAAO;AACnD,YAAM,cAAc,OAAO;AAC3B,aAAO,SAAS,KAAK,YAAY,KAAK;AAAA,IACxC;AACA,WAAO,KAAK,OAAO,IAAI;AACvB,UAAM,aAAa,QAAQ,QAAQ,QAAQ,iBAAiB,GAAG,eAAe;AAC9E,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,QAAQ;AACZ,cAAM,aAAa,YAAY,OAAO,GAAG,sBAAsB;AAC/D,cAAM,aAAa,QAAQ,QAAQ,QAAQ,kBAAkB,CAAC,GAAG,gBAAgB;AACjF,cAAM,aAAa,UAAU,MAAM,GAAG,aAAa;AACnD,cAAM,QAAQ,kBAAkB;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,UAAM,uBAAuB,8BAA8B,CAAC,MAAM;AAAC,YAAM;AAAA,IAAM,GAAG,OAAO,CAAC;AAC1F,UAAM;AAAA,EACR;AACF;","names":[]}
|
|
@@ -24,10 +24,11 @@ module.exports = __toCommonJS(host_transport_exports);
|
|
|
24
24
|
var import_renderer_log_batch_receiver = require("../../runtime/renderer_log_batch_receiver.js");
|
|
25
25
|
var import_ioc = require("@noego/ioc");
|
|
26
26
|
var import_tracing = require("../../tracing/index.js");
|
|
27
|
-
function nativeHostTransport(inputs, emit) {
|
|
27
|
+
function nativeHostTransport(inputs, emit, isDestroyedContext = () => false) {
|
|
28
28
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
29
29
|
const deliveries = /* @__PURE__ */ new Set();
|
|
30
30
|
const failures = [];
|
|
31
|
+
let documentId = 0;
|
|
31
32
|
let closed = false;
|
|
32
33
|
const assertOpen = () => {
|
|
33
34
|
if (closed) throw new Error("Wood native tab is disposed");
|
|
@@ -38,8 +39,9 @@ function nativeHostTransport(inputs, emit) {
|
|
|
38
39
|
if (typeof channel !== "string") return Promise.reject(new TypeError("Wood operation channel must be a string"));
|
|
39
40
|
return inputs.environment.call(channel, payload);
|
|
40
41
|
},
|
|
41
|
-
subscribe(channel, enabled) {
|
|
42
|
+
subscribe(channel, enabled, owner = documentId) {
|
|
42
43
|
assertOpen();
|
|
44
|
+
if (owner !== documentId) return;
|
|
43
45
|
if (typeof channel !== "string" || typeof enabled !== "boolean") throw new TypeError("Invalid Wood subscription");
|
|
44
46
|
if (!enabled) {
|
|
45
47
|
subscriptions.get(channel)?.();
|
|
@@ -49,12 +51,16 @@ function nativeHostTransport(inputs, emit) {
|
|
|
49
51
|
if (subscriptions.has(channel)) return;
|
|
50
52
|
subscriptions.set(channel, inputs.events().on(channel, (payload) => {
|
|
51
53
|
if (closed) return;
|
|
52
|
-
const task = emit(channel, payload)
|
|
54
|
+
const task = Promise.resolve().then(() => emit(channel, payload, owner)).then(
|
|
55
|
+
() => {
|
|
56
|
+
deliveries.delete(task);
|
|
57
|
+
},
|
|
58
|
+
(error) => {
|
|
59
|
+
deliveries.delete(task);
|
|
60
|
+
failures.push({ documentId: owner, error });
|
|
61
|
+
}
|
|
62
|
+
);
|
|
53
63
|
deliveries.add(task);
|
|
54
|
-
task.then(() => deliveries.delete(task), (error) => {
|
|
55
|
-
deliveries.delete(task);
|
|
56
|
-
failures.push(error);
|
|
57
|
-
});
|
|
58
64
|
}));
|
|
59
65
|
},
|
|
60
66
|
log(payload) {
|
|
@@ -68,11 +74,15 @@ function nativeHostTransport(inputs, emit) {
|
|
|
68
74
|
},
|
|
69
75
|
async flush() {
|
|
70
76
|
while (deliveries.size) await Promise.all([...deliveries]);
|
|
71
|
-
|
|
77
|
+
const errors = failures.splice(0).filter(
|
|
78
|
+
(failure) => failure.documentId === documentId || !isDestroyedContext(failure.error)
|
|
79
|
+
).map((failure) => failure.error);
|
|
80
|
+
if (errors.length) throw Object.assign(new Error("Wood native event delivery failed"), { errors });
|
|
72
81
|
},
|
|
73
82
|
resetSubscriptions() {
|
|
74
83
|
for (const unsubscribe of subscriptions.values()) unsubscribe();
|
|
75
84
|
subscriptions.clear();
|
|
85
|
+
return ++documentId;
|
|
76
86
|
},
|
|
77
87
|
dispose() {
|
|
78
88
|
if (closed) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/host_transport.ts"],"sourcesContent":["import { dispatchRendererLogBatch } from '../../runtime/renderer_log_batch_receiver.js';\nimport { ExecutionContext } from '@noego/ioc';\nimport { dispatchRendererTracePayload } from '../../tracing/index.js';\nimport type { NativeBrowserInputs } from './native_browser.types.js';\n\n/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */\nexport function nativeHostTransport(inputs: NativeBrowserInputs, emit: (channel: string, payload: unknown) => Promise<void
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/host_transport.ts"],"sourcesContent":["import { dispatchRendererLogBatch } from '../../runtime/renderer_log_batch_receiver.js';\nimport { ExecutionContext } from '@noego/ioc';\nimport { dispatchRendererTracePayload } from '../../tracing/index.js';\nimport type { NativeBrowserInputs } from './native_browser.types.js';\n\n/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */\nexport function nativeHostTransport(\n inputs: Pick<NativeBrowserInputs, 'environment' | 'events'>,\n emit: (channel: string, payload: unknown, documentId: number) => Promise<void>,\n isDestroyedContext: (error: unknown) => boolean = () => false,\n) {\n const subscriptions = new Map<string, () => void>();\n const deliveries = new Set<Promise<void>>();\n const failures: {documentId: number; error: unknown}[] = [];\n let documentId = 0;\n let closed = false;\n const assertOpen = () => { if (closed) throw new Error('Wood native tab is disposed'); };\n return {\n invoke(channel: string, payload: unknown): Promise<unknown> {\n assertOpen();\n if (typeof channel !== 'string') return Promise.reject(new TypeError('Wood operation channel must be a string'));\n // Includes aperture checks, lazy startup, ExecutionContext and the real router.\n return inputs.environment.call(channel, payload);\n },\n subscribe(channel: string, enabled: boolean, owner = documentId): void {\n assertOpen();\n if (owner !== documentId) return;\n if (typeof channel !== 'string' || typeof enabled !== 'boolean') throw new TypeError('Invalid Wood subscription');\n if (!enabled) { subscriptions.get(channel)?.(); subscriptions.delete(channel); return; }\n if (subscriptions.has(channel)) return;\n subscriptions.set(channel, inputs.events().on(channel, (payload: unknown) => {\n if (closed) return;\n // Retain errors until flush can distinguish an obsolete document from\n // a real callback failure. Drain settlement promises, not raw rejects.\n const task = Promise.resolve().then(() => emit(channel, payload, owner)).then(\n () => { deliveries.delete(task); },\n error => { deliveries.delete(task); failures.push({documentId: owner, error}); },\n );\n deliveries.add(task);\n }));\n },\n log(payload: unknown): void {\n assertOpen();\n const result = ExecutionContext.run(inputs.environment.root as never, () => dispatchRendererLogBatch(payload));\n if (!result.ok) throw new Error(`Wood renderer log batch rejected: ${result.reason}`);\n },\n trace(payload: unknown): void {\n assertOpen();\n ExecutionContext.run(inputs.environment.root as never, () => dispatchRendererTracePayload(payload));\n },\n async flush(): Promise<void> {\n while (deliveries.size) await Promise.all([...deliveries]);\n const errors = failures.splice(0).filter(failure =>\n failure.documentId === documentId || !isDestroyedContext(failure.error),\n ).map(failure => failure.error);\n if (errors.length) throw Object.assign(new Error('Wood native event delivery failed'), {errors});\n },\n resetSubscriptions(): number {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n return ++documentId;\n },\n dispose(): void {\n if (closed) return;\n closed = true;\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yCAAyC;AACzC,iBAAiC;AACjC,qBAA6C;AAItC,SAAS,oBACd,QACA,MACA,qBAAkD,MAAM,OACxD;AACA,QAAM,gBAAgB,oBAAI,IAAwB;AAClD,QAAM,aAAa,oBAAI,IAAmB;AAC1C,QAAM,WAAmD,CAAC;AAC1D,MAAI,aAAa;AACjB,MAAI,SAAS;AACb,QAAM,aAAa,MAAM;AAAE,QAAI,OAAQ,OAAM,IAAI,MAAM,6BAA6B;AAAA,EAAG;AACvF,SAAO;AAAA,IACL,OAAO,SAAiB,SAAoC;AAC1D,iBAAW;AACX,UAAI,OAAO,YAAY,SAAU,QAAO,QAAQ,OAAO,IAAI,UAAU,yCAAyC,CAAC;AAE/G,aAAO,OAAO,YAAY,KAAK,SAAS,OAAO;AAAA,IACjD;AAAA,IACA,UAAU,SAAiB,SAAkB,QAAQ,YAAkB;AACrE,iBAAW;AACX,UAAI,UAAU,WAAY;AAC1B,UAAI,OAAO,YAAY,YAAY,OAAO,YAAY,UAAW,OAAM,IAAI,UAAU,2BAA2B;AAChH,UAAI,CAAC,SAAS;AAAE,sBAAc,IAAI,OAAO,IAAI;AAAG,sBAAc,OAAO,OAAO;AAAG;AAAA,MAAQ;AACvF,UAAI,cAAc,IAAI,OAAO,EAAG;AAChC,oBAAc,IAAI,SAAS,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC,YAAqB;AAC3E,YAAI,OAAQ;AAGZ,cAAM,OAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,KAAK,SAAS,SAAS,KAAK,CAAC,EAAE;AAAA,UACvE,MAAM;AAAE,uBAAW,OAAO,IAAI;AAAA,UAAG;AAAA,UACjC,WAAS;AAAE,uBAAW,OAAO,IAAI;AAAG,qBAAS,KAAK,EAAC,YAAY,OAAO,MAAK,CAAC;AAAA,UAAG;AAAA,QACjF;AACA,mBAAW,IAAI,IAAI;AAAA,MACrB,CAAC,CAAC;AAAA,IACJ;AAAA,IACA,IAAI,SAAwB;AAC1B,iBAAW;AACX,YAAM,SAAS,4BAAiB,IAAI,OAAO,YAAY,MAAe,UAAM,6DAAyB,OAAO,CAAC;AAC7G,UAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,qCAAqC,OAAO,MAAM,EAAE;AAAA,IACtF;AAAA,IACA,MAAM,SAAwB;AAC5B,iBAAW;AACX,kCAAiB,IAAI,OAAO,YAAY,MAAe,UAAM,6CAA6B,OAAO,CAAC;AAAA,IACpG;AAAA,IACA,MAAM,QAAuB;AAC3B,aAAO,WAAW,KAAM,OAAM,QAAQ,IAAI,CAAC,GAAG,UAAU,CAAC;AACzD,YAAM,SAAS,SAAS,OAAO,CAAC,EAAE;AAAA,QAAO,aACvC,QAAQ,eAAe,cAAc,CAAC,mBAAmB,QAAQ,KAAK;AAAA,MACxE,EAAE,IAAI,aAAW,QAAQ,KAAK;AAC9B,UAAI,OAAO,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,mCAAmC,GAAG,EAAC,OAAM,CAAC;AAAA,IACjG;AAAA,IACA,qBAA6B;AAC3B,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AACpB,aAAO,EAAE;AAAA,IACX;AAAA,IACA,UAAgB;AACd,UAAI,OAAQ;AACZ,eAAS;AACT,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -31,13 +31,13 @@ import '../registration_only_root.cjs';
|
|
|
31
31
|
import '../test_wood.cjs';
|
|
32
32
|
|
|
33
33
|
/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */
|
|
34
|
-
declare function nativeHostTransport(inputs: NativeBrowserInputs, emit: (channel: string, payload: unknown) => Promise<void
|
|
34
|
+
declare function nativeHostTransport(inputs: Pick<NativeBrowserInputs, 'environment' | 'events'>, emit: (channel: string, payload: unknown, documentId: number) => Promise<void>, isDestroyedContext?: (error: unknown) => boolean): {
|
|
35
35
|
invoke(channel: string, payload: unknown): Promise<unknown>;
|
|
36
|
-
subscribe(channel: string, enabled: boolean): void;
|
|
36
|
+
subscribe(channel: string, enabled: boolean, owner?: number): void;
|
|
37
37
|
log(payload: unknown): void;
|
|
38
38
|
trace(payload: unknown): void;
|
|
39
39
|
flush(): Promise<void>;
|
|
40
|
-
resetSubscriptions():
|
|
40
|
+
resetSubscriptions(): number;
|
|
41
41
|
dispose(): void;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -31,13 +31,13 @@ import '../registration_only_root.js';
|
|
|
31
31
|
import '../test_wood.js';
|
|
32
32
|
|
|
33
33
|
/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */
|
|
34
|
-
declare function nativeHostTransport(inputs: NativeBrowserInputs, emit: (channel: string, payload: unknown) => Promise<void
|
|
34
|
+
declare function nativeHostTransport(inputs: Pick<NativeBrowserInputs, 'environment' | 'events'>, emit: (channel: string, payload: unknown, documentId: number) => Promise<void>, isDestroyedContext?: (error: unknown) => boolean): {
|
|
35
35
|
invoke(channel: string, payload: unknown): Promise<unknown>;
|
|
36
|
-
subscribe(channel: string, enabled: boolean): void;
|
|
36
|
+
subscribe(channel: string, enabled: boolean, owner?: number): void;
|
|
37
37
|
log(payload: unknown): void;
|
|
38
38
|
trace(payload: unknown): void;
|
|
39
39
|
flush(): Promise<void>;
|
|
40
|
-
resetSubscriptions():
|
|
40
|
+
resetSubscriptions(): number;
|
|
41
41
|
dispose(): void;
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { dispatchRendererLogBatch } from "../../runtime/renderer_log_batch_receiver.js";
|
|
2
2
|
import { ExecutionContext } from "@noego/ioc";
|
|
3
3
|
import { dispatchRendererTracePayload } from "../../tracing/index.js";
|
|
4
|
-
function nativeHostTransport(inputs, emit) {
|
|
4
|
+
function nativeHostTransport(inputs, emit, isDestroyedContext = () => false) {
|
|
5
5
|
const subscriptions = /* @__PURE__ */ new Map();
|
|
6
6
|
const deliveries = /* @__PURE__ */ new Set();
|
|
7
7
|
const failures = [];
|
|
8
|
+
let documentId = 0;
|
|
8
9
|
let closed = false;
|
|
9
10
|
const assertOpen = () => {
|
|
10
11
|
if (closed) throw new Error("Wood native tab is disposed");
|
|
@@ -15,8 +16,9 @@ function nativeHostTransport(inputs, emit) {
|
|
|
15
16
|
if (typeof channel !== "string") return Promise.reject(new TypeError("Wood operation channel must be a string"));
|
|
16
17
|
return inputs.environment.call(channel, payload);
|
|
17
18
|
},
|
|
18
|
-
subscribe(channel, enabled) {
|
|
19
|
+
subscribe(channel, enabled, owner = documentId) {
|
|
19
20
|
assertOpen();
|
|
21
|
+
if (owner !== documentId) return;
|
|
20
22
|
if (typeof channel !== "string" || typeof enabled !== "boolean") throw new TypeError("Invalid Wood subscription");
|
|
21
23
|
if (!enabled) {
|
|
22
24
|
subscriptions.get(channel)?.();
|
|
@@ -26,12 +28,16 @@ function nativeHostTransport(inputs, emit) {
|
|
|
26
28
|
if (subscriptions.has(channel)) return;
|
|
27
29
|
subscriptions.set(channel, inputs.events().on(channel, (payload) => {
|
|
28
30
|
if (closed) return;
|
|
29
|
-
const task = emit(channel, payload)
|
|
31
|
+
const task = Promise.resolve().then(() => emit(channel, payload, owner)).then(
|
|
32
|
+
() => {
|
|
33
|
+
deliveries.delete(task);
|
|
34
|
+
},
|
|
35
|
+
(error) => {
|
|
36
|
+
deliveries.delete(task);
|
|
37
|
+
failures.push({ documentId: owner, error });
|
|
38
|
+
}
|
|
39
|
+
);
|
|
30
40
|
deliveries.add(task);
|
|
31
|
-
task.then(() => deliveries.delete(task), (error) => {
|
|
32
|
-
deliveries.delete(task);
|
|
33
|
-
failures.push(error);
|
|
34
|
-
});
|
|
35
41
|
}));
|
|
36
42
|
},
|
|
37
43
|
log(payload) {
|
|
@@ -45,11 +51,15 @@ function nativeHostTransport(inputs, emit) {
|
|
|
45
51
|
},
|
|
46
52
|
async flush() {
|
|
47
53
|
while (deliveries.size) await Promise.all([...deliveries]);
|
|
48
|
-
|
|
54
|
+
const errors = failures.splice(0).filter(
|
|
55
|
+
(failure) => failure.documentId === documentId || !isDestroyedContext(failure.error)
|
|
56
|
+
).map((failure) => failure.error);
|
|
57
|
+
if (errors.length) throw Object.assign(new Error("Wood native event delivery failed"), { errors });
|
|
49
58
|
},
|
|
50
59
|
resetSubscriptions() {
|
|
51
60
|
for (const unsubscribe of subscriptions.values()) unsubscribe();
|
|
52
61
|
subscriptions.clear();
|
|
62
|
+
return ++documentId;
|
|
53
63
|
},
|
|
54
64
|
dispose() {
|
|
55
65
|
if (closed) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/host_transport.ts"],"sourcesContent":["import { dispatchRendererLogBatch } from '../../runtime/renderer_log_batch_receiver.js';\nimport { ExecutionContext } from '@noego/ioc';\nimport { dispatchRendererTracePayload } from '../../tracing/index.js';\nimport type { NativeBrowserInputs } from './native_browser.types.js';\n\n/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */\nexport function nativeHostTransport(inputs: NativeBrowserInputs, emit: (channel: string, payload: unknown) => Promise<void
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/host_transport.ts"],"sourcesContent":["import { dispatchRendererLogBatch } from '../../runtime/renderer_log_batch_receiver.js';\nimport { ExecutionContext } from '@noego/ioc';\nimport { dispatchRendererTracePayload } from '../../tracing/index.js';\nimport type { NativeBrowserInputs } from './native_browser.types.js';\n\n/** Per-tab subscriptions and in-flight host deliveries; never a process singleton. */\nexport function nativeHostTransport(\n inputs: Pick<NativeBrowserInputs, 'environment' | 'events'>,\n emit: (channel: string, payload: unknown, documentId: number) => Promise<void>,\n isDestroyedContext: (error: unknown) => boolean = () => false,\n) {\n const subscriptions = new Map<string, () => void>();\n const deliveries = new Set<Promise<void>>();\n const failures: {documentId: number; error: unknown}[] = [];\n let documentId = 0;\n let closed = false;\n const assertOpen = () => { if (closed) throw new Error('Wood native tab is disposed'); };\n return {\n invoke(channel: string, payload: unknown): Promise<unknown> {\n assertOpen();\n if (typeof channel !== 'string') return Promise.reject(new TypeError('Wood operation channel must be a string'));\n // Includes aperture checks, lazy startup, ExecutionContext and the real router.\n return inputs.environment.call(channel, payload);\n },\n subscribe(channel: string, enabled: boolean, owner = documentId): void {\n assertOpen();\n if (owner !== documentId) return;\n if (typeof channel !== 'string' || typeof enabled !== 'boolean') throw new TypeError('Invalid Wood subscription');\n if (!enabled) { subscriptions.get(channel)?.(); subscriptions.delete(channel); return; }\n if (subscriptions.has(channel)) return;\n subscriptions.set(channel, inputs.events().on(channel, (payload: unknown) => {\n if (closed) return;\n // Retain errors until flush can distinguish an obsolete document from\n // a real callback failure. Drain settlement promises, not raw rejects.\n const task = Promise.resolve().then(() => emit(channel, payload, owner)).then(\n () => { deliveries.delete(task); },\n error => { deliveries.delete(task); failures.push({documentId: owner, error}); },\n );\n deliveries.add(task);\n }));\n },\n log(payload: unknown): void {\n assertOpen();\n const result = ExecutionContext.run(inputs.environment.root as never, () => dispatchRendererLogBatch(payload));\n if (!result.ok) throw new Error(`Wood renderer log batch rejected: ${result.reason}`);\n },\n trace(payload: unknown): void {\n assertOpen();\n ExecutionContext.run(inputs.environment.root as never, () => dispatchRendererTracePayload(payload));\n },\n async flush(): Promise<void> {\n while (deliveries.size) await Promise.all([...deliveries]);\n const errors = failures.splice(0).filter(failure =>\n failure.documentId === documentId || !isDestroyedContext(failure.error),\n ).map(failure => failure.error);\n if (errors.length) throw Object.assign(new Error('Wood native event delivery failed'), {errors});\n },\n resetSubscriptions(): number {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n return ++documentId;\n },\n dispose(): void {\n if (closed) return;\n closed = true;\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\n },\n };\n}\n"],"mappings":"AAAA,SAAS,gCAAgC;AACzC,SAAS,wBAAwB;AACjC,SAAS,oCAAoC;AAItC,SAAS,oBACd,QACA,MACA,qBAAkD,MAAM,OACxD;AACA,QAAM,gBAAgB,oBAAI,IAAwB;AAClD,QAAM,aAAa,oBAAI,IAAmB;AAC1C,QAAM,WAAmD,CAAC;AAC1D,MAAI,aAAa;AACjB,MAAI,SAAS;AACb,QAAM,aAAa,MAAM;AAAE,QAAI,OAAQ,OAAM,IAAI,MAAM,6BAA6B;AAAA,EAAG;AACvF,SAAO;AAAA,IACL,OAAO,SAAiB,SAAoC;AAC1D,iBAAW;AACX,UAAI,OAAO,YAAY,SAAU,QAAO,QAAQ,OAAO,IAAI,UAAU,yCAAyC,CAAC;AAE/G,aAAO,OAAO,YAAY,KAAK,SAAS,OAAO;AAAA,IACjD;AAAA,IACA,UAAU,SAAiB,SAAkB,QAAQ,YAAkB;AACrE,iBAAW;AACX,UAAI,UAAU,WAAY;AAC1B,UAAI,OAAO,YAAY,YAAY,OAAO,YAAY,UAAW,OAAM,IAAI,UAAU,2BAA2B;AAChH,UAAI,CAAC,SAAS;AAAE,sBAAc,IAAI,OAAO,IAAI;AAAG,sBAAc,OAAO,OAAO;AAAG;AAAA,MAAQ;AACvF,UAAI,cAAc,IAAI,OAAO,EAAG;AAChC,oBAAc,IAAI,SAAS,OAAO,OAAO,EAAE,GAAG,SAAS,CAAC,YAAqB;AAC3E,YAAI,OAAQ;AAGZ,cAAM,OAAO,QAAQ,QAAQ,EAAE,KAAK,MAAM,KAAK,SAAS,SAAS,KAAK,CAAC,EAAE;AAAA,UACvE,MAAM;AAAE,uBAAW,OAAO,IAAI;AAAA,UAAG;AAAA,UACjC,WAAS;AAAE,uBAAW,OAAO,IAAI;AAAG,qBAAS,KAAK,EAAC,YAAY,OAAO,MAAK,CAAC;AAAA,UAAG;AAAA,QACjF;AACA,mBAAW,IAAI,IAAI;AAAA,MACrB,CAAC,CAAC;AAAA,IACJ;AAAA,IACA,IAAI,SAAwB;AAC1B,iBAAW;AACX,YAAM,SAAS,iBAAiB,IAAI,OAAO,YAAY,MAAe,MAAM,yBAAyB,OAAO,CAAC;AAC7G,UAAI,CAAC,OAAO,GAAI,OAAM,IAAI,MAAM,qCAAqC,OAAO,MAAM,EAAE;AAAA,IACtF;AAAA,IACA,MAAM,SAAwB;AAC5B,iBAAW;AACX,uBAAiB,IAAI,OAAO,YAAY,MAAe,MAAM,6BAA6B,OAAO,CAAC;AAAA,IACpG;AAAA,IACA,MAAM,QAAuB;AAC3B,aAAO,WAAW,KAAM,OAAM,QAAQ,IAAI,CAAC,GAAG,UAAU,CAAC;AACzD,YAAM,SAAS,SAAS,OAAO,CAAC,EAAE;AAAA,QAAO,aACvC,QAAQ,eAAe,cAAc,CAAC,mBAAmB,QAAQ,KAAK;AAAA,MACxE,EAAE,IAAI,aAAW,QAAQ,KAAK;AAC9B,UAAI,OAAO,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,mCAAmC,GAAG,EAAC,OAAM,CAAC;AAAA,IACjG;AAAA,IACA,qBAA6B;AAC3B,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AACpB,aAAO,EAAE;AAAA,IACX;AAAA,IACA,UAAgB;AACd,UAAI,OAAQ;AACZ,eAAS;AACT,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -35,12 +35,9 @@ async function openPlaywrightTab(inputs, context, bundle, route) {
|
|
|
35
35
|
const page = await context.newPage();
|
|
36
36
|
const errors = [];
|
|
37
37
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
38
|
-
const transport = (0, import_host_transport.nativeHostTransport)(inputs, (channel, payload) => page.evaluate(({ channel: channel2, payload: payload2 }) => {
|
|
39
|
-
window.__woodNativeEmit(channel2, payload2);
|
|
40
|
-
}, { channel, payload }));
|
|
41
|
-
page.on("framenavigated", (frame) => {
|
|
42
|
-
if (frame === page.mainFrame()) transport.resetSubscriptions();
|
|
43
|
-
});
|
|
38
|
+
const transport = (0, import_host_transport.nativeHostTransport)(inputs, (channel, payload, documentId) => page.evaluate(({ channel: channel2, payload: payload2, documentId: documentId2 }) => {
|
|
39
|
+
if (window.__woodNativeDocumentId === documentId2) window.__woodNativeEmit(channel2, payload2);
|
|
40
|
+
}, { channel, payload, documentId }), (error) => error instanceof Error && error.message === "page.evaluate: Execution context was destroyed, most likely because of a navigation");
|
|
44
41
|
page.on("close", () => transport.dispose());
|
|
45
42
|
let closing = null;
|
|
46
43
|
const dispose = () => closing ?? (closing = (0, import_native_browser_lifecycle.disposeNativeResources)("Wood Playwright tab cleanup failed", [
|
|
@@ -52,6 +49,7 @@ async function openPlaywrightTab(inputs, context, bundle, route) {
|
|
|
52
49
|
try {
|
|
53
50
|
await page.exposeFunction("__woodNativeInvoke", transport.invoke);
|
|
54
51
|
await page.exposeFunction("__woodNativeSubscribe", transport.subscribe);
|
|
52
|
+
await page.exposeFunction("__woodNativeResetSubscriptions", transport.resetSubscriptions);
|
|
55
53
|
await page.exposeFunction("__woodNativeTrace", transport.trace);
|
|
56
54
|
await page.exposeFunction("__woodNativeLog", transport.log);
|
|
57
55
|
await page.addInitScript((input) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/playwright_tab.ts"],"sourcesContent":["import { assetContentType } from '@noego/testing/browser';\nimport type { BrowserContext } from 'playwright';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\n\nfunction requestedAsset(bundle: NativeRendererBundle, pathname: string) {\n const normalized = pathname.replace(/^\\/+/, '');\n for (const [fileName, asset] of bundle.assets) {\n if (normalized === fileName || normalized.endsWith('/' + fileName)) return asset;\n }\n return null;\n}\n\nexport async function openPlaywrightTab(\n inputs: NativeBrowserInputs, context: BrowserContext, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const page = await context.newPage();\n const errors: string[] = [];\n page.on('pageerror', error => errors.push(error.message));\n const transport = nativeHostTransport(inputs, (channel, payload) => page.evaluate(({channel, payload}) => {\n (window as any).__woodNativeEmit(channel, payload);\n }, {channel, payload})
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/playwright_tab.ts"],"sourcesContent":["import { assetContentType } from '@noego/testing/browser';\nimport type { BrowserContext } from 'playwright';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\n\nfunction requestedAsset(bundle: NativeRendererBundle, pathname: string) {\n const normalized = pathname.replace(/^\\/+/, '');\n for (const [fileName, asset] of bundle.assets) {\n if (normalized === fileName || normalized.endsWith('/' + fileName)) return asset;\n }\n return null;\n}\n\nexport async function openPlaywrightTab(\n inputs: NativeBrowserInputs, context: BrowserContext, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const page = await context.newPage();\n const errors: string[] = [];\n page.on('pageerror', error => errors.push(error.message));\n const transport = nativeHostTransport(inputs, (channel, payload, documentId) => page.evaluate(({channel, payload, documentId}) => {\n // A delivery queued for the old realm must never replay into its replacement.\n if ((window as any).__woodNativeDocumentId === documentId) (window as any).__woodNativeEmit(channel, payload);\n }, {channel, payload, documentId}), error => error instanceof Error &&\n error.message === 'page.evaluate: Execution context was destroyed, most likely because of a navigation');\n page.on('close', () => transport.dispose());\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood Playwright tab cleanup failed', [\n () => page.isClosed() ? undefined : hostDeadline(page.evaluate(() => (window as any).__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => page.isClosed() ? undefined : hostDeadline(transport.flush(), 'event drain'),\n () => page.close(),\n ]);\n try {\n await page.exposeFunction('__woodNativeInvoke', transport.invoke);\n await page.exposeFunction('__woodNativeSubscribe', transport.subscribe);\n await page.exposeFunction('__woodNativeResetSubscriptions', transport.resetSubscriptions);\n await page.exposeFunction('__woodNativeTrace', transport.trace);\n await page.exposeFunction('__woodNativeLog', transport.log);\n await page.addInitScript(input => {\n (window as any).__woodNativeRoute = input;\n (globalThis as any).global = globalThis;\n }, route);\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n if (windowConfig) await page.setViewportSize({width: windowConfig.width, height: windowConfig.height});\n // A real origin permits native cookies/localStorage. Bundle assets are\n // inlined by Vite library mode; no per-test Vite server or browser launch.\n const url = `https://wood.test/${encodeURIComponent(route.page)}`;\n const scriptUrl = `${url}/__wood.js`;\n const cssUrl = `${url}/__wood.css`;\n await page.route('https://wood.test/**', async intercepted => {\n const requestUrl = intercepted.request().url();\n if (requestUrl === url) {\n await intercepted.fulfill({\n contentType: 'text/html',\n body: `<!doctype html><html><head><meta charset=\"utf-8\"><link rel=\"stylesheet\" href=\"${cssUrl}\"></head><body><div id=\"app\"></div><script src=\"${scriptUrl}\"></script></body></html>`,\n });\n return;\n }\n if (requestUrl === scriptUrl) {\n await intercepted.fulfill({contentType: 'text/javascript', body: bundle.code});\n return;\n }\n if (requestUrl === cssUrl) {\n await intercepted.fulfill({contentType: 'text/css', body: bundle.css});\n return;\n }\n const asset = requestedAsset(bundle, new URL(requestUrl).pathname);\n if (asset) {\n await intercepted.fulfill({contentType: assetContentType(asset.fileName), body: Buffer.from(asset.source)});\n return;\n }\n await intercepted.fulfill({status: 404, contentType: 'text/plain', body: 'Wood native asset not found'});\n });\n await page.goto(url);\n await hostDeadline(page.evaluate(() => (window as any).__woodNativeReady), 'initial mount');\n if (errors.length) throw new Error(errors.join('\\n'));\n return {\n native: page,\n async flush() {\n if (page.isClosed()) return;\n await hostDeadline(page.evaluate(() => (window as any).__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await page.evaluate(() => (window as any).__woodNativeFlush());\n },\n dispose,\n };\n } catch (error) {\n let html = '';\n if (!page.isClosed()) { try {html = (await page.content()).slice(0, 4000);} catch {} }\n const primary = Object.assign(new Error(`Wood could not open ${route.page}: ${error instanceof Error ? error.message : String(error)}\\n${errors.join('\\n')}\\n${html}`), {cause: error});\n await disposeNativeResources('Wood browser open failed', [() => {throw primary;}, dispose]);\n throw primary;\n }\n}\n"],"mappings":"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","names":["channel","payload","documentId"]}
|
|
@@ -12,12 +12,9 @@ async function openPlaywrightTab(inputs, context, bundle, route) {
|
|
|
12
12
|
const page = await context.newPage();
|
|
13
13
|
const errors = [];
|
|
14
14
|
page.on("pageerror", (error) => errors.push(error.message));
|
|
15
|
-
const transport = nativeHostTransport(inputs, (channel, payload) => page.evaluate(({ channel: channel2, payload: payload2 }) => {
|
|
16
|
-
window.__woodNativeEmit(channel2, payload2);
|
|
17
|
-
}, { channel, payload }));
|
|
18
|
-
page.on("framenavigated", (frame) => {
|
|
19
|
-
if (frame === page.mainFrame()) transport.resetSubscriptions();
|
|
20
|
-
});
|
|
15
|
+
const transport = nativeHostTransport(inputs, (channel, payload, documentId) => page.evaluate(({ channel: channel2, payload: payload2, documentId: documentId2 }) => {
|
|
16
|
+
if (window.__woodNativeDocumentId === documentId2) window.__woodNativeEmit(channel2, payload2);
|
|
17
|
+
}, { channel, payload, documentId }), (error) => error instanceof Error && error.message === "page.evaluate: Execution context was destroyed, most likely because of a navigation");
|
|
21
18
|
page.on("close", () => transport.dispose());
|
|
22
19
|
let closing = null;
|
|
23
20
|
const dispose = () => closing ?? (closing = disposeNativeResources("Wood Playwright tab cleanup failed", [
|
|
@@ -29,6 +26,7 @@ async function openPlaywrightTab(inputs, context, bundle, route) {
|
|
|
29
26
|
try {
|
|
30
27
|
await page.exposeFunction("__woodNativeInvoke", transport.invoke);
|
|
31
28
|
await page.exposeFunction("__woodNativeSubscribe", transport.subscribe);
|
|
29
|
+
await page.exposeFunction("__woodNativeResetSubscriptions", transport.resetSubscriptions);
|
|
32
30
|
await page.exposeFunction("__woodNativeTrace", transport.trace);
|
|
33
31
|
await page.exposeFunction("__woodNativeLog", transport.log);
|
|
34
32
|
await page.addInitScript((input) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/playwright_tab.ts"],"sourcesContent":["import { assetContentType } from '@noego/testing/browser';\nimport type { BrowserContext } from 'playwright';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\n\nfunction requestedAsset(bundle: NativeRendererBundle, pathname: string) {\n const normalized = pathname.replace(/^\\/+/, '');\n for (const [fileName, asset] of bundle.assets) {\n if (normalized === fileName || normalized.endsWith('/' + fileName)) return asset;\n }\n return null;\n}\n\nexport async function openPlaywrightTab(\n inputs: NativeBrowserInputs, context: BrowserContext, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const page = await context.newPage();\n const errors: string[] = [];\n page.on('pageerror', error => errors.push(error.message));\n const transport = nativeHostTransport(inputs, (channel, payload) => page.evaluate(({channel, payload}) => {\n (window as any).__woodNativeEmit(channel, payload);\n }, {channel, payload})
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/playwright_tab.ts"],"sourcesContent":["import { assetContentType } from '@noego/testing/browser';\nimport type { BrowserContext } from 'playwright';\nimport type { NativeBrowserInputs, NativeBrowserTab, NativeRendererBundle, NativeRendererRoute } from './native_browser.types.js';\nimport { nativeHostTransport } from './host_transport.js';\nimport { disposeNativeResources, hostDeadline } from './native_browser_lifecycle.js';\n\n\nfunction requestedAsset(bundle: NativeRendererBundle, pathname: string) {\n const normalized = pathname.replace(/^\\/+/, '');\n for (const [fileName, asset] of bundle.assets) {\n if (normalized === fileName || normalized.endsWith('/' + fileName)) return asset;\n }\n return null;\n}\n\nexport async function openPlaywrightTab(\n inputs: NativeBrowserInputs, context: BrowserContext, bundle: NativeRendererBundle, route: NativeRendererRoute,\n): Promise<NativeBrowserTab> {\n const page = await context.newPage();\n const errors: string[] = [];\n page.on('pageerror', error => errors.push(error.message));\n const transport = nativeHostTransport(inputs, (channel, payload, documentId) => page.evaluate(({channel, payload, documentId}) => {\n // A delivery queued for the old realm must never replay into its replacement.\n if ((window as any).__woodNativeDocumentId === documentId) (window as any).__woodNativeEmit(channel, payload);\n }, {channel, payload, documentId}), error => error instanceof Error &&\n error.message === 'page.evaluate: Execution context was destroyed, most likely because of a navigation');\n page.on('close', () => transport.dispose());\n let closing: Promise<void> | null = null;\n const dispose = () => closing ??= disposeNativeResources('Wood Playwright tab cleanup failed', [\n () => page.isClosed() ? undefined : hostDeadline(page.evaluate(() => (window as any).__woodNativeDispose?.()), 'renderer teardown'),\n () => transport.dispose(),\n () => page.isClosed() ? undefined : hostDeadline(transport.flush(), 'event drain'),\n () => page.close(),\n ]);\n try {\n await page.exposeFunction('__woodNativeInvoke', transport.invoke);\n await page.exposeFunction('__woodNativeSubscribe', transport.subscribe);\n await page.exposeFunction('__woodNativeResetSubscriptions', transport.resetSubscriptions);\n await page.exposeFunction('__woodNativeTrace', transport.trace);\n await page.exposeFunction('__woodNativeLog', transport.log);\n await page.addInitScript(input => {\n (window as any).__woodNativeRoute = input;\n (globalThis as any).global = globalThis;\n }, route);\n const windowConfig = inputs.source.config.windows[route.page.split('.')[0]!];\n if (windowConfig) await page.setViewportSize({width: windowConfig.width, height: windowConfig.height});\n // A real origin permits native cookies/localStorage. Bundle assets are\n // inlined by Vite library mode; no per-test Vite server or browser launch.\n const url = `https://wood.test/${encodeURIComponent(route.page)}`;\n const scriptUrl = `${url}/__wood.js`;\n const cssUrl = `${url}/__wood.css`;\n await page.route('https://wood.test/**', async intercepted => {\n const requestUrl = intercepted.request().url();\n if (requestUrl === url) {\n await intercepted.fulfill({\n contentType: 'text/html',\n body: `<!doctype html><html><head><meta charset=\"utf-8\"><link rel=\"stylesheet\" href=\"${cssUrl}\"></head><body><div id=\"app\"></div><script src=\"${scriptUrl}\"></script></body></html>`,\n });\n return;\n }\n if (requestUrl === scriptUrl) {\n await intercepted.fulfill({contentType: 'text/javascript', body: bundle.code});\n return;\n }\n if (requestUrl === cssUrl) {\n await intercepted.fulfill({contentType: 'text/css', body: bundle.css});\n return;\n }\n const asset = requestedAsset(bundle, new URL(requestUrl).pathname);\n if (asset) {\n await intercepted.fulfill({contentType: assetContentType(asset.fileName), body: Buffer.from(asset.source)});\n return;\n }\n await intercepted.fulfill({status: 404, contentType: 'text/plain', body: 'Wood native asset not found'});\n });\n await page.goto(url);\n await hostDeadline(page.evaluate(() => (window as any).__woodNativeReady), 'initial mount');\n if (errors.length) throw new Error(errors.join('\\n'));\n return {\n native: page,\n async flush() {\n if (page.isClosed()) return;\n await hostDeadline(page.evaluate(() => (window as any).__woodNativeFlush()), 'renderer flush');\n await hostDeadline(transport.flush(), 'event flush');\n await page.evaluate(() => (window as any).__woodNativeFlush());\n },\n dispose,\n };\n } catch (error) {\n let html = '';\n if (!page.isClosed()) { try {html = (await page.content()).slice(0, 4000);} catch {} }\n const primary = Object.assign(new Error(`Wood could not open ${route.page}: ${error instanceof Error ? error.message : String(error)}\\n${errors.join('\\n')}\\n${html}`), {cause: error});\n await disposeNativeResources('Wood browser open failed', [() => {throw primary;}, dispose]);\n throw primary;\n }\n}\n"],"mappings":"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","names":["channel","payload","documentId"]}
|
|
@@ -72,14 +72,14 @@ const transport = {
|
|
|
72
72
|
if (!listeners) {
|
|
73
73
|
listeners = new Set();
|
|
74
74
|
callbacks.set(channel, listeners);
|
|
75
|
-
track(subscriptionChanges, window.__woodNativeSubscribe(channel, true));
|
|
75
|
+
track(subscriptionChanges, window.__woodNativeSubscribe(channel, true, window.__woodNativeDocumentId));
|
|
76
76
|
}
|
|
77
77
|
listeners.add(callback);
|
|
78
78
|
return () => {
|
|
79
79
|
listeners.delete(callback);
|
|
80
80
|
if (listeners.size === 0) {
|
|
81
81
|
callbacks.delete(channel);
|
|
82
|
-
track(subscriptionChanges, window.__woodNativeSubscribe(channel, false));
|
|
82
|
+
track(subscriptionChanges, window.__woodNativeSubscribe(channel, false, window.__woodNativeDocumentId));
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
},
|
|
@@ -124,6 +124,9 @@ window.__woodNativeDispose = () => closing ??= (async () => {
|
|
|
124
124
|
}
|
|
125
125
|
})();
|
|
126
126
|
window.__woodNativeReady = (async () => {
|
|
127
|
+
// A new document owns a fresh callback map. Same-document navigation keeps
|
|
128
|
+
// the existing callbacks and must not reset their host subscriptions.
|
|
129
|
+
window.__woodNativeDocumentId = await window.__woodNativeResetSubscriptions();
|
|
127
130
|
const container = getContainer();
|
|
128
131
|
if (configureRenderer) await configureRenderer(container);
|
|
129
132
|
const config = await container.get(TraceProviderConfiguration);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/testing/native_browser/renderer_entry.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type { WoodConfigSource } from '../wood_config_source.js';\n\n/**\n * Identical renderer bootstrap for Chromium and happy-dom's isolated VM realm.\n * Production codegen owns controllers, layouts, data loaders and navigation.\n * Only the host transport differs. No components or Node globals are faked.\n */\nexport function nativeRendererEntry(source: WoodConfigSource): string {\n const operations = [...source.operations];\n const reflection = createRequire(path.join(source.configDir, 'package.json')).resolve('reflect-metadata');\n const rendererCompose = source.hookModule(source.config.renderer.compose);\n const composeImport = rendererCompose\n ? `import configureRenderer from ${JSON.stringify(rendererCompose)};`\n : 'const configureRenderer = null;';\n return `import ${JSON.stringify(reflection)};\nimport { flushSync, tick, unmount } from 'svelte';\nimport { buildTransportBridge } from '@noego/wood/transport';\nimport { getContainer, TraceProviderConfiguration, flushRendererLogs } from '@noego/wood/client';\nimport * as generated from './app.ts';\n${composeImport}\n\nconst route = window.__woodNativeRoute;\nconst callbacks = new Map();\nconst subscriptionChanges = new Set();\nconst requests = new Set();\nlet mounted;\nlet closing;\nfunction track(set, task) {\n const promise = Promise.resolve(task);\n set.add(promise);\n promise.then(() => set.delete(promise), () => set.delete(promise));\n return promise;\n}\nasync function subscriptionsReady() {\n while (subscriptionChanges.size) await Promise.all([...subscriptionChanges]);\n}\nconst transport = {\n invoke(channel, payload) {\n return track(requests, (async () => {\n await subscriptionsReady();\n return window.__woodNativeInvoke(channel, payload === undefined ? {} : payload);\n })());\n },\n subscribe(channel, callback) {\n let listeners = callbacks.get(channel);\n if (!listeners) {\n listeners = new Set();\n callbacks.set(channel, listeners);\n track(subscriptionChanges, window.__woodNativeSubscribe(channel, true));\n }\n listeners.add(callback);\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0) {\n callbacks.delete(channel);\n track(subscriptionChanges, window.__woodNativeSubscribe(channel, false));\n }\n };\n },\n};\nwindow.__woodNativeEmit = (channel, payload) => {\n for (const callback of callbacks.get(channel) ?? []) callback(payload);\n};\nconst bridge = buildTransportBridge(new Map(${JSON.stringify(operations)}), transport).bridge;\nbridge.__window = { current: async () => ({ok: true, windowId: route.page.split('.')[0], defaultRoute: route.page}) };\nbridge.__woodConfig = async () => ({trace: {enabled: true, minLevel: 'debug'}});\nbridge.__trace = payload => track(requests, window.__woodNativeTrace(payload));\n// These are preload infrastructure channels, never product operation replacements.\nbridge.__logBatch = batch => track(requests, window.__woodNativeLog(batch));\nwindow.__NOEGO_WOOD_BRIDGE__ = bridge;\n\nwindow.__woodNativeFlush = async () => {\n await tick();\n flushSync();\n await subscriptionsReady();\n flushRendererLogs();\n while (requests.size) await Promise.all([...requests]);\n await tick();\n flushSync();\n};\nwindow.__woodNativeDispose = () => closing ??= (async () => {\n const errors = [];\n const attempt = async fn => {try {await fn();} catch (error) {errors.push(error);}};\n await attempt(() => mounted?.disposeNavigation?.());\n await attempt(() => mounted && unmount(mounted));\n await attempt(() => window.__woodNativeFlush());\n await attempt(() => getContainer().dispose());\n callbacks.clear();\n if (errors.length) {\n const describe = (error, depth = 0) => {\n const message = error?.message ?? String(error);\n const nested = error?.errors ?? error?.failures?.map(failure => failure.error);\n return depth < 4 && Array.isArray(nested)\n ? message + ': ' + nested.map(value => describe(value, depth + 1)).join('; ')\n : message;\n };\n throw new AggregateError(errors, 'Native Wood renderer teardown failed: ' + errors.map(error => describe(error)).join('; '));\n }\n})();\nwindow.__woodNativeReady = (async () => {\n const container = getContainer();\n if (configureRenderer) await configureRenderer(container);\n const config = await container.get(TraceProviderConfiguration);\n config.configureRuntime('renderer');\n config.configureEnabled(true);\n config.configureMinLevel('debug');\n mounted = await generated.app(document.getElementById('app'), {params: route.params, query: route.query});\n await tick();\n flushSync();\n // Subscribe before open() resolves: do not invent event replay to hide a race.\n await subscriptionsReady();\n})();\n// A host always awaits readiness. Attach a handler now so an immediate mount\n// failure cannot become an unhandled rejection before the host reads it.\nwindow.__woodNativeReady.catch(() => {});\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA8B;AAC9B,uBAAiB;AAQV,SAAS,oBAAoB,QAAkC;AACpE,QAAM,aAAa,CAAC,GAAG,OAAO,UAAU;AACxC,QAAM,iBAAa,kCAAc,iBAAAA,QAAK,KAAK,OAAO,WAAW,cAAc,CAAC,EAAE,QAAQ,kBAAkB;AACxG,QAAM,kBAAkB,OAAO,WAAW,OAAO,OAAO,SAAS,OAAO;AACxE,QAAM,gBAAgB,kBAClB,iCAAiC,KAAK,UAAU,eAAe,CAAC,MAChE;AACJ,SAAO,UAAU,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8CA4C+B,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;
|
|
1
|
+
{"version":3,"sources":["../../../src/testing/native_browser/renderer_entry.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport path from 'node:path';\nimport type { WoodConfigSource } from '../wood_config_source.js';\n\n/**\n * Identical renderer bootstrap for Chromium and happy-dom's isolated VM realm.\n * Production codegen owns controllers, layouts, data loaders and navigation.\n * Only the host transport differs. No components or Node globals are faked.\n */\nexport function nativeRendererEntry(source: WoodConfigSource): string {\n const operations = [...source.operations];\n const reflection = createRequire(path.join(source.configDir, 'package.json')).resolve('reflect-metadata');\n const rendererCompose = source.hookModule(source.config.renderer.compose);\n const composeImport = rendererCompose\n ? `import configureRenderer from ${JSON.stringify(rendererCompose)};`\n : 'const configureRenderer = null;';\n return `import ${JSON.stringify(reflection)};\nimport { flushSync, tick, unmount } from 'svelte';\nimport { buildTransportBridge } from '@noego/wood/transport';\nimport { getContainer, TraceProviderConfiguration, flushRendererLogs } from '@noego/wood/client';\nimport * as generated from './app.ts';\n${composeImport}\n\nconst route = window.__woodNativeRoute;\nconst callbacks = new Map();\nconst subscriptionChanges = new Set();\nconst requests = new Set();\nlet mounted;\nlet closing;\nfunction track(set, task) {\n const promise = Promise.resolve(task);\n set.add(promise);\n promise.then(() => set.delete(promise), () => set.delete(promise));\n return promise;\n}\nasync function subscriptionsReady() {\n while (subscriptionChanges.size) await Promise.all([...subscriptionChanges]);\n}\nconst transport = {\n invoke(channel, payload) {\n return track(requests, (async () => {\n await subscriptionsReady();\n return window.__woodNativeInvoke(channel, payload === undefined ? {} : payload);\n })());\n },\n subscribe(channel, callback) {\n let listeners = callbacks.get(channel);\n if (!listeners) {\n listeners = new Set();\n callbacks.set(channel, listeners);\n track(subscriptionChanges, window.__woodNativeSubscribe(channel, true, window.__woodNativeDocumentId));\n }\n listeners.add(callback);\n return () => {\n listeners.delete(callback);\n if (listeners.size === 0) {\n callbacks.delete(channel);\n track(subscriptionChanges, window.__woodNativeSubscribe(channel, false, window.__woodNativeDocumentId));\n }\n };\n },\n};\nwindow.__woodNativeEmit = (channel, payload) => {\n for (const callback of callbacks.get(channel) ?? []) callback(payload);\n};\nconst bridge = buildTransportBridge(new Map(${JSON.stringify(operations)}), transport).bridge;\nbridge.__window = { current: async () => ({ok: true, windowId: route.page.split('.')[0], defaultRoute: route.page}) };\nbridge.__woodConfig = async () => ({trace: {enabled: true, minLevel: 'debug'}});\nbridge.__trace = payload => track(requests, window.__woodNativeTrace(payload));\n// These are preload infrastructure channels, never product operation replacements.\nbridge.__logBatch = batch => track(requests, window.__woodNativeLog(batch));\nwindow.__NOEGO_WOOD_BRIDGE__ = bridge;\n\nwindow.__woodNativeFlush = async () => {\n await tick();\n flushSync();\n await subscriptionsReady();\n flushRendererLogs();\n while (requests.size) await Promise.all([...requests]);\n await tick();\n flushSync();\n};\nwindow.__woodNativeDispose = () => closing ??= (async () => {\n const errors = [];\n const attempt = async fn => {try {await fn();} catch (error) {errors.push(error);}};\n await attempt(() => mounted?.disposeNavigation?.());\n await attempt(() => mounted && unmount(mounted));\n await attempt(() => window.__woodNativeFlush());\n await attempt(() => getContainer().dispose());\n callbacks.clear();\n if (errors.length) {\n const describe = (error, depth = 0) => {\n const message = error?.message ?? String(error);\n const nested = error?.errors ?? error?.failures?.map(failure => failure.error);\n return depth < 4 && Array.isArray(nested)\n ? message + ': ' + nested.map(value => describe(value, depth + 1)).join('; ')\n : message;\n };\n throw new AggregateError(errors, 'Native Wood renderer teardown failed: ' + errors.map(error => describe(error)).join('; '));\n }\n})();\nwindow.__woodNativeReady = (async () => {\n // A new document owns a fresh callback map. Same-document navigation keeps\n // the existing callbacks and must not reset their host subscriptions.\n window.__woodNativeDocumentId = await window.__woodNativeResetSubscriptions();\n const container = getContainer();\n if (configureRenderer) await configureRenderer(container);\n const config = await container.get(TraceProviderConfiguration);\n config.configureRuntime('renderer');\n config.configureEnabled(true);\n config.configureMinLevel('debug');\n mounted = await generated.app(document.getElementById('app'), {params: route.params, query: route.query});\n await tick();\n flushSync();\n // Subscribe before open() resolves: do not invent event replay to hide a race.\n await subscriptionsReady();\n})();\n// A host always awaits readiness. Attach a handler now so an immediate mount\n// failure cannot become an unhandled rejection before the host reads it.\nwindow.__woodNativeReady.catch(() => {});\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yBAA8B;AAC9B,uBAAiB;AAQV,SAAS,oBAAoB,QAAkC;AACpE,QAAM,aAAa,CAAC,GAAG,OAAO,UAAU;AACxC,QAAM,iBAAa,kCAAc,iBAAAA,QAAK,KAAK,OAAO,WAAW,cAAc,CAAC,EAAE,QAAQ,kBAAkB;AACxG,QAAM,kBAAkB,OAAO,WAAW,OAAO,OAAO,SAAS,OAAO;AACxE,QAAM,gBAAgB,kBAClB,iCAAiC,KAAK,UAAU,eAAe,CAAC,MAChE;AACJ,SAAO,UAAU,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA,EAK3C,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8CA4C+B,KAAK,UAAU,UAAU,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwDxE;","names":["path"]}
|