@noego/wood 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wood.js +9 -1
- package/dist/client/app_env.cjs +39 -0
- package/dist/client/app_env.cjs.map +1 -0
- package/dist/client/app_env.d.cts +7 -0
- package/dist/client/app_env.d.ts +7 -0
- package/dist/client/app_env.js +14 -0
- package/dist/client/app_env.js.map +1 -0
- package/dist/client/index.cjs +33 -9
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +5 -1
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +32 -9
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/channel_map_generator.cjs +102 -0
- package/dist/codegen/channel_map_generator.cjs.map +1 -0
- package/dist/codegen/channel_map_generator.d.cts +6 -0
- package/dist/codegen/channel_map_generator.d.ts +6 -0
- package/dist/codegen/channel_map_generator.js +78 -0
- package/dist/codegen/channel_map_generator.js.map +1 -0
- package/dist/codegen/controller_registration_generator.cjs +6 -1
- package/dist/codegen/controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/controller_registration_generator.js +6 -1
- package/dist/codegen/controller_registration_generator.js.map +1 -1
- package/dist/codegen/generated_file_notice.cjs +40 -0
- package/dist/codegen/generated_file_notice.cjs.map +1 -0
- package/dist/codegen/generated_file_notice.d.cts +3 -0
- package/dist/codegen/generated_file_notice.d.ts +3 -0
- package/dist/codegen/generated_file_notice.js +16 -0
- package/dist/codegen/generated_file_notice.js.map +1 -0
- package/dist/codegen/index.cjs +3 -0
- package/dist/codegen/index.cjs.map +1 -1
- package/dist/codegen/index.d.cts +1 -0
- package/dist/codegen/index.d.ts +1 -0
- package/dist/codegen/index.js +2 -0
- package/dist/codegen/index.js.map +1 -1
- package/dist/codegen/preload_generator.cjs +97 -1
- package/dist/codegen/preload_generator.cjs.map +1 -1
- package/dist/codegen/preload_generator.js +97 -1
- package/dist/codegen/preload_generator.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +6 -1
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +6 -1
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs +6 -1
- package/dist/codegen/renderer_controller_registration_generator.cjs.map +1 -1
- package/dist/codegen/renderer_controller_registration_generator.js +6 -1
- package/dist/codegen/renderer_controller_registration_generator.js.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs +6 -1
- package/dist/codegen/renderer_view_manifest_generator.cjs.map +1 -1
- package/dist/codegen/renderer_view_manifest_generator.js +6 -1
- package/dist/codegen/renderer_view_manifest_generator.js.map +1 -1
- package/dist/codegen/runtime_generator.cjs +6 -1
- package/dist/codegen/runtime_generator.cjs.map +1 -1
- package/dist/codegen/runtime_generator.js +6 -1
- package/dist/codegen/runtime_generator.js.map +1 -1
- package/dist/codegen/test_mock_generator.cjs +5 -1
- package/dist/codegen/test_mock_generator.cjs.map +1 -1
- package/dist/codegen/test_mock_generator.js +5 -1
- package/dist/codegen/test_mock_generator.js.map +1 -1
- package/dist/codegen/type_generator.cjs +7 -1
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +7 -1
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/index.cjs +19 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main/index.cjs +287 -62
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +57 -3
- package/dist/main/index.d.ts +57 -3
- package/dist/main/index.js +289 -62
- package/dist/main/index.js.map +1 -1
- package/dist/main/renderer_trace_drain_coordinator.cjs +127 -0
- package/dist/main/renderer_trace_drain_coordinator.cjs.map +1 -0
- package/dist/main/renderer_trace_drain_coordinator.d.cts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.d.ts +46 -0
- package/dist/main/renderer_trace_drain_coordinator.js +101 -0
- package/dist/main/renderer_trace_drain_coordinator.js.map +1 -0
- package/dist/router/wood_router.cjs +9 -1
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +1 -1
- package/dist/router/wood_router.d.ts +1 -1
- package/dist/router/wood_router.js +9 -1
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/app_env.cjs +52 -0
- package/dist/runtime/app_env.cjs.map +1 -0
- package/dist/runtime/app_env.d.cts +10 -0
- package/dist/runtime/app_env.d.ts +10 -0
- package/dist/runtime/app_env.js +18 -0
- package/dist/runtime/app_env.js.map +1 -0
- package/dist/runtime/index.cjs +9 -2
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +5 -1
- package/dist/runtime/index.d.ts +5 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.cts +1 -1
- package/dist/runtime/renderer_log_dispatcher.d.ts +1 -1
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs +50 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.cjs.map +1 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.cts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.d.ts +10 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js +24 -0
- package/dist/runtime/renderer_trace_batch_receiver_registry.js.map +1 -0
- package/dist/runtime/runtime.cjs +75 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +8 -1
- package/dist/runtime/runtime.d.ts +8 -1
- package/dist/runtime/runtime.js +80 -58
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/testing/browser.cjs +540 -11
- package/dist/testing/browser.cjs.map +1 -1
- package/dist/testing/browser.d.cts +219 -3
- package/dist/testing/browser.d.ts +219 -3
- package/dist/testing/browser.js +540 -11
- package/dist/testing/browser.js.map +1 -1
- package/dist/testing/create_controller_harness.d.cts +1 -1
- package/dist/testing/create_controller_harness.d.ts +1 -1
- package/dist/testing/index.cjs +13 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +7 -2
- package/dist/testing/index.d.ts +7 -2
- package/dist/testing/index.js +14 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/trace-testing/index.d.cts +1 -1
- package/dist/trace-testing/index.d.ts +1 -1
- package/dist/trace-testing/trace_recorder.d.cts +1 -1
- package/dist/trace-testing/trace_recorder.d.ts +1 -1
- package/dist/trace-transport/index.cjs +32 -0
- package/dist/trace-transport/index.cjs.map +1 -0
- package/dist/trace-transport/index.d.cts +6 -0
- package/dist/trace-transport/index.d.ts +6 -0
- package/dist/trace-transport/index.js +7 -0
- package/dist/trace-transport/index.js.map +1 -0
- package/dist/trace_hub-Cob54qRx.d.cts +56 -0
- package/dist/trace_hub-Cob54qRx.d.ts +56 -0
- package/dist/tracing/index.cjs +14 -0
- package/dist/tracing/index.cjs.map +1 -1
- package/dist/tracing/index.d.cts +7 -1
- package/dist/tracing/index.d.ts +7 -1
- package/dist/tracing/index.js +12 -0
- package/dist/tracing/index.js.map +1 -1
- package/dist/tracing/renderer_trace_batch_host.cjs +80 -0
- package/dist/tracing/renderer_trace_batch_host.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_host.d.cts +16 -0
- package/dist/tracing/renderer_trace_batch_host.d.ts +16 -0
- package/dist/tracing/renderer_trace_batch_host.js +56 -0
- package/dist/tracing/renderer_trace_batch_host.js.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs +313 -0
- package/dist/tracing/renderer_trace_batch_transport.cjs.map +1 -0
- package/dist/tracing/renderer_trace_batch_transport.d.cts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.d.ts +53 -0
- package/dist/tracing/renderer_trace_batch_transport.js +292 -0
- package/dist/tracing/renderer_trace_batch_transport.js.map +1 -0
- package/dist/tracing/trace_commit_writer.cjs +159 -0
- package/dist/tracing/trace_commit_writer.cjs.map +1 -0
- package/dist/tracing/trace_commit_writer.d.cts +97 -0
- package/dist/tracing/trace_commit_writer.d.ts +97 -0
- package/dist/tracing/trace_commit_writer.js +135 -0
- package/dist/tracing/trace_commit_writer.js.map +1 -0
- package/dist/tracing/trace_hub.cjs +58 -3
- package/dist/tracing/trace_hub.cjs.map +1 -1
- package/dist/tracing/trace_hub.d.cts +2 -46
- package/dist/tracing/trace_hub.d.ts +2 -46
- package/dist/tracing/trace_hub.js +56 -3
- package/dist/tracing/trace_hub.js.map +1 -1
- package/dist/tracing/trace_provider.cjs +42 -12
- package/dist/tracing/trace_provider.cjs.map +1 -1
- package/dist/tracing/trace_provider.d.cts +15 -1
- package/dist/tracing/trace_provider.d.ts +15 -1
- package/dist/tracing/trace_provider.js +43 -12
- package/dist/tracing/trace_provider.js.map +1 -1
- package/dist/tracing/types/wood_trace_batch.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_batch.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_batch.types.d.cts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.d.ts +124 -0
- package/dist/tracing/types/wood_trace_batch.types.js +1 -0
- package/dist/tracing/types/wood_trace_batch.types.js.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs +17 -0
- package/dist/tracing/types/wood_trace_sink.types.cjs.map +1 -0
- package/dist/tracing/types/wood_trace_sink.types.d.cts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.d.ts +2 -0
- package/dist/tracing/types/wood_trace_sink.types.js +1 -0
- package/dist/tracing/types/wood_trace_sink.types.js.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.cjs +229 -0
- package/dist/tracing/wood_trace_batch_contract.cjs.map +1 -0
- package/dist/tracing/wood_trace_batch_contract.d.cts +23 -0
- package/dist/tracing/wood_trace_batch_contract.d.ts +23 -0
- package/dist/tracing/wood_trace_batch_contract.js +205 -0
- package/dist/tracing/wood_trace_batch_contract.js.map +1 -0
- package/dist/transport/build_transport_bridge.cjs +62 -0
- package/dist/transport/build_transport_bridge.cjs.map +1 -0
- package/dist/transport/build_transport_bridge.d.cts +29 -0
- package/dist/transport/build_transport_bridge.d.ts +29 -0
- package/dist/transport/build_transport_bridge.js +38 -0
- package/dist/transport/build_transport_bridge.js.map +1 -0
- package/dist/transport/in_process_router_transport.cjs +41 -0
- package/dist/transport/in_process_router_transport.cjs.map +1 -0
- package/dist/transport/in_process_router_transport.d.cts +31 -0
- package/dist/transport/in_process_router_transport.d.ts +31 -0
- package/dist/transport/in_process_router_transport.js +17 -0
- package/dist/transport/in_process_router_transport.js.map +1 -0
- package/dist/transport/index.cjs +42 -0
- package/dist/transport/index.cjs.map +1 -0
- package/dist/transport/index.d.cts +8 -0
- package/dist/transport/index.d.ts +8 -0
- package/dist/transport/index.js +19 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/transport/loopback_remote_transport.cjs +270 -0
- package/dist/transport/loopback_remote_transport.cjs.map +1 -0
- package/dist/transport/loopback_remote_transport.d.cts +49 -0
- package/dist/transport/loopback_remote_transport.d.ts +49 -0
- package/dist/transport/loopback_remote_transport.js +237 -0
- package/dist/transport/loopback_remote_transport.js.map +1 -0
- package/dist/transport/loopback_serialization.cjs +116 -0
- package/dist/transport/loopback_serialization.cjs.map +1 -0
- package/dist/transport/loopback_serialization.d.cts +28 -0
- package/dist/transport/loopback_serialization.d.ts +28 -0
- package/dist/transport/loopback_serialization.js +91 -0
- package/dist/transport/loopback_serialization.js.map +1 -0
- package/dist/transport/transport_contract_suite.cjs +110 -0
- package/dist/transport/transport_contract_suite.cjs.map +1 -0
- package/dist/transport/transport_contract_suite.d.cts +23 -0
- package/dist/transport/transport_contract_suite.d.ts +23 -0
- package/dist/transport/transport_contract_suite.js +85 -0
- package/dist/transport/transport_contract_suite.js.map +1 -0
- package/dist/transport/wood_rpc_transport.cjs +17 -0
- package/dist/transport/wood_rpc_transport.cjs.map +1 -0
- package/dist/transport/wood_rpc_transport.d.cts +44 -0
- package/dist/transport/wood_rpc_transport.d.ts +44 -0
- package/dist/transport/wood_rpc_transport.js +1 -0
- package/dist/transport/wood_rpc_transport.js.map +1 -0
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +12 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/window/window_manager.cjs +3 -0
- package/dist/window/window_manager.cjs.map +1 -1
- package/dist/window/window_manager.js +3 -0
- package/dist/window/window_manager.js.map +1 -1
- package/docs/browser-testing.md +139 -1
- package/package.json +22 -3
- package/src/components/NavigationShell.browser.test.ts +236 -0
- package/src/components/NavigationShell.svelte +162 -49
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
5
|
+
import { browser } from '../testing/browser';
|
|
6
|
+
|
|
7
|
+
type ReproSnapshot = {
|
|
8
|
+
pageCreateCount: number;
|
|
9
|
+
layoutCreateCount: number;
|
|
10
|
+
disposeCounts: Record<string, number>;
|
|
11
|
+
rendered: string;
|
|
12
|
+
pageErrors: string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe('NavigationShell failed transition recovery', () => {
|
|
16
|
+
const harnesses: Array<{ destroy(): Promise<void> }> = [];
|
|
17
|
+
const tempRoots: string[] = [];
|
|
18
|
+
|
|
19
|
+
afterEach(async () => {
|
|
20
|
+
while (harnesses.length > 0) {
|
|
21
|
+
await harnesses.pop()?.destroy();
|
|
22
|
+
}
|
|
23
|
+
while (tempRoots.length > 0) {
|
|
24
|
+
rmSync(tempRoots.pop()!, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('does not orphan a controller when a sibling layout creation rejects', async () => {
|
|
29
|
+
const woodRoot = path.resolve(__dirname, '../..');
|
|
30
|
+
const rootDir = mkdtempSync(path.join(os.tmpdir(), 'wood-navigation-shell-repro-'));
|
|
31
|
+
tempRoots.push(rootDir);
|
|
32
|
+
mkdirSync(path.join(rootDir, 'ui'), { recursive: true });
|
|
33
|
+
|
|
34
|
+
writeFileSync(
|
|
35
|
+
path.join(rootDir, 'ui', 'ReproView.svelte'),
|
|
36
|
+
`<script lang="ts">
|
|
37
|
+
let { data = {}, page = {} } = $props<{
|
|
38
|
+
data?: { id?: string };
|
|
39
|
+
page?: { params?: { threadId?: string } };
|
|
40
|
+
}>();
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<main data-repro-view>
|
|
44
|
+
thread={page.params?.threadId ?? 'missing'};controller={data.id ?? 'missing'}
|
|
45
|
+
</main>`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
writeFileSync(
|
|
49
|
+
path.join(rootDir, 'ui', 'ReproLayout.svelte'),
|
|
50
|
+
`<script lang="ts">
|
|
51
|
+
import type { Snippet } from 'svelte';
|
|
52
|
+
let { children }: { children: Snippet } = $props();
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<section data-repro-layout>{@render children()}</section>`,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
writeFileSync(
|
|
59
|
+
path.join(rootDir, 'ui', 'NavigationShellRepro.svelte'),
|
|
60
|
+
`<script lang="ts">
|
|
61
|
+
import '/@fs/${woodRoot}/src/client/global_polyfill.ts';
|
|
62
|
+
import NavigationShell from '/@fs/${woodRoot}/src/components/NavigationShell.svelte';
|
|
63
|
+
import { getNavigation } from '/@fs/${woodRoot}/src/navigation/index.ts';
|
|
64
|
+
import ReproLayout from './ReproLayout.svelte';
|
|
65
|
+
import ReproView from './ReproView.svelte';
|
|
66
|
+
|
|
67
|
+
type ReproState = {
|
|
68
|
+
pageCreateCount: number;
|
|
69
|
+
layoutCreateCount: number;
|
|
70
|
+
disposeCounts: Record<string, number>;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const state: ReproState = {
|
|
74
|
+
pageCreateCount: 0,
|
|
75
|
+
layoutCreateCount: 0,
|
|
76
|
+
disposeCounts: {},
|
|
77
|
+
};
|
|
78
|
+
(window as unknown as { __WOOD_NAVIGATION_REPRO__: ReproState })
|
|
79
|
+
.__WOOD_NAVIGATION_REPRO__ = state;
|
|
80
|
+
|
|
81
|
+
function createController(id: string) {
|
|
82
|
+
state.disposeCounts[id] = 0;
|
|
83
|
+
return {
|
|
84
|
+
data: { id },
|
|
85
|
+
dispose() {
|
|
86
|
+
state.disposeCounts[id] += 1;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const entry = {
|
|
92
|
+
key: 'main.chat_dom',
|
|
93
|
+
windowName: 'main',
|
|
94
|
+
pageName: 'chat_dom',
|
|
95
|
+
layoutPaths: ['ui/ReproLayout.svelte'],
|
|
96
|
+
layoutControllers: ['repro-layout-controller'],
|
|
97
|
+
layoutPreserve: [false],
|
|
98
|
+
view: ReproView,
|
|
99
|
+
layouts: [ReproLayout],
|
|
100
|
+
};
|
|
101
|
+
const manifest = { [entry.key]: entry };
|
|
102
|
+
const initialController = createController('initial-page');
|
|
103
|
+
const initialLayoutControllers = [createController('initial-layout')];
|
|
104
|
+
|
|
105
|
+
async function createViewControllerFn() {
|
|
106
|
+
state.pageCreateCount += 1;
|
|
107
|
+
return createController(\`page-\${state.pageCreateCount}\`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function createLayoutControllersFn() {
|
|
111
|
+
state.layoutCreateCount += 1;
|
|
112
|
+
if (state.layoutCreateCount === 1) {
|
|
113
|
+
await Promise.resolve();
|
|
114
|
+
throw new Error('controlled layout creation failure');
|
|
115
|
+
}
|
|
116
|
+
return [createController(\`layout-\${state.layoutCreateCount}\`)];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getViewManifestEntryFn(viewKey: string) {
|
|
120
|
+
return manifest[viewKey as keyof typeof manifest] ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const navigation = getNavigation();
|
|
124
|
+
navigation.replace(entry.key, { threadId: '1' }, {});
|
|
125
|
+
(window as unknown as {
|
|
126
|
+
__WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
|
|
127
|
+
}).__WOOD_NAVIGATION_REPRO_GO__ = (threadId: string) => {
|
|
128
|
+
navigation.go(entry.key, { threadId }, {});
|
|
129
|
+
};
|
|
130
|
+
</script>
|
|
131
|
+
|
|
132
|
+
<NavigationShell
|
|
133
|
+
{manifest}
|
|
134
|
+
initialEntry={entry}
|
|
135
|
+
{initialController}
|
|
136
|
+
{initialLayoutControllers}
|
|
137
|
+
{createViewControllerFn}
|
|
138
|
+
{createLayoutControllersFn}
|
|
139
|
+
{getViewManifestEntryFn}
|
|
140
|
+
/>`,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const pageErrors: string[] = [];
|
|
144
|
+
const consoleErrors: string[] = [];
|
|
145
|
+
const harness = await browser.createHarness({
|
|
146
|
+
rootDir: woodRoot,
|
|
147
|
+
browser: { headless: true },
|
|
148
|
+
page: {
|
|
149
|
+
beforeMount({ page }) {
|
|
150
|
+
page.on('pageerror', (error: Error) => pageErrors.push(error.message));
|
|
151
|
+
page.on('console', (message: { type(): string; text(): string }) => {
|
|
152
|
+
if (message.type() === 'error') consoleErrors.push(message.text());
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
harnesses.push(harness);
|
|
158
|
+
const mounted = await harness.mountComponent(
|
|
159
|
+
path.join(rootDir, 'ui', 'NavigationShellRepro.svelte'),
|
|
160
|
+
);
|
|
161
|
+
await mounted.locator('[data-repro-view]').waitFor({ state: 'visible', timeout: 5_000 }).catch(
|
|
162
|
+
async (error: unknown) => {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`${error instanceof Error ? error.message : String(error)}\n` +
|
|
165
|
+
`Page errors: ${pageErrors.join(' | ')}\n` +
|
|
166
|
+
`Console errors: ${consoleErrors.join(' | ')}\n` +
|
|
167
|
+
`HTML: ${await mounted.page.content()}`,
|
|
168
|
+
);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
await expect.poll(async () => {
|
|
173
|
+
return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
174
|
+
}).toBe('thread=1;controller=initial-page');
|
|
175
|
+
|
|
176
|
+
await mounted.evaluate(() => {
|
|
177
|
+
(window as unknown as {
|
|
178
|
+
__WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
|
|
179
|
+
}).__WOOD_NAVIGATION_REPRO_GO__('2');
|
|
180
|
+
});
|
|
181
|
+
await expect.poll(async () => {
|
|
182
|
+
return await mounted.evaluate(() => {
|
|
183
|
+
return (window as unknown as {
|
|
184
|
+
__WOOD_NAVIGATION_REPRO__: { layoutCreateCount: number };
|
|
185
|
+
}).__WOOD_NAVIGATION_REPRO__.layoutCreateCount;
|
|
186
|
+
});
|
|
187
|
+
}).toBe(1);
|
|
188
|
+
await expect.poll(() => pageErrors.length + consoleErrors.length).toBeGreaterThan(0);
|
|
189
|
+
|
|
190
|
+
await mounted.evaluate(() => {
|
|
191
|
+
(window as unknown as {
|
|
192
|
+
__WOOD_NAVIGATION_REPRO_GO__: (threadId: string) => void;
|
|
193
|
+
}).__WOOD_NAVIGATION_REPRO_GO__('2');
|
|
194
|
+
});
|
|
195
|
+
await expect.poll(async () => {
|
|
196
|
+
return await mounted.evaluate(() => {
|
|
197
|
+
return (window as unknown as {
|
|
198
|
+
__WOOD_NAVIGATION_REPRO__: { pageCreateCount: number };
|
|
199
|
+
}).__WOOD_NAVIGATION_REPRO__.pageCreateCount;
|
|
200
|
+
});
|
|
201
|
+
}).toBe(2);
|
|
202
|
+
await expect.poll(async () => {
|
|
203
|
+
return (await mounted.locator('[data-repro-view]').textContent())?.replace(/\s+/g, ' ').trim();
|
|
204
|
+
}).toBe('thread=2;controller=page-2');
|
|
205
|
+
|
|
206
|
+
const snapshot = await mounted.evaluate<ReproSnapshot>(() => {
|
|
207
|
+
const state = (window as unknown as {
|
|
208
|
+
__WOOD_NAVIGATION_REPRO__: Omit<ReproSnapshot, 'rendered' | 'pageErrors'>;
|
|
209
|
+
}).__WOOD_NAVIGATION_REPRO__;
|
|
210
|
+
return {
|
|
211
|
+
...state,
|
|
212
|
+
disposeCounts: { ...state.disposeCounts },
|
|
213
|
+
rendered: document.querySelector('[data-repro-view]')?.textContent?.replace(/\s+/g, ' ').trim() ?? '',
|
|
214
|
+
pageErrors: [],
|
|
215
|
+
};
|
|
216
|
+
});
|
|
217
|
+
snapshot.pageErrors = [...pageErrors];
|
|
218
|
+
|
|
219
|
+
expect(
|
|
220
|
+
snapshot,
|
|
221
|
+
'A failed transition must preserve committed controllers and dispose any staged controller before a retry.',
|
|
222
|
+
).toEqual({
|
|
223
|
+
pageCreateCount: 2,
|
|
224
|
+
layoutCreateCount: 2,
|
|
225
|
+
disposeCounts: {
|
|
226
|
+
'initial-page': 1,
|
|
227
|
+
'initial-layout': 1,
|
|
228
|
+
'page-1': 1,
|
|
229
|
+
'page-2': 0,
|
|
230
|
+
'layout-2': 0,
|
|
231
|
+
},
|
|
232
|
+
rendered: 'thread=2;controller=page-2',
|
|
233
|
+
pageErrors: [],
|
|
234
|
+
});
|
|
235
|
+
}, 30_000);
|
|
236
|
+
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { setContext, onDestroy } from 'svelte';
|
|
3
|
-
import { getNavigation } from '@noego/wood/navigation';
|
|
3
|
+
import { getNavigation, type NavigationEntry } from '@noego/wood/navigation';
|
|
4
4
|
import { disposeControllerLifecycle } from '../controller/controller_lifecycle';
|
|
5
5
|
import { createNavigationMountKey } from '../navigation/navigation_identity';
|
|
6
6
|
import WoodRecursiveRender from './WoodRecursiveRender.svelte';
|
|
7
|
+
import { getWoodAppEnv } from '../client/app_env';
|
|
7
8
|
|
|
8
9
|
type ViewManifestEntry = {
|
|
9
10
|
key: string;
|
|
@@ -74,6 +75,8 @@
|
|
|
74
75
|
createLayoutMountKey(getInitialEntry(), index, activePageKey),
|
|
75
76
|
),
|
|
76
77
|
);
|
|
78
|
+
let navigationTransitionQueue: Promise<void> = Promise.resolve();
|
|
79
|
+
let destroyed = false;
|
|
77
80
|
|
|
78
81
|
let activePage = $derived({
|
|
79
82
|
key: activeEntry.key,
|
|
@@ -84,6 +87,7 @@
|
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
const navigation = getNavigation();
|
|
90
|
+
const appEnv = getWoodAppEnv();
|
|
87
91
|
|
|
88
92
|
function getWoodBridge(): WoodBridgeLike | null {
|
|
89
93
|
if (typeof window === 'undefined') {
|
|
@@ -202,20 +206,74 @@
|
|
|
202
206
|
return depth;
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
async function disposeControllerSet(
|
|
210
|
+
controller: unknown,
|
|
211
|
+
layoutControllers: Array<unknown>,
|
|
212
|
+
fromLayoutIndex = 0,
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
await disposeControllerLifecycle(controller);
|
|
215
|
+
for (let index = fromLayoutIndex; index < layoutControllers.length; index += 1) {
|
|
216
|
+
await disposeControllerLifecycle(layoutControllers[index]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
async function disposeActiveControllers(): Promise<void> {
|
|
206
|
-
await
|
|
207
|
-
|
|
221
|
+
await disposeControllerSet(activeController, activeLayoutControllers);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function disposeLayoutControllers(layoutControllers: Array<unknown>): Promise<void> {
|
|
225
|
+
for (const layoutController of layoutControllers) {
|
|
208
226
|
await disposeControllerLifecycle(layoutController);
|
|
209
227
|
}
|
|
210
228
|
}
|
|
211
229
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
function reportNavigationError(context: string, error: unknown): void {
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
console.error(`[wood] ${context}: ${message}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function disposeStagedControllers(
|
|
236
|
+
controller: unknown,
|
|
237
|
+
layoutControllers: Array<unknown>,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const cleanupResults = await Promise.allSettled([
|
|
240
|
+
disposeControllerLifecycle(controller),
|
|
241
|
+
disposeLayoutControllers(layoutControllers),
|
|
242
|
+
]);
|
|
243
|
+
for (const result of cleanupResults) {
|
|
244
|
+
if (result.status === 'rejected') {
|
|
245
|
+
reportNavigationError('navigation transition cleanup failed', result.reason);
|
|
246
|
+
}
|
|
216
247
|
}
|
|
217
248
|
}
|
|
218
249
|
|
|
250
|
+
async function createTransitionControllers(
|
|
251
|
+
entry: ViewManifestEntry,
|
|
252
|
+
preservedDepth: number,
|
|
253
|
+
): Promise<{ controller: unknown; layoutControllers: Array<unknown> }> {
|
|
254
|
+
const [controllerResult, layoutControllersResult] = await Promise.allSettled([
|
|
255
|
+
createViewControllerFn(entry),
|
|
256
|
+
createLayoutControllersFn(entry, preservedDepth),
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
if (controllerResult.status === 'fulfilled' && layoutControllersResult.status === 'fulfilled') {
|
|
260
|
+
return {
|
|
261
|
+
controller: controllerResult.value,
|
|
262
|
+
layoutControllers: layoutControllersResult.value,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await disposeStagedControllers(
|
|
267
|
+
controllerResult.status === 'fulfilled' ? controllerResult.value : undefined,
|
|
268
|
+
layoutControllersResult.status === 'fulfilled' ? layoutControllersResult.value : [],
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (controllerResult.status === 'rejected') {
|
|
272
|
+
throw controllerResult.reason;
|
|
273
|
+
}
|
|
274
|
+
throw layoutControllersResult.reason;
|
|
275
|
+
}
|
|
276
|
+
|
|
219
277
|
function mergeLayoutControllers(
|
|
220
278
|
preservedDepth: number,
|
|
221
279
|
createdControllers: Array<unknown>,
|
|
@@ -230,14 +288,69 @@
|
|
|
230
288
|
return merged;
|
|
231
289
|
}
|
|
232
290
|
|
|
291
|
+
async function applyNavigationEntry(navEntry: NavigationEntry | null): Promise<void> {
|
|
292
|
+
if (destroyed || !navEntry) return;
|
|
293
|
+
|
|
294
|
+
const newKey = navEntry.page;
|
|
295
|
+
const entry = getViewManifestEntryFn(newKey);
|
|
296
|
+
if (!entry) return;
|
|
297
|
+
|
|
298
|
+
const nextPageKey = createNavigationMountKey(newKey, navEntry.params);
|
|
299
|
+
|
|
300
|
+
if (nextPageKey === activePageKey) {
|
|
301
|
+
activeParams = { ...navEntry.params };
|
|
302
|
+
activeQuery = { ...navEntry.query };
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const preservedDepth = computePreservedLayoutDepth(
|
|
307
|
+
activeEntry,
|
|
308
|
+
entry,
|
|
309
|
+
navEntry.options ?? {},
|
|
310
|
+
);
|
|
311
|
+
const preservedLayoutKeys = activeLayoutKeys.slice(0, preservedDepth);
|
|
312
|
+
const previousController = activeController;
|
|
313
|
+
const previousLayoutControllers = [...activeLayoutControllers];
|
|
314
|
+
const { controller, layoutControllers } = await createTransitionControllers(entry, preservedDepth);
|
|
315
|
+
|
|
316
|
+
if (destroyed) {
|
|
317
|
+
await disposeStagedControllers(controller, layoutControllers);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const nextLayoutControllers = mergeLayoutControllers(
|
|
322
|
+
preservedDepth,
|
|
323
|
+
layoutControllers,
|
|
324
|
+
entry.layoutPaths.length,
|
|
325
|
+
);
|
|
326
|
+
const nextLayoutKeys = createLayoutMountKeys(entry, nextPageKey, preservedLayoutKeys);
|
|
327
|
+
|
|
328
|
+
activeEntry = entry;
|
|
329
|
+
activeController = controller;
|
|
330
|
+
activeLayoutControllers = nextLayoutControllers;
|
|
331
|
+
activeParams = { ...navEntry.params };
|
|
332
|
+
activeQuery = { ...navEntry.query };
|
|
333
|
+
activePageKey = nextPageKey;
|
|
334
|
+
activeLayoutKeys = nextLayoutKeys;
|
|
335
|
+
|
|
336
|
+
await disposeControllerSet(previousController, previousLayoutControllers, preservedDepth);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function enqueueNavigationEntry(navEntry: NavigationEntry | null): void {
|
|
340
|
+
navigationTransitionQueue = navigationTransitionQueue
|
|
341
|
+
.then(() => applyNavigationEntry(navEntry))
|
|
342
|
+
.catch((error: unknown) => {
|
|
343
|
+
reportNavigationError('navigation transition failed', error);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
233
347
|
setContext('wood:navigate', (
|
|
234
348
|
viewKey: string,
|
|
235
349
|
params?: Record<string, unknown>,
|
|
236
350
|
options?: { replace?: boolean; preserveLayouts?: boolean },
|
|
237
351
|
) => {
|
|
238
352
|
void navigateWithinCurrentWindow(viewKey, params, options).catch((error) => {
|
|
239
|
-
|
|
240
|
-
console.error(`[wood] navigation failed: ${message}`);
|
|
353
|
+
reportNavigationError('navigation failed', error);
|
|
241
354
|
});
|
|
242
355
|
});
|
|
243
356
|
|
|
@@ -256,7 +369,7 @@
|
|
|
256
369
|
|
|
257
370
|
let initialFire = true;
|
|
258
371
|
|
|
259
|
-
const unsubscribe = navigation.subscribe(
|
|
372
|
+
const unsubscribe = navigation.subscribe((navEntry) => {
|
|
260
373
|
if (initialFire) {
|
|
261
374
|
initialFire = false;
|
|
262
375
|
if (navEntry) {
|
|
@@ -268,44 +381,7 @@
|
|
|
268
381
|
return;
|
|
269
382
|
}
|
|
270
383
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const newKey = navEntry.page;
|
|
274
|
-
const entry = getViewManifestEntryFn(newKey);
|
|
275
|
-
if (!entry) return;
|
|
276
|
-
|
|
277
|
-
const nextPageKey = createNavigationMountKey(newKey, navEntry.params);
|
|
278
|
-
|
|
279
|
-
if (nextPageKey === activePageKey) {
|
|
280
|
-
activeParams = { ...navEntry.params };
|
|
281
|
-
activeQuery = { ...navEntry.query };
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const preservedDepth = computePreservedLayoutDepth(
|
|
286
|
-
activeEntry,
|
|
287
|
-
entry,
|
|
288
|
-
navEntry.options ?? {},
|
|
289
|
-
);
|
|
290
|
-
const preservedLayoutKeys = activeLayoutKeys.slice(0, preservedDepth);
|
|
291
|
-
|
|
292
|
-
await disposeChangedControllers(preservedDepth);
|
|
293
|
-
const [controller, layoutControllers] = await Promise.all([
|
|
294
|
-
createViewControllerFn(entry),
|
|
295
|
-
createLayoutControllersFn(entry, preservedDepth),
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
activeEntry = entry;
|
|
299
|
-
activeController = controller;
|
|
300
|
-
activeLayoutControllers = mergeLayoutControllers(
|
|
301
|
-
preservedDepth,
|
|
302
|
-
layoutControllers,
|
|
303
|
-
entry.layoutPaths.length,
|
|
304
|
-
);
|
|
305
|
-
activeParams = { ...navEntry.params };
|
|
306
|
-
activeQuery = { ...navEntry.query };
|
|
307
|
-
activePageKey = nextPageKey;
|
|
308
|
-
activeLayoutKeys = createLayoutMountKeys(entry, nextPageKey, preservedLayoutKeys);
|
|
384
|
+
enqueueNavigationEntry(navEntry);
|
|
309
385
|
});
|
|
310
386
|
|
|
311
387
|
const bridge = getWoodBridge();
|
|
@@ -328,9 +404,14 @@
|
|
|
328
404
|
});
|
|
329
405
|
|
|
330
406
|
onDestroy(() => {
|
|
407
|
+
destroyed = true;
|
|
331
408
|
unsubscribe();
|
|
332
409
|
unsubscribeWindowNavigate?.();
|
|
333
|
-
void
|
|
410
|
+
void navigationTransitionQueue
|
|
411
|
+
.then(() => disposeActiveControllers())
|
|
412
|
+
.catch((error: unknown) => {
|
|
413
|
+
reportNavigationError('navigation teardown failed', error);
|
|
414
|
+
});
|
|
334
415
|
});
|
|
335
416
|
</script>
|
|
336
417
|
|
|
@@ -345,3 +426,35 @@
|
|
|
345
426
|
query={activeQuery}
|
|
346
427
|
page={activePage}
|
|
347
428
|
/>
|
|
429
|
+
|
|
430
|
+
{#if appEnv !== 'packaged'}
|
|
431
|
+
<div class="wood-env-ribbon" class:wood-env-ribbon--local={appEnv === 'local'} aria-hidden="true">
|
|
432
|
+
{appEnv === 'dev' ? 'DEBUG' : 'LOCAL'}
|
|
433
|
+
</div>
|
|
434
|
+
{/if}
|
|
435
|
+
|
|
436
|
+
<style>
|
|
437
|
+
.wood-env-ribbon {
|
|
438
|
+
position: absolute;
|
|
439
|
+
top: 22px;
|
|
440
|
+
right: -34px;
|
|
441
|
+
z-index: 2147483647;
|
|
442
|
+
pointer-events: none;
|
|
443
|
+
transform: rotate(45deg);
|
|
444
|
+
width: 140px;
|
|
445
|
+
text-align: center;
|
|
446
|
+
padding: 3px 0;
|
|
447
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
font-weight: 700;
|
|
450
|
+
letter-spacing: 2px;
|
|
451
|
+
color: #fff;
|
|
452
|
+
background: #b71c1c;
|
|
453
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
|
454
|
+
user-select: none;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.wood-env-ribbon--local {
|
|
458
|
+
background: #1b7f3a;
|
|
459
|
+
}
|
|
460
|
+
</style>
|