@noego/wood 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/client/index.cjs +18 -11
- package/dist/client/index.cjs.map +1 -1
- package/dist/client/index.d.cts +21 -1
- package/dist/client/index.d.ts +21 -1
- package/dist/client/index.js +17 -11
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/renderer_app_generator.cjs +2 -9
- package/dist/codegen/renderer_app_generator.cjs.map +1 -1
- package/dist/codegen/renderer_app_generator.js +2 -9
- package/dist/codegen/renderer_app_generator.js.map +1 -1
- package/dist/controller/cleanup.cjs +39 -0
- package/dist/controller/cleanup.cjs.map +1 -0
- package/dist/controller/cleanup.d.cts +4 -0
- package/dist/controller/cleanup.d.ts +4 -0
- package/dist/controller/cleanup.js +15 -0
- package/dist/controller/cleanup.js.map +1 -0
- package/dist/controller/controller_lifecycle.cjs +26 -2
- package/dist/controller/controller_lifecycle.cjs.map +1 -1
- package/dist/controller/controller_lifecycle.d.cts +3 -1
- package/dist/controller/controller_lifecycle.d.ts +3 -1
- package/dist/controller/controller_lifecycle.js +24 -1
- package/dist/controller/controller_lifecycle.js.map +1 -1
- package/dist/frontend/frontend_lifecycle.cjs +5 -7
- package/dist/frontend/frontend_lifecycle.cjs.map +1 -1
- package/dist/frontend/frontend_lifecycle.d.cts +1 -14
- package/dist/frontend/frontend_lifecycle.d.ts +1 -14
- package/dist/frontend/frontend_lifecycle.js +5 -7
- package/dist/frontend/frontend_lifecycle.js.map +1 -1
- package/dist/frontend/frontend_surface.cjs +130 -104
- package/dist/frontend/frontend_surface.cjs.map +1 -1
- package/dist/frontend/frontend_surface.js +131 -105
- package/dist/frontend/frontend_surface.js.map +1 -1
- package/dist/frontend/work_registry.cjs +29 -4
- package/dist/frontend/work_registry.cjs.map +1 -1
- package/dist/frontend/work_registry.d.cts +2 -0
- package/dist/frontend/work_registry.d.ts +2 -0
- package/dist/frontend/work_registry.js +29 -4
- package/dist/frontend/work_registry.js.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/ipc/index.cjs +6 -2
- package/dist/ipc/index.cjs.map +1 -1
- package/dist/ipc/index.d.cts +9 -0
- package/dist/ipc/index.d.ts +9 -0
- package/dist/ipc/index.js +6 -2
- package/dist/ipc/index.js.map +1 -1
- package/dist/main/index.cjs +53 -35
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +11 -2
- package/dist/main/index.d.ts +11 -2
- package/dist/main/index.js +56 -40
- package/dist/main/index.js.map +1 -1
- package/dist/main/startup_failure_handler.cjs +67 -0
- package/dist/main/startup_failure_handler.cjs.map +1 -0
- package/dist/main/startup_failure_handler.d.cts +16 -0
- package/dist/main/startup_failure_handler.d.ts +16 -0
- package/dist/main/startup_failure_handler.js +43 -0
- package/dist/main/startup_failure_handler.js.map +1 -0
- package/dist/router/index.d.cts +3 -0
- package/dist/router/index.d.ts +3 -0
- package/dist/router/wood_router.cjs +204 -123
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +20 -0
- package/dist/router/wood_router.d.ts +20 -0
- package/dist/router/wood_router.js +206 -125
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/index.cjs +13 -0
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +8 -4
- package/dist/runtime/index.d.ts +8 -4
- package/dist/runtime/index.js +8 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/renderer_config_extension.cjs +1 -2
- package/dist/runtime/renderer_config_extension.cjs.map +1 -1
- package/dist/runtime/renderer_config_extension.d.cts +1 -1
- package/dist/runtime/renderer_config_extension.d.ts +1 -1
- package/dist/runtime/renderer_config_extension.js +1 -2
- package/dist/runtime/renderer_config_extension.js.map +1 -1
- package/dist/runtime/renderer_log_dispatcher.cjs +2 -0
- 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 +1 -0
- package/dist/runtime/renderer_log_dispatcher.js.map +1 -1
- package/dist/runtime/runtime.cjs +107 -57
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.d.cts +12 -0
- package/dist/runtime/runtime.d.ts +12 -0
- package/dist/runtime/runtime.js +110 -59
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/runtime_module.cjs +58 -0
- package/dist/runtime/runtime_module.cjs.map +1 -0
- package/dist/runtime/runtime_module.d.cts +22 -0
- package/dist/runtime/runtime_module.d.ts +22 -0
- package/dist/runtime/runtime_module.js +30 -0
- package/dist/runtime/runtime_module.js.map +1 -0
- package/dist/testing/create_controller_harness.d.cts +3 -0
- package/dist/testing/create_controller_harness.d.ts +3 -0
- package/dist/testing/create_router_bridge.d.cts +3 -0
- package/dist/testing/create_router_bridge.d.ts +3 -0
- package/dist/testing/create_test_router.d.cts +3 -0
- package/dist/testing/create_test_router.d.ts +3 -0
- package/dist/testing/index.cjs +3 -0
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +6 -1
- package/dist/testing/index.d.ts +6 -1
- package/dist/testing/index.js +2 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/registration_only_root.cjs +168 -0
- package/dist/testing/registration_only_root.cjs.map +1 -0
- package/dist/testing/registration_only_root.d.cts +46 -0
- package/dist/testing/registration_only_root.d.ts +46 -0
- package/dist/testing/registration_only_root.js +141 -0
- package/dist/testing/registration_only_root.js.map +1 -0
- package/dist/testing/test_wood_config.cjs +382 -70
- package/dist/testing/test_wood_config.cjs.map +1 -1
- package/dist/testing/test_wood_config.d.cts +25 -1
- package/dist/testing/test_wood_config.d.ts +25 -1
- package/dist/testing/test_wood_config.js +388 -71
- package/dist/testing/test_wood_config.js.map +1 -1
- package/dist/testing/wood_dom_animation.cjs +213 -0
- package/dist/testing/wood_dom_animation.cjs.map +1 -0
- package/dist/testing/wood_dom_animation.d.cts +89 -0
- package/dist/testing/wood_dom_animation.d.ts +89 -0
- package/dist/testing/wood_dom_animation.js +185 -0
- package/dist/testing/wood_dom_animation.js.map +1 -0
- package/dist/testing/wood_env_renderer.cjs +81 -32
- package/dist/testing/wood_env_renderer.cjs.map +1 -1
- package/dist/testing/wood_env_renderer.d.cts +32 -1
- package/dist/testing/wood_env_renderer.d.ts +32 -1
- package/dist/testing/wood_env_renderer.js +84 -32
- package/dist/testing/wood_env_renderer.js.map +1 -1
- package/dist/transport/in_process_router_transport.d.cts +3 -0
- package/dist/transport/in_process_router_transport.d.ts +3 -0
- package/dist/transport/index.d.cts +3 -0
- package/dist/transport/index.d.ts +3 -0
- package/package.json +8 -6
- package/src/components/NavigationShell.svelte +12 -7
- package/src/controller/cleanup.ts +8 -0
- package/src/controller/controller_lifecycle.ts +24 -0
- package/src/navigation/layout_preservation.ts +45 -0
|
@@ -2,6 +2,7 @@ async function disposeControllerLifecycle(target) {
|
|
|
2
2
|
if (!target || typeof target !== "object" && typeof target !== "function") {
|
|
3
3
|
return;
|
|
4
4
|
}
|
|
5
|
+
guardControllerLifecycleOnce(target);
|
|
5
6
|
const lifecycle = target;
|
|
6
7
|
if (typeof lifecycle.dispose === "function") {
|
|
7
8
|
await lifecycle.dispose();
|
|
@@ -11,7 +12,29 @@ async function disposeControllerLifecycle(target) {
|
|
|
11
12
|
await lifecycle.destroy();
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
const guardedControllers = /* @__PURE__ */ new WeakSet();
|
|
16
|
+
function guardControllerLifecycleOnce(target) {
|
|
17
|
+
if (!target || typeof target !== "object" && typeof target !== "function") return;
|
|
18
|
+
if (guardedControllers.has(target)) return;
|
|
19
|
+
guardedControllers.add(target);
|
|
20
|
+
const instance = target;
|
|
21
|
+
for (const name of ["dispose", "destroy"]) {
|
|
22
|
+
const original = instance[name];
|
|
23
|
+
if (typeof original !== "function") continue;
|
|
24
|
+
let called = false;
|
|
25
|
+
Object.defineProperty(instance, name, {
|
|
26
|
+
configurable: true,
|
|
27
|
+
writable: true,
|
|
28
|
+
value: function(...args) {
|
|
29
|
+
if (called) return;
|
|
30
|
+
called = true;
|
|
31
|
+
return original.apply(this, args);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
14
36
|
export {
|
|
15
|
-
disposeControllerLifecycle
|
|
37
|
+
disposeControllerLifecycle,
|
|
38
|
+
guardControllerLifecycleOnce
|
|
16
39
|
};
|
|
17
40
|
//# sourceMappingURL=controller_lifecycle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n"],"mappings":"AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/controller/controller_lifecycle.ts"],"sourcesContent":["export interface ControllerLifecycle {\n dispose?(): void | Promise<void>;\n destroy?(): void | Promise<void>;\n}\n\n/**\n * Dispose a controller or controller-like instance if it exposes a lifecycle\n * hook. `dispose` is the preferred Wood lifecycle name; `destroy` is supported\n * for existing page controllers.\n */\nexport async function disposeControllerLifecycle(target: unknown): Promise<void> {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) {\n return;\n }\n\n guardControllerLifecycleOnce(target);\n const lifecycle = target as ControllerLifecycle;\n if (typeof lifecycle.dispose === 'function') {\n await lifecycle.dispose();\n return;\n }\n if (typeof lifecycle.destroy === 'function') {\n await lifecycle.destroy();\n }\n}\n\nconst guardedControllers = new WeakSet<object>();\n\n/** Share one teardown attempt between the explicit controller owner and IoC. */\nexport function guardControllerLifecycleOnce(target: unknown): void {\n if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;\n if (guardedControllers.has(target as object)) return;\n guardedControllers.add(target as object);\n const instance = target as Record<string, unknown>;\n for (const name of ['dispose', 'destroy'] as const) {\n const original = instance[name];\n if (typeof original !== 'function') continue;\n let called = false;\n Object.defineProperty(instance, name, {\n configurable: true, writable: true,\n value: function (this: unknown, ...args: unknown[]) {\n if (called) return;\n called = true;\n return original.apply(this, args);\n },\n });\n }\n}\n"],"mappings":"AAUA,eAAsB,2BAA2B,QAAgC;AAC/E,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,YAAa;AAC3E;AAAA,EACF;AAEA,+BAA6B,MAAM;AACnC,QAAM,YAAY;AAClB,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AACxB;AAAA,EACF;AACA,MAAI,OAAO,UAAU,YAAY,YAAY;AAC3C,UAAM,UAAU,QAAQ;AAAA,EAC1B;AACF;AAEA,MAAM,qBAAqB,oBAAI,QAAgB;AAGxC,SAAS,6BAA6B,QAAuB;AAClE,MAAI,CAAC,UAAW,OAAO,WAAW,YAAY,OAAO,WAAW,WAAa;AAC7E,MAAI,mBAAmB,IAAI,MAAgB,EAAG;AAC9C,qBAAmB,IAAI,MAAgB;AACvC,QAAM,WAAW;AACjB,aAAW,QAAQ,CAAC,WAAW,SAAS,GAAY;AAClD,UAAM,WAAW,SAAS,IAAI;AAC9B,QAAI,OAAO,aAAa,WAAY;AACpC,QAAI,SAAS;AACb,WAAO,eAAe,UAAU,MAAM;AAAA,MACpC,cAAc;AAAA,MAAM,UAAU;AAAA,MAC9B,OAAO,YAA4B,MAAiB;AAClD,YAAI,OAAQ;AACZ,iBAAS;AACT,eAAO,SAAS,MAAM,MAAM,IAAI;AAAA,MAClC;AAAA,IACF,CAAC;AAAA,EACH;AACF;","names":[]}
|
|
@@ -21,6 +21,7 @@ __export(frontend_lifecycle_exports, {
|
|
|
21
21
|
FrontendLifecycleOwner: () => FrontendLifecycleOwner
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(frontend_lifecycle_exports);
|
|
24
|
+
var import_cleanup = require("../controller/cleanup.js");
|
|
24
25
|
var import_work_registry = require("./work_registry.js");
|
|
25
26
|
class FrontendLifecycleOwner {
|
|
26
27
|
constructor(registry, owner) {
|
|
@@ -56,15 +57,12 @@ class FrontendLifecycleOwner {
|
|
|
56
57
|
get isDisposed() {
|
|
57
58
|
return this.disposed;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (this.disposed) return;
|
|
60
|
+
dispose() {
|
|
61
|
+
if (this.closing) return this.closing;
|
|
62
62
|
this.disposed = true;
|
|
63
63
|
this.abortController.abort();
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
this.resources.length = 0;
|
|
64
|
+
const resources = this.resources.splice(0).reverse();
|
|
65
|
+
return this.closing = (0, import_cleanup.cleanupAll)(resources.map((resource) => () => resource.dispose()), "Wood frontend lifecycle cleanup failed");
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n
|
|
1
|
+
{"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n private closing?: Promise<void>;\n dispose(): Promise<void> {\n if (this.closing) return this.closing;\n this.disposed = true;\n this.abortController.abort();\n const resources = this.resources.splice(0).reverse();\n return this.closing = cleanupAll(resources.map(resource => () => resource.dispose()), 'Wood frontend lifecycle cleanup failed');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAA2B;AAC3B,2BAAgD;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAIA,UAAyB;AACvB,QAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,UAAM,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,QAAQ;AACnD,WAAO,KAAK,cAAU,2BAAW,UAAU,IAAI,cAAY,MAAM,SAAS,QAAQ,CAAC,GAAG,wCAAwC;AAAA,EAChI;AACF;","names":[]}
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { WorkRegistry } from './work_registry.cjs';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Production lifecycle owners for deliberately detached frontend work
|
|
5
|
-
* (18A D-FAS-01 §2.2). Two natural owners exist per environment:
|
|
6
|
-
*
|
|
7
|
-
* page lifecycle — default for PageController work; aborted and
|
|
8
|
-
* disposed when that page ends
|
|
9
|
-
* application lifecycle — explicit for work that intentionally survives
|
|
10
|
-
* page navigation; disposed with the environment
|
|
11
|
-
*
|
|
12
|
-
* PageControllers/client services register their own deliberately detached
|
|
13
|
-
* finite work through their injected lifecycle owner — tests do not repair
|
|
14
|
-
* unowned product code by wrapping it in track() from the outside.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
3
|
interface FrontendLifecycle {
|
|
18
4
|
readonly signal: AbortSignal;
|
|
19
5
|
track<T>(work: Promise<T>, options: {
|
|
@@ -44,6 +30,7 @@ declare class FrontendLifecycleOwner implements FrontendLifecycle {
|
|
|
44
30
|
}): void;
|
|
45
31
|
get isDisposed(): boolean;
|
|
46
32
|
/** Abort owned work and dispose owned resources in reverse order. */
|
|
33
|
+
private closing?;
|
|
47
34
|
dispose(): Promise<void>;
|
|
48
35
|
}
|
|
49
36
|
|
|
@@ -1,19 +1,5 @@
|
|
|
1
1
|
import { WorkRegistry } from './work_registry.js';
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Production lifecycle owners for deliberately detached frontend work
|
|
5
|
-
* (18A D-FAS-01 §2.2). Two natural owners exist per environment:
|
|
6
|
-
*
|
|
7
|
-
* page lifecycle — default for PageController work; aborted and
|
|
8
|
-
* disposed when that page ends
|
|
9
|
-
* application lifecycle — explicit for work that intentionally survives
|
|
10
|
-
* page navigation; disposed with the environment
|
|
11
|
-
*
|
|
12
|
-
* PageControllers/client services register their own deliberately detached
|
|
13
|
-
* finite work through their injected lifecycle owner — tests do not repair
|
|
14
|
-
* unowned product code by wrapping it in track() from the outside.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
3
|
interface FrontendLifecycle {
|
|
18
4
|
readonly signal: AbortSignal;
|
|
19
5
|
track<T>(work: Promise<T>, options: {
|
|
@@ -44,6 +30,7 @@ declare class FrontendLifecycleOwner implements FrontendLifecycle {
|
|
|
44
30
|
}): void;
|
|
45
31
|
get isDisposed(): boolean;
|
|
46
32
|
/** Abort owned work and dispose owned resources in reverse order. */
|
|
33
|
+
private closing?;
|
|
47
34
|
dispose(): Promise<void>;
|
|
48
35
|
}
|
|
49
36
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cleanupAll } from "../controller/cleanup.js";
|
|
1
2
|
import { FrontendWorkError } from "./work_registry.js";
|
|
2
3
|
class FrontendLifecycleOwner {
|
|
3
4
|
constructor(registry, owner) {
|
|
@@ -33,15 +34,12 @@ class FrontendLifecycleOwner {
|
|
|
33
34
|
get isDisposed() {
|
|
34
35
|
return this.disposed;
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (this.disposed) return;
|
|
37
|
+
dispose() {
|
|
38
|
+
if (this.closing) return this.closing;
|
|
39
39
|
this.disposed = true;
|
|
40
40
|
this.abortController.abort();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
this.resources.length = 0;
|
|
41
|
+
const resources = this.resources.splice(0).reverse();
|
|
42
|
+
return this.closing = cleanupAll(resources.map((resource) => () => resource.dispose()), "Wood frontend lifecycle cleanup failed");
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n
|
|
1
|
+
{"version":3,"sources":["../../src/frontend/frontend_lifecycle.ts"],"sourcesContent":["/**\n * Production lifecycle owners for deliberately detached frontend work\n * (18A D-FAS-01 §2.2). Two natural owners exist per environment:\n *\n * page lifecycle — default for PageController work; aborted and\n * disposed when that page ends\n * application lifecycle — explicit for work that intentionally survives\n * page navigation; disposed with the environment\n *\n * PageControllers/client services register their own deliberately detached\n * finite work through their injected lifecycle owner — tests do not repair\n * unowned product code by wrapping it in track() from the outside.\n */\nimport { cleanupAll } from '../controller/cleanup.js';\nimport { WorkRegistry, FrontendWorkError } from './work_registry.js';\n\nexport interface FrontendLifecycle {\n readonly signal: AbortSignal;\n\n track<T>(\n work: Promise<T>,\n options: {\n label: string;\n kind?: string;\n },\n ): Promise<T>;\n\n own(resource: { dispose(): void | Promise<void> }): void;\n}\n\nexport class FrontendLifecycleOwner implements FrontendLifecycle {\n private readonly abortController = new AbortController();\n private readonly resources: Array<{ dispose(): void | Promise<void> }> = [];\n private disposed = false;\n\n constructor(\n private readonly registry: WorkRegistry,\n /** Diagnostic owner identity, e.g. `page main.chat_dom` or `application`. */\n readonly owner: string,\n ) {}\n\n get signal(): AbortSignal {\n return this.abortController.signal;\n }\n\n track<T>(work: Promise<T>, options: { label: string; kind?: string }): Promise<T> {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot track \"${options.label}\": lifecycle \"${this.owner}\" is disposed. ` +\n 'Work started after a page/environment ended is a product lifecycle bug.',\n );\n }\n return this.registry.track(work, {\n label: options.label,\n kind: options.kind ?? 'tracked',\n owner: this.owner,\n });\n }\n\n own(resource: { dispose(): void | Promise<void> }): void {\n if (this.disposed) {\n throw new FrontendWorkError(\n `cannot own a resource: lifecycle \"${this.owner}\" is disposed.`,\n );\n }\n this.resources.push(resource);\n }\n\n get isDisposed(): boolean {\n return this.disposed;\n }\n\n /** Abort owned work and dispose owned resources in reverse order. */\n private closing?: Promise<void>;\n dispose(): Promise<void> {\n if (this.closing) return this.closing;\n this.disposed = true;\n this.abortController.abort();\n const resources = this.resources.splice(0).reverse();\n return this.closing = cleanupAll(resources.map(resource => () => resource.dispose()), 'Wood frontend lifecycle cleanup failed');\n }\n}\n"],"mappings":"AAaA,SAAS,kBAAkB;AAC3B,SAAuB,yBAAyB;AAgBzC,MAAM,uBAAoD;AAAA,EAK/D,YACmB,UAER,OACT;AAHiB;AAER;AAPX,SAAiB,kBAAkB,IAAI,gBAAgB;AACvD,SAAiB,YAAwD,CAAC;AAC1E,SAAQ,WAAW;AAAA,EAMhB;AAAA,EAEH,IAAI,SAAsB;AACxB,WAAO,KAAK,gBAAgB;AAAA,EAC9B;AAAA,EAEA,MAAS,MAAkB,SAAuD;AAChF,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,iBAAiB,QAAQ,KAAK,iBAAiB,KAAK,KAAK;AAAA,MAE3D;AAAA,IACF;AACA,WAAO,KAAK,SAAS,MAAM,MAAM;AAAA,MAC/B,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ,QAAQ;AAAA,MACtB,OAAO,KAAK;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,IAAI,UAAqD;AACvD,QAAI,KAAK,UAAU;AACjB,YAAM,IAAI;AAAA,QACR,qCAAqC,KAAK,KAAK;AAAA,MACjD;AAAA,IACF;AACA,SAAK,UAAU,KAAK,QAAQ;AAAA,EAC9B;AAAA,EAEA,IAAI,aAAsB;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAIA,UAAyB;AACvB,QAAI,KAAK,QAAS,QAAO,KAAK;AAC9B,SAAK,WAAW;AAChB,SAAK,gBAAgB,MAAM;AAC3B,UAAM,YAAY,KAAK,UAAU,OAAO,CAAC,EAAE,QAAQ;AACnD,WAAO,KAAK,UAAU,WAAW,UAAU,IAAI,cAAY,MAAM,SAAS,QAAQ,CAAC,GAAG,wCAAwC;AAAA,EAChI;AACF;","names":[]}
|
|
@@ -24,6 +24,7 @@ __export(frontend_surface_exports, {
|
|
|
24
24
|
module.exports = __toCommonJS(frontend_surface_exports);
|
|
25
25
|
var import_ioc = require("@noego/ioc");
|
|
26
26
|
var import_layout_preservation = require("../navigation/layout_preservation.js");
|
|
27
|
+
var import_cleanup = require("../controller/cleanup.js");
|
|
27
28
|
var import_controller_lifecycle = require("../controller/controller_lifecycle.js");
|
|
28
29
|
var import_page_catalog = require("./page_catalog.js");
|
|
29
30
|
var import_work_registry = require("./work_registry.js");
|
|
@@ -34,26 +35,6 @@ function registerControllerUnlessBound(scope, ControllerClass) {
|
|
|
34
35
|
if (typeof registrable.isRegistered === "function" && registrable.isRegistered(ControllerClass)) return;
|
|
35
36
|
if (typeof scope.registerClass === "function") scope.registerClass(ControllerClass);
|
|
36
37
|
}
|
|
37
|
-
function guardControllerLifecycleOnce(instance) {
|
|
38
|
-
if (!instance || typeof instance !== "object") return;
|
|
39
|
-
const target = instance;
|
|
40
|
-
for (const hook of ["dispose", "destroy"]) {
|
|
41
|
-
const original = target[hook];
|
|
42
|
-
if (typeof original !== "function") continue;
|
|
43
|
-
if (Object.prototype.hasOwnProperty.call(target, hook)) continue;
|
|
44
|
-
let called = false;
|
|
45
|
-
Object.defineProperty(target, hook, {
|
|
46
|
-
configurable: true,
|
|
47
|
-
writable: true,
|
|
48
|
-
enumerable: false,
|
|
49
|
-
value: function guarded(...args) {
|
|
50
|
-
if (called) return void 0;
|
|
51
|
-
called = true;
|
|
52
|
-
return original.apply(this, args);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
38
|
function createFrontendSurface(options) {
|
|
58
39
|
const { catalog, rendererRoot, registry, navigation } = options;
|
|
59
40
|
const flushHooks = options.flushHooks ?? [];
|
|
@@ -61,6 +42,16 @@ function createFrontendSurface(options) {
|
|
|
61
42
|
let active = null;
|
|
62
43
|
let transitioning = false;
|
|
63
44
|
let disposed = false;
|
|
45
|
+
let navigationQueue = Promise.resolve();
|
|
46
|
+
let closing;
|
|
47
|
+
const retainedPageScopes = /* @__PURE__ */ new Map();
|
|
48
|
+
const locallyOwnedControllers = /* @__PURE__ */ new WeakSet();
|
|
49
|
+
const markController = (instance, token) => {
|
|
50
|
+
const root = rendererRoot;
|
|
51
|
+
if (!root.isRegistered?.(token) && instance && typeof instance === "object") locallyOwnedControllers.add(instance);
|
|
52
|
+
(0, import_controller_lifecycle.guardControllerLifecycleOnce)(instance);
|
|
53
|
+
};
|
|
54
|
+
const disposeLocalController = (instance) => instance && typeof instance === "object" && locallyOwnedControllers.has(instance) ? (0, import_controller_lifecycle.disposeControllerLifecycle)(instance) : Promise.resolve();
|
|
64
55
|
const currentOwner = () => active ? `page ${active.identity}` : "application";
|
|
65
56
|
async function constructPage(view, params, query, preserved = []) {
|
|
66
57
|
const identity = (0, import_page_catalog.pageIdentity)(view);
|
|
@@ -72,91 +63,111 @@ function createFrontendSurface(options) {
|
|
|
72
63
|
const ControllerClass = await options.resolveController(view.controller, view);
|
|
73
64
|
const scope = rendererRoot.extend();
|
|
74
65
|
const lifecycle = new import_frontend_lifecycle.FrontendLifecycleOwner(registry, `page ${identity}`);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
66
|
+
const rollback = [
|
|
67
|
+
() => import_ioc.ExecutionContext.run(scope, () => scope.dispose?.()),
|
|
68
|
+
() => lifecycle.dispose()
|
|
69
|
+
];
|
|
70
|
+
try {
|
|
71
|
+
scope.registerFunction?.(import_frontend_lifecycle.FrontendLifecycleOwner, () => lifecycle, { loadAs: import_ioc.LoadAs.Scoped });
|
|
72
|
+
await import_ioc.ExecutionContext.run(scope, () => options.configurePageScope?.(scope, view, { kind: "page", controller: view.controller }));
|
|
73
|
+
registerControllerUnlessBound(scope, ControllerClass);
|
|
74
|
+
const layouts = [...preserved];
|
|
75
|
+
for (let index = preserved.length; index < view.layouts.length; index += 1) {
|
|
76
|
+
const layoutIdentity = view.layoutControllers?.[index];
|
|
77
|
+
if (!layoutIdentity) continue;
|
|
78
|
+
const preserve = view.layoutPreserve?.[index] === true;
|
|
79
|
+
const LayoutClass = await options.resolveController(layoutIdentity, view);
|
|
80
|
+
const ownScope = preserve ? rendererRoot.extend() : null;
|
|
81
|
+
const ownLifecycle = ownScope ? new import_frontend_lifecycle.FrontendLifecycleOwner(registry, `layout ${layoutIdentity}`) : null;
|
|
82
|
+
const layoutScope = ownScope ?? scope;
|
|
83
|
+
if (ownScope) {
|
|
84
|
+
rollback.push(() => import_ioc.ExecutionContext.run(ownScope, () => ownScope.dispose?.()));
|
|
85
|
+
rollback.push(() => ownLifecycle.dispose());
|
|
86
|
+
ownScope.registerFunction?.(import_frontend_lifecycle.FrontendLifecycleOwner, () => ownLifecycle, { loadAs: import_ioc.LoadAs.Scoped });
|
|
87
|
+
await import_ioc.ExecutionContext.run(ownScope, () => options.configurePageScope?.(ownScope, view, {
|
|
88
|
+
kind: "layout",
|
|
89
|
+
layoutIndex: index,
|
|
90
|
+
controller: layoutIdentity
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
registerControllerUnlessBound(layoutScope, LayoutClass);
|
|
94
|
+
const instance = await import_ioc.ExecutionContext.run(
|
|
95
|
+
layoutScope,
|
|
96
|
+
() => layoutScope.get(LayoutClass)
|
|
97
|
+
);
|
|
98
|
+
markController(instance, LayoutClass);
|
|
99
|
+
rollback.push(() => import_ioc.ExecutionContext.run(layoutScope, () => disposeLocalController(instance)));
|
|
100
|
+
layouts.push({
|
|
101
|
+
index,
|
|
102
|
+
path: view.layouts[index],
|
|
103
|
+
controller: layoutIdentity,
|
|
104
|
+
preserve,
|
|
105
|
+
instance,
|
|
106
|
+
scope: layoutScope,
|
|
107
|
+
ownScope,
|
|
108
|
+
ownLifecycle
|
|
96
109
|
});
|
|
97
110
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
() => layoutScope.get(LayoutClass)
|
|
111
|
+
const controller = await import_ioc.ExecutionContext.run(
|
|
112
|
+
scope,
|
|
113
|
+
() => scope.get(ControllerClass)
|
|
102
114
|
);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
markController(controller, ControllerClass);
|
|
116
|
+
rollback.push(() => import_ioc.ExecutionContext.run(scope, () => disposeLocalController(controller)));
|
|
117
|
+
if (typeof controller.initialize === "function") {
|
|
118
|
+
await registry.track(
|
|
119
|
+
Promise.resolve(
|
|
120
|
+
import_ioc.ExecutionContext.run(
|
|
121
|
+
scope,
|
|
122
|
+
() => controller.initialize({ params, query })
|
|
123
|
+
)
|
|
124
|
+
).then(() => void 0),
|
|
125
|
+
{ label: `initialize ${identity}`, kind: "initialize", owner: `page ${identity}` }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const handle = (0, import_frontend_page.createFrontendPage)({
|
|
129
|
+
identity,
|
|
130
|
+
controller,
|
|
131
|
+
scope,
|
|
132
|
+
layouts,
|
|
133
|
+
onClose: async () => {
|
|
134
|
+
await disposePage(identity);
|
|
135
|
+
}
|
|
113
136
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (typeof controller.initialize === "function") {
|
|
121
|
-
await registry.track(
|
|
122
|
-
Promise.resolve(
|
|
123
|
-
import_ioc.ExecutionContext.run(
|
|
124
|
-
scope,
|
|
125
|
-
() => controller.initialize({ params, query })
|
|
126
|
-
)
|
|
127
|
-
).then(() => void 0),
|
|
128
|
-
{ label: `initialize ${identity}`, kind: "initialize", owner: `page ${identity}` }
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
const handle = (0, import_frontend_page.createFrontendPage)({
|
|
132
|
-
identity,
|
|
133
|
-
controller,
|
|
134
|
-
scope,
|
|
135
|
-
layouts,
|
|
136
|
-
onClose: async () => {
|
|
137
|
-
await disposePage(identity);
|
|
137
|
+
return { identity, view, scope, lifecycle, handle, controller, layouts };
|
|
138
|
+
} catch (error) {
|
|
139
|
+
try {
|
|
140
|
+
await (0, import_cleanup.cleanupAll)(rollback.reverse(), "Wood page rollback failed");
|
|
141
|
+
} catch (cleanupError) {
|
|
142
|
+
throw Object.assign(new Error("Wood page construction and cleanup failed"), { cause: error, errors: [error, cleanupError] });
|
|
138
143
|
}
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
141
146
|
}
|
|
142
147
|
async function endPage(page, preservedDepth = 0) {
|
|
143
148
|
page.handle.invalidate();
|
|
144
|
-
|
|
149
|
+
const steps = [
|
|
150
|
+
() => import_ioc.ExecutionContext.run(page.scope, () => disposeLocalController(page.controller))
|
|
151
|
+
];
|
|
145
152
|
for (const layout of page.layouts) {
|
|
146
153
|
if (layout.index < preservedDepth) continue;
|
|
147
|
-
|
|
154
|
+
steps.push(() => import_ioc.ExecutionContext.run(layout.scope, () => disposeLocalController(layout.instance)));
|
|
155
|
+
steps.push(() => layout.ownLifecycle?.dispose());
|
|
156
|
+
if (layout.ownScope) steps.push(() => import_ioc.ExecutionContext.run(layout.ownScope, () => layout.ownScope.dispose?.()));
|
|
148
157
|
}
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
|
|
158
|
+
const carriesPageScope = page.layouts.some((layout) => layout.index < preservedDepth && layout.scope === page.scope);
|
|
159
|
+
if (carriesPageScope) retainedPageScopes.set(page.scope, page.lifecycle);
|
|
160
|
+
else {
|
|
161
|
+
steps.push(() => page.lifecycle.dispose());
|
|
162
|
+
steps.push(() => import_ioc.ExecutionContext.run(page.scope, () => page.scope.dispose?.()));
|
|
152
163
|
}
|
|
153
|
-
for (const
|
|
154
|
-
if (layout.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
164
|
+
for (const [scope, lifecycle] of retainedPageScopes) {
|
|
165
|
+
if (active?.layouts.some((layout) => layout.scope === scope)) continue;
|
|
166
|
+
retainedPageScopes.delete(scope);
|
|
167
|
+
steps.push(() => lifecycle.dispose());
|
|
168
|
+
steps.push(() => import_ioc.ExecutionContext.run(scope, () => scope.dispose?.()));
|
|
159
169
|
}
|
|
170
|
+
await (0, import_cleanup.cleanupAll)(steps, "Wood page cleanup failed");
|
|
160
171
|
}
|
|
161
172
|
async function disposePage(identity) {
|
|
162
173
|
if (!active || active.identity !== identity) return;
|
|
@@ -165,6 +176,7 @@ function createFrontendSurface(options) {
|
|
|
165
176
|
await endPage(ended);
|
|
166
177
|
}
|
|
167
178
|
async function transitionTo(entry) {
|
|
179
|
+
if (disposed) return;
|
|
168
180
|
const view = catalog.resolve(entry.page);
|
|
169
181
|
const previous = active;
|
|
170
182
|
const depth = previous ? (0, import_layout_preservation.computePreservedLayoutDepth)(
|
|
@@ -174,16 +186,23 @@ function createFrontendSurface(options) {
|
|
|
174
186
|
) : 0;
|
|
175
187
|
const carried = previous ? previous.layouts.filter((layout) => layout.index < depth) : [];
|
|
176
188
|
const next = await constructPage(view, entry.params, entry.query, carried);
|
|
189
|
+
if (disposed) {
|
|
190
|
+
await endPage(next, depth);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
177
193
|
active = next;
|
|
178
194
|
if (previous) {
|
|
179
195
|
await endPage(previous, depth);
|
|
180
196
|
}
|
|
181
197
|
}
|
|
182
|
-
navigation.subscribe((entry) => {
|
|
198
|
+
const unsubscribe = navigation.subscribe((entry) => {
|
|
183
199
|
if (!entry || transitioning || disposed) return;
|
|
184
200
|
if (active && entry.page === active.identity) return;
|
|
185
201
|
if (!active) return;
|
|
186
|
-
|
|
202
|
+
const transition = navigationQueue.then(() => transitionTo(entry));
|
|
203
|
+
navigationQueue = transition.catch(() => {
|
|
204
|
+
});
|
|
205
|
+
registry.track(transition, {
|
|
187
206
|
label: `navigate ${active.identity} -> ${entry.page}`,
|
|
188
207
|
kind: "navigation",
|
|
189
208
|
owner: currentOwner()
|
|
@@ -192,6 +211,7 @@ function createFrontendSurface(options) {
|
|
|
192
211
|
return {
|
|
193
212
|
navigation,
|
|
194
213
|
async open(openOptions) {
|
|
214
|
+
if (disposed || transitioning) throw new import_page_catalog.PageCatalogError("Wood frontend is closed or opening a page");
|
|
195
215
|
if (!openOptions || typeof openOptions.page !== "string" || !openOptions.page.length) {
|
|
196
216
|
throw new import_page_catalog.PageCatalogError("frontend.open() requires an explicit { page } identity");
|
|
197
217
|
}
|
|
@@ -206,7 +226,12 @@ function createFrontendSurface(options) {
|
|
|
206
226
|
transitioning = true;
|
|
207
227
|
try {
|
|
208
228
|
navigation.replace((0, import_page_catalog.pageIdentity)(view), params, query);
|
|
209
|
-
|
|
229
|
+
const opened = await constructPage(view, params, query);
|
|
230
|
+
if (disposed) {
|
|
231
|
+
await endPage(opened);
|
|
232
|
+
throw new import_page_catalog.PageCatalogError("Wood frontend was disposed during page construction");
|
|
233
|
+
}
|
|
234
|
+
active = opened;
|
|
210
235
|
} finally {
|
|
211
236
|
transitioning = false;
|
|
212
237
|
}
|
|
@@ -216,7 +241,7 @@ function createFrontendSurface(options) {
|
|
|
216
241
|
return active ? active.handle.page : null;
|
|
217
242
|
},
|
|
218
243
|
async act(action) {
|
|
219
|
-
|
|
244
|
+
void registry.track(
|
|
220
245
|
Promise.resolve().then(() => action()).then(() => void 0),
|
|
221
246
|
{ label: "act()", kind: "tracked", owner: currentOwner() }
|
|
222
247
|
).catch(() => {
|
|
@@ -229,15 +254,16 @@ function createFrontendSurface(options) {
|
|
|
229
254
|
get errors() {
|
|
230
255
|
return registry.errors();
|
|
231
256
|
},
|
|
232
|
-
|
|
233
|
-
if (
|
|
257
|
+
dispose() {
|
|
258
|
+
if (closing) return closing;
|
|
234
259
|
disposed = true;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
260
|
+
unsubscribe();
|
|
261
|
+
const ended = active;
|
|
262
|
+
active = null;
|
|
263
|
+
return closing = (0, import_cleanup.cleanupAll)([
|
|
264
|
+
() => ended ? endPage(ended) : void 0,
|
|
265
|
+
() => application.dispose()
|
|
266
|
+
], "Wood frontend cleanup failed");
|
|
241
267
|
}
|
|
242
268
|
};
|
|
243
269
|
}
|