@noego/wood 0.7.2 → 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.
Files changed (37) hide show
  1. package/bin/wood.js +9 -1
  2. package/dist/codegen/renderer_app_generator.cjs +14 -13
  3. package/dist/codegen/renderer_app_generator.cjs.map +1 -1
  4. package/dist/codegen/renderer_app_generator.js +14 -13
  5. package/dist/codegen/renderer_app_generator.js.map +1 -1
  6. package/dist/navigation/index.cjs +14 -0
  7. package/dist/navigation/index.cjs.map +1 -1
  8. package/dist/navigation/index.d.cts +3 -1
  9. package/dist/navigation/index.d.ts +3 -1
  10. package/dist/navigation/index.js +13 -0
  11. package/dist/navigation/index.js.map +1 -1
  12. package/dist/testing/browser.cjs +11 -1
  13. package/dist/testing/browser.cjs.map +1 -1
  14. package/dist/testing/browser.d.cts +7 -1
  15. package/dist/testing/browser.d.ts +7 -1
  16. package/dist/testing/browser.js +10 -1
  17. package/dist/testing/browser.js.map +1 -1
  18. package/dist/testing/native_browser/happy_dom_tab.cjs +3 -2
  19. package/dist/testing/native_browser/happy_dom_tab.cjs.map +1 -1
  20. package/dist/testing/native_browser/happy_dom_tab.js +3 -2
  21. package/dist/testing/native_browser/happy_dom_tab.js.map +1 -1
  22. package/dist/testing/native_browser/host_transport.cjs +18 -8
  23. package/dist/testing/native_browser/host_transport.cjs.map +1 -1
  24. package/dist/testing/native_browser/host_transport.d.cts +3 -3
  25. package/dist/testing/native_browser/host_transport.d.ts +3 -3
  26. package/dist/testing/native_browser/host_transport.js +18 -8
  27. package/dist/testing/native_browser/host_transport.js.map +1 -1
  28. package/dist/testing/native_browser/playwright_tab.cjs +4 -6
  29. package/dist/testing/native_browser/playwright_tab.cjs.map +1 -1
  30. package/dist/testing/native_browser/playwright_tab.js +4 -6
  31. package/dist/testing/native_browser/playwright_tab.js.map +1 -1
  32. package/dist/testing/native_browser/renderer_entry.cjs +5 -2
  33. package/dist/testing/native_browser/renderer_entry.cjs.map +1 -1
  34. package/dist/testing/native_browser/renderer_entry.js +5 -2
  35. package/dist/testing/native_browser/renderer_entry.js.map +1 -1
  36. package/docs/native-browser-testing.md +11 -0
  37. package/package.json +2 -2
@@ -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,YAAY;AACxE,YAAQ,iBAAiB,SAAS,WAAW,OAAO,CAAC;AAAA,EACvD,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,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,0BAAY,wDAAuB,wCAAwC;AAAA,IAC/F,UAAM,8CAAa,QAAQ,QAAQ,QAAQ,sBAAsB,CAAC,GAAG,mBAAmB;AAAA,IACxF,MAAM,UAAU,QAAQ;AAAA,IACxB,UAAM,8CAAa,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,cAAM,8CAAa,QAAQ,QAAQ,QAAQ,iBAAiB,GAAG,eAAe;AAC9E,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,QAAQ;AACZ,kBAAM,8CAAa,YAAY,OAAO,GAAG,sBAAsB;AAC/D,kBAAM,8CAAa,QAAQ,QAAQ,QAAQ,kBAAkB,CAAC,GAAG,gBAAgB;AACjF,kBAAM,8CAAa,UAAU,MAAM,GAAG,aAAa;AACnD,cAAM,QAAQ,kBAAkB;AAAA,MAClC;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,cAAM,wDAAuB,8BAA8B,CAAC,MAAM;AAAC,YAAM;AAAA,IAAM,GAAG,OAAO,CAAC;AAC1F,UAAM;AAAA,EACR;AACF;","names":[]}
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,YAAY;AACxE,YAAQ,iBAAiB,SAAS,WAAW,OAAO,CAAC;AAAA,EACvD,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,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":[]}
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
- if (failures.length) throw Object.assign(new Error("Wood native event delivery failed"), { errors: failures.splice(0) });
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>) {\n const subscriptions = new Map<string, () => void>();\n const deliveries = new Set<Promise<void>>();\n const failures: unknown[] = [];\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): void {\n assertOpen();\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 const task = emit(channel, payload);\n deliveries.add(task);\n task.then(() => deliveries.delete(task), error => {deliveries.delete(task); failures.push(error);});\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 if (failures.length) throw Object.assign(new Error('Wood native event delivery failed'), {errors: failures.splice(0)});\n },\n resetSubscriptions(): void {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\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,oBAAoB,QAA6B,MAA4D;AAC3H,QAAM,gBAAgB,oBAAI,IAAwB;AAClD,QAAM,aAAa,oBAAI,IAAmB;AAC1C,QAAM,WAAsB,CAAC;AAC7B,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,SAAwB;AACjD,iBAAW;AACX,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;AACZ,cAAM,OAAO,KAAK,SAAS,OAAO;AAClC,mBAAW,IAAI,IAAI;AACnB,aAAK,KAAK,MAAM,WAAW,OAAO,IAAI,GAAG,WAAS;AAAC,qBAAW,OAAO,IAAI;AAAG,mBAAS,KAAK,KAAK;AAAA,QAAE,CAAC;AAAA,MACpG,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,UAAI,SAAS,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,mCAAmC,GAAG,EAAC,QAAQ,SAAS,OAAO,CAAC,EAAC,CAAC;AAAA,IACvH;AAAA,IACA,qBAA2B;AACzB,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,IACA,UAAgB;AACd,UAAI,OAAQ;AACZ,eAAS;AACT,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,EACF;AACF;","names":[]}
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(): void;
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(): void;
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
- if (failures.length) throw Object.assign(new Error("Wood native event delivery failed"), { errors: failures.splice(0) });
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>) {\n const subscriptions = new Map<string, () => void>();\n const deliveries = new Set<Promise<void>>();\n const failures: unknown[] = [];\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): void {\n assertOpen();\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 const task = emit(channel, payload);\n deliveries.add(task);\n task.then(() => deliveries.delete(task), error => {deliveries.delete(task); failures.push(error);});\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 if (failures.length) throw Object.assign(new Error('Wood native event delivery failed'), {errors: failures.splice(0)});\n },\n resetSubscriptions(): void {\n for (const unsubscribe of subscriptions.values()) unsubscribe();\n subscriptions.clear();\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,oBAAoB,QAA6B,MAA4D;AAC3H,QAAM,gBAAgB,oBAAI,IAAwB;AAClD,QAAM,aAAa,oBAAI,IAAmB;AAC1C,QAAM,WAAsB,CAAC;AAC7B,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,SAAwB;AACjD,iBAAW;AACX,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;AACZ,cAAM,OAAO,KAAK,SAAS,OAAO;AAClC,mBAAW,IAAI,IAAI;AACnB,aAAK,KAAK,MAAM,WAAW,OAAO,IAAI,GAAG,WAAS;AAAC,qBAAW,OAAO,IAAI;AAAG,mBAAS,KAAK,KAAK;AAAA,QAAE,CAAC;AAAA,MACpG,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,UAAI,SAAS,OAAQ,OAAM,OAAO,OAAO,IAAI,MAAM,mCAAmC,GAAG,EAAC,QAAQ,SAAS,OAAO,CAAC,EAAC,CAAC;AAAA,IACvH;AAAA,IACA,qBAA2B;AACzB,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,IACA,UAAgB;AACd,UAAI,OAAQ;AACZ,eAAS;AACT,iBAAW,eAAe,cAAc,OAAO,EAAG,aAAY;AAC9D,oBAAc,MAAM;AAAA,IACtB;AAAA,EACF;AACF;","names":[]}
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}));\n page.on('framenavigated', frame => {\n if (frame === page.mainFrame()) transport.resetSubscriptions();\n });\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('__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":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiC;AAGjC,4BAAoC;AACpC,sCAAqD;AAGrD,SAAS,eAAe,QAA8B,UAAkB;AACtE,QAAM,aAAa,SAAS,QAAQ,QAAQ,EAAE;AAC9C,aAAW,CAAC,UAAU,KAAK,KAAK,OAAO,QAAQ;AAC7C,QAAI,eAAe,YAAY,WAAW,SAAS,MAAM,QAAQ,EAAG,QAAO;AAAA,EAC7E;AACA,SAAO;AACT;AAEA,eAAsB,kBACpB,QAA6B,SAAyB,QAA8B,OACzD;AAC3B,QAAM,OAAO,MAAM,QAAQ,QAAQ;AACnC,QAAM,SAAmB,CAAC;AAC1B,OAAK,GAAG,aAAa,WAAS,OAAO,KAAK,MAAM,OAAO,CAAC;AACxD,QAAM,gBAAY,2CAAoB,QAAQ,CAAC,SAAS,YAAY,KAAK,SAAS,CAAC,EAAC,SAAAA,UAAS,SAAAC,SAAO,MAAM;AACxG,IAAC,OAAe,iBAAiBD,UAASC,QAAO;AAAA,EACnD,GAAG,EAAC,SAAS,QAAO,CAAC,CAAC;AACtB,OAAK,GAAG,kBAAkB,WAAS;AACjC,QAAI,UAAU,KAAK,UAAU,EAAG,WAAU,mBAAmB;AAAA,EAC/D,CAAC;AACD,OAAK,GAAG,SAAS,MAAM,UAAU,QAAQ,CAAC;AAC1C,MAAI,UAAgC;AACpC,QAAM,UAAU,MAAM,0BAAY,wDAAuB,sCAAsC;AAAA,IAC7F,MAAM,KAAK,SAAS,IAAI,aAAY,8CAAa,KAAK,SAAS,MAAO,OAAe,sBAAsB,CAAC,GAAG,mBAAmB;AAAA,IAClI,MAAM,UAAU,QAAQ;AAAA,IACxB,MAAM,KAAK,SAAS,IAAI,aAAY,8CAAa,UAAU,MAAM,GAAG,aAAa;AAAA,IACjF,MAAM,KAAK,MAAM;AAAA,EACnB,CAAC;AACD,MAAI;AACF,UAAM,KAAK,eAAe,sBAAsB,UAAU,MAAM;AAChE,UAAM,KAAK,eAAe,yBAAyB,UAAU,SAAS;AACtE,UAAM,KAAK,eAAe,qBAAqB,UAAU,KAAK;AAC9D,UAAM,KAAK,eAAe,mBAAmB,UAAU,GAAG;AAC1D,UAAM,KAAK,cAAc,WAAS;AAChC,MAAC,OAAe,oBAAoB;AACpC,MAAC,WAAmB,SAAS;AAAA,IAC/B,GAAG,KAAK;AACR,UAAM,eAAe,OAAO,OAAO,OAAO,QAAQ,MAAM,KAAK,MAAM,GAAG,EAAE,CAAC,CAAE;AAC3E,QAAI,aAAc,OAAM,KAAK,gBAAgB,EAAC,OAAO,aAAa,OAAO,QAAQ,aAAa,OAAM,CAAC;AAGrG,UAAM,MAAM,qBAAqB,mBAAmB,MAAM,IAAI,CAAC;AAC/D,UAAM,YAAY,GAAG,GAAG;AACxB,UAAM,SAAS,GAAG,GAAG;AACrB,UAAM,KAAK,MAAM,wBAAwB,OAAM,gBAAe;AAC5D,YAAM,aAAa,YAAY,QAAQ,EAAE,IAAI;AAC7C,UAAI,eAAe,KAAK;AACtB,cAAM,YAAY,QAAQ;AAAA,UACxB,aAAa;AAAA,UACb,MAAM,iFAAiF,MAAM,mDAAmD,SAAS;AAAA,QAC3J,CAAC;AACD;AAAA,MACF;AACA,UAAI,eAAe,WAAW;AAC5B,cAAM,YAAY,QAAQ,EAAC,aAAa,mBAAmB,MAAM,OAAO,KAAI,CAAC;AAC7E;AAAA,MACF;AACA,UAAI,eAAe,QAAQ;AACzB,cAAM,YAAY,QAAQ,EAAC,aAAa,YAAY,MAAM,OAAO,IAAG,CAAC;AACrE;AAAA,MACF;AACA,YAAM,QAAQ,eAAe,QAAQ,IAAI,IAAI,UAAU,EAAE,QAAQ;AACjE,UAAI,OAAO;AACT,cAAM,YAAY,QAAQ,EAAC,iBAAa,iCAAiB,MAAM,QAAQ,GAAG,MAAM,OAAO,KAAK,MAAM,MAAM,EAAC,CAAC;AAC1G;AAAA,MACF;AACA,YAAM,YAAY,QAAQ,EAAC,QAAQ,KAAK,aAAa,cAAc,MAAM,8BAA6B,CAAC;AAAA,IACzG,CAAC;AACD,UAAM,KAAK,KAAK,GAAG;AACnB,cAAM,8CAAa,KAAK,SAAS,MAAO,OAAe,iBAAiB,GAAG,eAAe;AAC1F,QAAI,OAAO,OAAQ,OAAM,IAAI,MAAM,OAAO,KAAK,IAAI,CAAC;AACpD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM,QAAQ;AACZ,YAAI,KAAK,SAAS,EAAG;AACrB,kBAAM,8CAAa,KAAK,SAAS,MAAO,OAAe,kBAAkB,CAAC,GAAG,gBAAgB;AAC7F,kBAAM,8CAAa,UAAU,MAAM,GAAG,aAAa;AACnD,cAAM,KAAK,SAAS,MAAO,OAAe,kBAAkB,CAAC;AAAA,MAC/D;AAAA,MACA;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,QAAI,OAAO;AACX,QAAI,CAAC,KAAK,SAAS,GAAG;AAAE,UAAI;AAAC,gBAAQ,MAAM,KAAK,QAAQ,GAAG,MAAM,GAAG,GAAI;AAAA,MAAE,QAAQ;AAAA,MAAC;AAAA,IAAE;AACrF,UAAM,UAAU,OAAO,OAAO,IAAI,MAAM,uBAAuB,MAAM,IAAI,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AAAA,EAAK,OAAO,KAAK,IAAI,CAAC;AAAA,EAAK,IAAI,EAAE,GAAG,EAAC,OAAO,MAAK,CAAC;AACtL,cAAM,wDAAuB,4BAA4B,CAAC,MAAM;AAAC,YAAM;AAAA,IAAQ,GAAG,OAAO,CAAC;AAC1F,UAAM;AAAA,EACR;AACF;","names":["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}));\n page.on('framenavigated', frame => {\n if (frame === page.mainFrame()) transport.resetSubscriptions();\n });\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('__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"]}
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;AAqDxE;","names":["path"]}
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"]}
@@ -39,14 +39,14 @@ const transport = {
39
39
  if (!listeners) {
40
40
  listeners = new Set();
41
41
  callbacks.set(channel, listeners);
42
- track(subscriptionChanges, window.__woodNativeSubscribe(channel, true));
42
+ track(subscriptionChanges, window.__woodNativeSubscribe(channel, true, window.__woodNativeDocumentId));
43
43
  }
44
44
  listeners.add(callback);
45
45
  return () => {
46
46
  listeners.delete(callback);
47
47
  if (listeners.size === 0) {
48
48
  callbacks.delete(channel);
49
- track(subscriptionChanges, window.__woodNativeSubscribe(channel, false));
49
+ track(subscriptionChanges, window.__woodNativeSubscribe(channel, false, window.__woodNativeDocumentId));
50
50
  }
51
51
  };
52
52
  },
@@ -91,6 +91,9 @@ window.__woodNativeDispose = () => closing ??= (async () => {
91
91
  }
92
92
  })();
93
93
  window.__woodNativeReady = (async () => {
94
+ // A new document owns a fresh callback map. Same-document navigation keeps
95
+ // the existing callbacks and must not reset their host subscriptions.
96
+ window.__woodNativeDocumentId = await window.__woodNativeResetSubscriptions();
94
97
  const container = getContainer();
95
98
  if (configureRenderer) await configureRenderer(container);
96
99
  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,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AAQV,SAAS,oBAAoB,QAAkC;AACpE,QAAM,aAAa,CAAC,GAAG,OAAO,UAAU;AACxC,QAAM,aAAa,cAAc,KAAK,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;AAqDxE;","names":[]}
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,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AAQV,SAAS,oBAAoB,QAAkC;AACpE,QAAM,aAAa,CAAC,GAAG,OAAO,UAAU;AACxC,QAAM,aAAa,cAAc,KAAK,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":[]}
@@ -132,6 +132,17 @@ its durable commit barrier. There is no pretend durable batch acknowledgement.
132
132
  Building records application composition without opening a tab or Window.
133
133
  First `open()` compiles the selected renderer and acquires the host. Native
134
134
  Playwright reload remounts the renderer without duplicating subscriptions.
135
+ Same-document history navigation preserves live subscriptions; a new renderer
136
+ document resets the previous host subscriptions before mounting. Each delivery
137
+ is bound to that document; a queued old payload cannot replay into the new one.
138
+ Only Playwright's exact context-destroyed navigation error from a proven retired
139
+ document is discarded. Current-document and renderer callback failures remain
140
+ visible during settlement.
141
+
142
+ Production renderer bootstrap restores the current marked browser-history page,
143
+ params and query after a reload. It validates string-valued route records and
144
+ requires the page to exist in the current window's generated manifest. Invalid,
145
+ unmarked or foreign-window history falls back to the host's bootstrap route.
135
146
  Closing an environment fences new opens, joins admitted opens, attempts all
136
147
  cleanup steps, and reports cleanup failures without abandoning other resources.
137
148
  Page mount errors include the route and browser diagnostics.