@hypen-space/core 0.4.956 → 0.5.2
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 +5 -8
- package/dist/app.js +13 -1168
- package/dist/app.js.map +3 -9
- package/dist/chunk-00zt3w6e.js +58 -0
- package/dist/chunk-00zt3w6e.js.map +10 -0
- package/dist/chunk-3zvyth50.js +87 -0
- package/dist/chunk-3zvyth50.js.map +10 -0
- package/dist/chunk-5gtx3pza.js +11 -0
- package/dist/chunk-5gtx3pza.js.map +9 -0
- package/dist/chunk-7222131a.js +191 -0
- package/dist/chunk-7222131a.js.map +10 -0
- package/dist/chunk-avchpwhf.js +200 -0
- package/dist/chunk-avchpwhf.js.map +10 -0
- package/dist/chunk-ec0tgy9a.js +154 -0
- package/dist/chunk-ec0tgy9a.js.map +10 -0
- package/dist/chunk-fnss4dm2.js +479 -0
- package/dist/chunk-fnss4dm2.js.map +10 -0
- package/dist/chunk-g2gz9pgg.js +133 -0
- package/dist/chunk-g2gz9pgg.js.map +10 -0
- package/dist/chunk-g72v6mmf.js +341 -0
- package/dist/chunk-g72v6mmf.js.map +10 -0
- package/dist/chunk-g7g3jxty.js +351 -0
- package/dist/chunk-g7g3jxty.js.map +10 -0
- package/dist/chunk-g830re2c.js +50 -0
- package/dist/chunk-g830re2c.js.map +10 -0
- package/dist/chunk-gtwh1sg7.js +127 -0
- package/dist/chunk-gtwh1sg7.js.map +10 -0
- package/dist/chunk-k12cwfst.js +72 -0
- package/dist/chunk-k12cwfst.js.map +10 -0
- package/dist/chunk-mbqqsz6r.js +128 -0
- package/dist/chunk-mbqqsz6r.js.map +10 -0
- package/dist/chunk-ne632swm.js +209 -0
- package/dist/chunk-ne632swm.js.map +10 -0
- package/dist/chunk-p3pj1bax.js +206 -0
- package/dist/chunk-p3pj1bax.js.map +10 -0
- package/dist/chunk-vshdk2pw.js +56 -0
- package/dist/chunk-vshdk2pw.js.map +10 -0
- package/dist/chunk-wp2ebn2m.js +51 -0
- package/dist/chunk-wp2ebn2m.js.map +10 -0
- package/dist/chunk-x4qv4jm9.js +88 -0
- package/dist/chunk-x4qv4jm9.js.map +10 -0
- package/dist/chunk-ycxp4drd.js +211 -0
- package/dist/chunk-ycxp4drd.js.map +10 -0
- package/dist/components/builtin.js +14 -1246
- package/dist/components/builtin.js.map +3 -10
- package/dist/context.js +7 -362
- package/dist/context.js.map +3 -6
- package/dist/datasource.js +5 -76
- package/dist/datasource.js.map +3 -4
- package/dist/disposable.js +20 -367
- package/dist/disposable.js.map +3 -5
- package/dist/engine-base.d.ts +38 -2
- package/dist/engine-base.js +45 -412
- package/dist/engine-base.js.map +4 -6
- package/dist/events.js +7 -285
- package/dist/events.js.map +3 -5
- package/dist/hypen.js +8 -71
- package/dist/hypen.js.map +3 -4
- package/dist/index.browser.js +58 -2152
- package/dist/index.browser.js.map +3 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +132 -3058
- package/dist/index.js.map +4 -22
- package/dist/logger.js +16 -220
- package/dist/logger.js.map +3 -4
- package/dist/managed-router.js +12 -1367
- package/dist/managed-router.js.map +3 -10
- package/dist/portable.js +6 -70
- package/dist/portable.js.map +3 -4
- package/dist/remote/client.js +9 -1010
- package/dist/remote/client.js.map +3 -8
- package/dist/remote/index.d.ts +2 -0
- package/dist/remote/index.js +511 -1066
- package/dist/remote/index.js.map +4 -9
- package/dist/remote/remote-session.d.ts +245 -0
- package/dist/remote/session.js +5 -147
- package/dist/remote/session.js.map +3 -4
- package/dist/renderer.js +7 -271
- package/dist/renderer.js.map +3 -5
- package/dist/resolver.js +5 -484
- package/dist/resolver.js.map +3 -5
- package/dist/result.js +25 -211
- package/dist/result.js.map +3 -4
- package/dist/retry.js +10 -336
- package/dist/retry.js.map +3 -5
- package/dist/router.js +9 -807
- package/dist/router.js.map +3 -8
- package/dist/state.js +10 -274
- package/dist/state.js.map +3 -5
- package/dist/validate.d.ts +19 -0
- package/package.json +13 -1
- package/src/engine-base.ts +87 -4
- package/src/index.ts +5 -0
- package/src/remote/index.ts +16 -0
- package/src/remote/remote-session.ts +998 -0
- package/src/validate.ts +37 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
frameworkLoggers
|
|
3
|
+
} from "./chunk-avchpwhf.js";
|
|
4
|
+
|
|
5
|
+
// src/events.ts
|
|
6
|
+
var log = frameworkLoggers.events;
|
|
7
|
+
|
|
8
|
+
class TypedEventEmitter {
|
|
9
|
+
eventBus = new Map;
|
|
10
|
+
emit(event, payload) {
|
|
11
|
+
const handlers = this.eventBus.get(event);
|
|
12
|
+
if (!handlers || handlers.size === 0) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
handlers.forEach((handler) => {
|
|
16
|
+
try {
|
|
17
|
+
handler(payload);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
log.error(`Error in event handler for "${String(event)}":`, error);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
on(event, handler) {
|
|
24
|
+
if (!this.eventBus.has(event)) {
|
|
25
|
+
this.eventBus.set(event, new Set);
|
|
26
|
+
}
|
|
27
|
+
const handlers = this.eventBus.get(event);
|
|
28
|
+
handlers.add(handler);
|
|
29
|
+
return () => {
|
|
30
|
+
handlers.delete(handler);
|
|
31
|
+
if (handlers.size === 0) {
|
|
32
|
+
this.eventBus.delete(event);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
once(event, handler) {
|
|
37
|
+
const wrappedHandler = (payload) => {
|
|
38
|
+
handler(payload);
|
|
39
|
+
unsubscribe();
|
|
40
|
+
};
|
|
41
|
+
const unsubscribe = this.on(event, wrappedHandler);
|
|
42
|
+
return unsubscribe;
|
|
43
|
+
}
|
|
44
|
+
off(event, handler) {
|
|
45
|
+
const handlers = this.eventBus.get(event);
|
|
46
|
+
if (handlers) {
|
|
47
|
+
handlers.delete(handler);
|
|
48
|
+
if (handlers.size === 0) {
|
|
49
|
+
this.eventBus.delete(event);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
removeAllListeners(event) {
|
|
54
|
+
this.eventBus.delete(event);
|
|
55
|
+
}
|
|
56
|
+
clearAll() {
|
|
57
|
+
this.eventBus.clear();
|
|
58
|
+
}
|
|
59
|
+
listenerCount(event) {
|
|
60
|
+
return this.eventBus.get(event)?.size ?? 0;
|
|
61
|
+
}
|
|
62
|
+
eventNames() {
|
|
63
|
+
return Array.from(this.eventBus.keys());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function createEventEmitter() {
|
|
67
|
+
return new TypedEventEmitter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { TypedEventEmitter, createEventEmitter };
|
|
71
|
+
|
|
72
|
+
//# debugId=4DB9F634486F2C2C64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/events.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Type-safe Event System\n *\n * Provides strongly-typed event emission and subscription with autocomplete support.\n */\n\nimport { frameworkLoggers } from \"./logger.js\";\n\nconst log = frameworkLoggers.events;\n\nexport type EventHandler<T = unknown> = (payload: T) => void;\n\n/**\n * Type-safe event emitter with generics\n */\nexport class TypedEventEmitter<TEvents extends Record<string, unknown> = Record<string, unknown>> {\n private eventBus = new Map<keyof TEvents, Set<EventHandler<any>>>();\n\n /**\n * Emit an event with type-safe payload\n */\n emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {\n const handlers = this.eventBus.get(event);\n if (!handlers || handlers.size === 0) {\n return;\n }\n\n handlers.forEach((handler) => {\n try {\n handler(payload);\n } catch (error) {\n log.error(`Error in event handler for \"${String(event)}\":`, error);\n }\n });\n }\n\n /**\n * Subscribe to an event with type-safe payload\n * Returns an unsubscribe function\n */\n on<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void {\n if (!this.eventBus.has(event)) {\n this.eventBus.set(event, new Set());\n }\n\n const handlers = this.eventBus.get(event)!;\n handlers.add(handler);\n\n // Return unsubscribe function\n return () => {\n handlers.delete(handler);\n if (handlers.size === 0) {\n this.eventBus.delete(event);\n }\n };\n }\n\n /**\n * Subscribe to an event once (auto-unsubscribe after first emit)\n */\n once<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): () => void {\n const wrappedHandler = (payload: TEvents[K]) => {\n handler(payload);\n unsubscribe();\n };\n\n const unsubscribe = this.on(event, wrappedHandler);\n return unsubscribe;\n }\n\n /**\n * Unsubscribe a specific handler from an event\n */\n off<K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): void {\n const handlers = this.eventBus.get(event);\n if (handlers) {\n handlers.delete(handler);\n if (handlers.size === 0) {\n this.eventBus.delete(event);\n }\n }\n }\n\n /**\n * Remove all listeners for a specific event\n */\n removeAllListeners<K extends keyof TEvents>(event: K): void {\n this.eventBus.delete(event);\n }\n\n /**\n * Remove all listeners for all events\n */\n clearAll(): void {\n this.eventBus.clear();\n }\n\n /**\n * Get the number of listeners for an event\n */\n listenerCount<K extends keyof TEvents>(event: K): number {\n return this.eventBus.get(event)?.size ?? 0;\n }\n\n /**\n * Get all registered event names\n */\n eventNames(): Array<keyof TEvents> {\n return Array.from(this.eventBus.keys());\n }\n}\n\n/**\n * Default events for Hypen framework (can be extended by users)\n */\nexport type HypenFrameworkEvents = {\n 'module:created': { moduleId: string };\n 'module:destroyed': { moduleId: string };\n 'route:changed': { from: string | null; to: string };\n 'state:updated': { moduleId: string; paths: string[] };\n 'action:dispatched': { moduleId: string; actionName: string; payload?: unknown };\n 'error': { message: string; error?: Error; context?: string };\n};\n\n/**\n * Create a typed event emitter with custom event map\n */\nexport function createEventEmitter<TEvents extends Record<string, unknown> = HypenFrameworkEvents>(): TypedEventEmitter<TEvents> {\n return new TypedEventEmitter<TEvents>();\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": ";;;;;AAQA,IAAM,MAAM,iBAAiB;AAAA;AAOtB,MAAM,kBAAqF;AAAA,EACxF,WAAW,IAAI;AAAA,EAKvB,IAA6B,CAAC,OAAU,SAA2B;AAAA,IACjE,MAAM,WAAW,KAAK,SAAS,IAAI,KAAK;AAAA,IACxC,IAAI,CAAC,YAAY,SAAS,SAAS,GAAG;AAAA,MACpC;AAAA,IACF;AAAA,IAEA,SAAS,QAAQ,CAAC,YAAY;AAAA,MAC5B,IAAI;AAAA,QACF,QAAQ,OAAO;AAAA,QACf,OAAO,OAAO;AAAA,QACd,IAAI,MAAM,+BAA+B,OAAO,KAAK,OAAO,KAAK;AAAA;AAAA,KAEpE;AAAA;AAAA,EAOH,EAA2B,CAAC,OAAU,SAA+C;AAAA,IACnF,IAAI,CAAC,KAAK,SAAS,IAAI,KAAK,GAAG;AAAA,MAC7B,KAAK,SAAS,IAAI,OAAO,IAAI,GAAK;AAAA,IACpC;AAAA,IAEA,MAAM,WAAW,KAAK,SAAS,IAAI,KAAK;AAAA,IACxC,SAAS,IAAI,OAAO;AAAA,IAGpB,OAAO,MAAM;AAAA,MACX,SAAS,OAAO,OAAO;AAAA,MACvB,IAAI,SAAS,SAAS,GAAG;AAAA,QACvB,KAAK,SAAS,OAAO,KAAK;AAAA,MAC5B;AAAA;AAAA;AAAA,EAOJ,IAA6B,CAAC,OAAU,SAA+C;AAAA,IACrF,MAAM,iBAAiB,CAAC,YAAwB;AAAA,MAC9C,QAAQ,OAAO;AAAA,MACf,YAAY;AAAA;AAAA,IAGd,MAAM,cAAc,KAAK,GAAG,OAAO,cAAc;AAAA,IACjD,OAAO;AAAA;AAAA,EAMT,GAA4B,CAAC,OAAU,SAAyC;AAAA,IAC9E,MAAM,WAAW,KAAK,SAAS,IAAI,KAAK;AAAA,IACxC,IAAI,UAAU;AAAA,MACZ,SAAS,OAAO,OAAO;AAAA,MACvB,IAAI,SAAS,SAAS,GAAG;AAAA,QACvB,KAAK,SAAS,OAAO,KAAK;AAAA,MAC5B;AAAA,IACF;AAAA;AAAA,EAMF,kBAA2C,CAAC,OAAgB;AAAA,IAC1D,KAAK,SAAS,OAAO,KAAK;AAAA;AAAA,EAM5B,QAAQ,GAAS;AAAA,IACf,KAAK,SAAS,MAAM;AAAA;AAAA,EAMtB,aAAsC,CAAC,OAAkB;AAAA,IACvD,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,QAAQ;AAAA;AAAA,EAM3C,UAAU,GAAyB;AAAA,IACjC,OAAO,MAAM,KAAK,KAAK,SAAS,KAAK,CAAC;AAAA;AAE1C;AAiBO,SAAS,kBAAkF,GAA+B;AAAA,EAC/H,OAAO,IAAI;AAAA;",
|
|
8
|
+
"debugId": "4DB9F634486F2C2C64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-5gtx3pza.js";
|
|
4
|
+
|
|
5
|
+
// src/resolver.ts
|
|
6
|
+
class ComponentResolver {
|
|
7
|
+
cache = new Map;
|
|
8
|
+
options;
|
|
9
|
+
moduleRegistry;
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
baseDir: options.baseDir || process.cwd(),
|
|
13
|
+
cache: options.cache ?? true,
|
|
14
|
+
customFetch: options.customFetch || this.defaultFetch.bind(this),
|
|
15
|
+
moduleRegistry: options.moduleRegistry
|
|
16
|
+
};
|
|
17
|
+
this.moduleRegistry = options.moduleRegistry;
|
|
18
|
+
}
|
|
19
|
+
async resolve(importStmt) {
|
|
20
|
+
if (this.moduleRegistry) {
|
|
21
|
+
const names = importStmt.clause.type === "named" ? importStmt.clause.names : [importStmt.clause.name];
|
|
22
|
+
const allFound = names.every((name) => this.moduleRegistry.has(name));
|
|
23
|
+
if (allFound) {
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const name of names) {
|
|
26
|
+
const def = this.moduleRegistry.get(name);
|
|
27
|
+
result[name] = {
|
|
28
|
+
module: def,
|
|
29
|
+
template: def?.template || ""
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const sourcePath = this.getSourcePath(importStmt.source);
|
|
36
|
+
if (this.options.cache && this.cache.has(sourcePath)) {
|
|
37
|
+
const cached = this.cache.get(sourcePath);
|
|
38
|
+
return this.extractComponents(importStmt.clause, cached);
|
|
39
|
+
}
|
|
40
|
+
let component;
|
|
41
|
+
if (importStmt.source.type === "local") {
|
|
42
|
+
component = await this.resolveLocal(importStmt.source.path);
|
|
43
|
+
} else {
|
|
44
|
+
component = await this.resolveUrl(importStmt.source.url);
|
|
45
|
+
}
|
|
46
|
+
if (this.options.cache) {
|
|
47
|
+
this.cache.set(sourcePath, component);
|
|
48
|
+
}
|
|
49
|
+
return this.extractComponents(importStmt.clause, component);
|
|
50
|
+
}
|
|
51
|
+
async resolveLocal(path) {
|
|
52
|
+
const { resolve, join } = await import("./chunk-g72v6mmf.js");
|
|
53
|
+
const { readFile } = await import("fs/promises");
|
|
54
|
+
const basePath = resolve(this.options.baseDir, path);
|
|
55
|
+
const hypenPath = basePath.endsWith(".hypen") ? basePath : `${basePath}.hypen`;
|
|
56
|
+
const template = await readFile(hypenPath, "utf-8");
|
|
57
|
+
const modulePath = hypenPath.replace(/\.hypen$/, ".ts");
|
|
58
|
+
let module = {};
|
|
59
|
+
try {
|
|
60
|
+
module = await import(modulePath);
|
|
61
|
+
} catch {}
|
|
62
|
+
return { module, template };
|
|
63
|
+
}
|
|
64
|
+
async resolveUrl(url) {
|
|
65
|
+
try {
|
|
66
|
+
const response = await this.options.customFetch(url);
|
|
67
|
+
const data = JSON.parse(response);
|
|
68
|
+
if (!data.module || !data.template) {
|
|
69
|
+
throw new Error(`Invalid component format from ${url}. Expected { module, template }`);
|
|
70
|
+
}
|
|
71
|
+
return data;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(`Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async defaultFetch(url) {
|
|
77
|
+
const response = await fetch(url);
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
80
|
+
}
|
|
81
|
+
return response.text();
|
|
82
|
+
}
|
|
83
|
+
extractComponents(clause, component) {
|
|
84
|
+
if (clause.type === "default") {
|
|
85
|
+
return {
|
|
86
|
+
[clause.name]: component
|
|
87
|
+
};
|
|
88
|
+
} else {
|
|
89
|
+
const result = {};
|
|
90
|
+
for (const name of clause.names) {
|
|
91
|
+
result[name] = component;
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
getSourcePath(source) {
|
|
97
|
+
return source.type === "local" ? source.path : source.url;
|
|
98
|
+
}
|
|
99
|
+
clearCache() {
|
|
100
|
+
this.cache.clear();
|
|
101
|
+
}
|
|
102
|
+
static parseImports(text) {
|
|
103
|
+
const imports = [];
|
|
104
|
+
const importRegex = /import\s+(?:(\{[^}]*\})|(\w+))\s+from\s+["']([^"']+)["']/g;
|
|
105
|
+
let match;
|
|
106
|
+
while ((match = importRegex.exec(text)) !== null) {
|
|
107
|
+
const [, namedImports, defaultImport, source] = match;
|
|
108
|
+
if (!source)
|
|
109
|
+
continue;
|
|
110
|
+
let clause;
|
|
111
|
+
if (namedImports) {
|
|
112
|
+
const names = namedImports.slice(1, -1).split(",").map((n) => n.trim()).filter((n) => n.length > 0);
|
|
113
|
+
clause = { type: "named", names };
|
|
114
|
+
} else if (defaultImport) {
|
|
115
|
+
clause = { type: "default", name: defaultImport };
|
|
116
|
+
} else {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const sourceObj = source.startsWith("http://") || source.startsWith("https://") ? { type: "url", url: source } : { type: "local", path: source };
|
|
120
|
+
imports.push({ clause, source: sourceObj });
|
|
121
|
+
}
|
|
122
|
+
return imports;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { ComponentResolver };
|
|
127
|
+
|
|
128
|
+
//# debugId=1E204F3EFBD0545664756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/resolver.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Component Resolver\n * Resolves component imports from local paths or web URLs\n */\n\nexport interface ImportStatement {\n clause: ImportClause;\n source: ImportSource;\n}\n\nexport type ImportClause =\n | { type: \"named\"; names: string[] }\n | { type: \"default\"; name: string };\n\nexport type ImportSource =\n | { type: \"local\"; path: string }\n | { type: \"url\"; url: string };\n\nexport interface ComponentDefinition {\n module: any;\n template: string;\n}\n\nexport interface ResolverOptions {\n /**\n * Base directory for resolving relative local paths\n * Defaults to current working directory\n */\n baseDir?: string;\n\n /**\n * Cache resolved components to avoid re-fetching\n * Defaults to true\n */\n cache?: boolean;\n\n /**\n * Custom fetch function for URL imports\n * Useful for adding authentication, custom headers, etc.\n */\n customFetch?: (url: string) => Promise<string>;\n\n /**\n * Module registry for looking up pre-registered module definitions.\n * When set, the resolver will check the registry before doing file I/O.\n */\n moduleRegistry?: { has: (name: string) => boolean; get: (name: string) => any };\n}\n\n/**\n * Component Resolver\n * Resolves and loads components from local files or remote URLs\n */\nexport class ComponentResolver {\n private cache = new Map<string, ComponentDefinition>();\n private options: Required<Pick<ResolverOptions, 'baseDir' | 'cache' | 'customFetch'>> & Pick<ResolverOptions, 'moduleRegistry'>;\n\n private moduleRegistry?: { has: (name: string) => boolean; get: (name: string) => any };\n\n constructor(options: ResolverOptions = {}) {\n this.options = {\n baseDir: options.baseDir || process.cwd(),\n cache: options.cache ?? true,\n customFetch: options.customFetch || this.defaultFetch.bind(this),\n moduleRegistry: options.moduleRegistry,\n };\n this.moduleRegistry = options.moduleRegistry;\n }\n\n /**\n * Resolve a component from an import statement.\n * Checks the module registry first (if available), then falls back to file I/O.\n */\n async resolve(\n importStmt: ImportStatement\n ): Promise<Record<string, ComponentDefinition>> {\n // Check module registry first — if a component is pre-registered,\n // use its template directly (it will be mounted as a full module)\n if (this.moduleRegistry) {\n const names = importStmt.clause.type === \"named\"\n ? importStmt.clause.names\n : [importStmt.clause.name];\n\n const allFound = names.every(name => this.moduleRegistry!.has(name));\n if (allFound) {\n const result: Record<string, ComponentDefinition> = {};\n for (const name of names) {\n const def = this.moduleRegistry!.get(name);\n result[name] = {\n module: def,\n template: def?.template || \"\",\n };\n }\n return result;\n }\n }\n\n const sourcePath = this.getSourcePath(importStmt.source);\n\n // Check cache first\n if (this.options.cache && this.cache.has(sourcePath)) {\n const cached = this.cache.get(sourcePath)!;\n return this.extractComponents(importStmt.clause, cached);\n }\n\n // Load the component\n let component: ComponentDefinition;\n if (importStmt.source.type === \"local\") {\n component = await this.resolveLocal(importStmt.source.path);\n } else {\n component = await this.resolveUrl(importStmt.source.url);\n }\n\n // Cache it\n if (this.options.cache) {\n this.cache.set(sourcePath, component);\n }\n\n return this.extractComponents(importStmt.clause, component);\n }\n\n /**\n * Resolve a component from a local file path\n */\n private async resolveLocal(path: string): Promise<ComponentDefinition> {\n const { resolve, join } = await import(\"path\");\n const { readFile } = await import(\"fs/promises\");\n\n const basePath = resolve(this.options.baseDir, path);\n\n // Try .hypen extension\n const hypenPath = basePath.endsWith(\".hypen\")\n ? basePath\n : `${basePath}.hypen`;\n\n const template = await readFile(hypenPath, \"utf-8\");\n\n // Try loading sibling .ts module (stateless components have no module)\n const modulePath = hypenPath.replace(/\\.hypen$/, \".ts\");\n let module = {};\n try {\n module = await import(modulePath);\n } catch {\n // Stateless component — no module file\n }\n\n return { module, template };\n }\n\n /**\n * Resolve a component from a URL\n */\n private async resolveUrl(url: string): Promise<ComponentDefinition> {\n try {\n const response = await this.options.customFetch(url);\n const data = JSON.parse(response);\n\n // Expected format: { module: {...}, template: \"...\" }\n if (!data.module || !data.template) {\n throw new Error(\n `Invalid component format from ${url}. Expected { module, template }`\n );\n }\n\n return data;\n } catch (error) {\n throw new Error(\n `Failed to resolve component from ${url}: ${error instanceof Error ? error.message : String(error)}`\n );\n }\n }\n\n /**\n * Default fetch implementation\n */\n private async defaultFetch(url: string): Promise<string> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.text();\n }\n\n /**\n * Extract the requested components based on the import clause\n */\n private extractComponents(\n clause: ImportClause,\n component: ComponentDefinition\n ): Record<string, ComponentDefinition> {\n if (clause.type === \"default\") {\n return {\n [clause.name]: component,\n };\n } else {\n // Named imports - for now, we only support single exports\n // In the future, we could support exporting multiple components\n // from a single file\n const result: Record<string, ComponentDefinition> = {};\n for (const name of clause.names) {\n result[name] = component;\n }\n return result;\n }\n }\n\n /**\n * Get the source path as a string (for caching)\n */\n private getSourcePath(source: ImportSource): string {\n return source.type === \"local\" ? source.path : source.url;\n }\n\n /**\n * Clear the component cache\n */\n clearCache(): void {\n this.cache.clear();\n }\n\n /**\n * Parse import statements from Hypen DSL text\n * This is a simple parser that extracts import statements\n */\n static parseImports(text: string): ImportStatement[] {\n const imports: ImportStatement[] = [];\n const importRegex =\n /import\\s+(?:(\\{[^}]*\\})|(\\w+))\\s+from\\s+[\"']([^\"']+)[\"']/g;\n\n let match;\n while ((match = importRegex.exec(text)) !== null) {\n const [, namedImports, defaultImport, source] = match;\n\n if (!source) continue;\n\n let clause: ImportClause;\n if (namedImports) {\n // Named imports: { Button, Card }\n const names = namedImports\n .slice(1, -1) // Remove { and }\n .split(\",\")\n .map((n) => n.trim())\n .filter((n) => n.length > 0);\n clause = { type: \"named\", names };\n } else if (defaultImport) {\n // Default import: HomePage\n clause = { type: \"default\", name: defaultImport };\n } else {\n continue;\n }\n\n // Determine if source is URL or local path\n const sourceObj: ImportSource = source.startsWith(\"http://\") ||\n source.startsWith(\"https://\")\n ? { type: \"url\", url: source }\n : { type: \"local\", path: source };\n\n imports.push({ clause, source: sourceObj });\n }\n\n return imports;\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "1E204F3EFBD0545664756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HypenModuleInstance
|
|
3
|
+
} from "./chunk-fnss4dm2.js";
|
|
4
|
+
import {
|
|
5
|
+
createLogger
|
|
6
|
+
} from "./chunk-avchpwhf.js";
|
|
7
|
+
|
|
8
|
+
// src/managed-router.ts
|
|
9
|
+
var log = createLogger("ManagedRouter");
|
|
10
|
+
var DEFAULT_MAX_PERSISTED_MODULES = 10;
|
|
11
|
+
|
|
12
|
+
class ManagedRouter {
|
|
13
|
+
router;
|
|
14
|
+
engine;
|
|
15
|
+
registry;
|
|
16
|
+
globalContext;
|
|
17
|
+
routes = [];
|
|
18
|
+
activeModule = null;
|
|
19
|
+
activeRoute = null;
|
|
20
|
+
unsubscribe = null;
|
|
21
|
+
persistedModules = new Map;
|
|
22
|
+
maxPersistedModules;
|
|
23
|
+
navPromise = Promise.resolve();
|
|
24
|
+
constructor(router, engine, registry, globalContext, options = {}) {
|
|
25
|
+
this.router = router;
|
|
26
|
+
this.engine = engine;
|
|
27
|
+
this.registry = registry;
|
|
28
|
+
this.globalContext = globalContext;
|
|
29
|
+
const cap = options.maxPersistedModules ?? DEFAULT_MAX_PERSISTED_MODULES;
|
|
30
|
+
this.maxPersistedModules = cap > 0 ? cap : DEFAULT_MAX_PERSISTED_MODULES;
|
|
31
|
+
}
|
|
32
|
+
addRoute(route) {
|
|
33
|
+
this.routes.push(route);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
this.unsubscribe = this.router.onNavigate((routeState) => {
|
|
38
|
+
this.handleRouteChange(routeState);
|
|
39
|
+
});
|
|
40
|
+
this.installRouterActions();
|
|
41
|
+
}
|
|
42
|
+
installRouterActions() {
|
|
43
|
+
const router = this.router;
|
|
44
|
+
const defer = (fn) => queueMicrotask(fn);
|
|
45
|
+
const readTo = (action) => {
|
|
46
|
+
const payload = action?.payload;
|
|
47
|
+
const to = payload?.to;
|
|
48
|
+
return typeof to === "string" && to.length > 0 ? to : null;
|
|
49
|
+
};
|
|
50
|
+
this.engine.onAction("router.push", (action) => {
|
|
51
|
+
const to = readTo(action);
|
|
52
|
+
if (to)
|
|
53
|
+
defer(() => router.push(to));
|
|
54
|
+
});
|
|
55
|
+
this.engine.onAction("router.replace", (action) => {
|
|
56
|
+
const to = readTo(action);
|
|
57
|
+
if (to)
|
|
58
|
+
defer(() => router.replace(to));
|
|
59
|
+
});
|
|
60
|
+
this.engine.onAction("router.back", () => {
|
|
61
|
+
defer(() => router.back());
|
|
62
|
+
});
|
|
63
|
+
this.engine.onAction("router.forward", () => {
|
|
64
|
+
defer(() => router.forward());
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async stop() {
|
|
68
|
+
if (this.unsubscribe) {
|
|
69
|
+
this.unsubscribe();
|
|
70
|
+
this.unsubscribe = null;
|
|
71
|
+
}
|
|
72
|
+
const cleanup = this.navPromise.catch(() => {}).then(async () => {
|
|
73
|
+
await this.unmountActive();
|
|
74
|
+
for (const [moduleId, instance] of this.persistedModules) {
|
|
75
|
+
log.debug(`Destroying persisted module on stop: ${moduleId}`);
|
|
76
|
+
try {
|
|
77
|
+
await instance.destroy();
|
|
78
|
+
} catch (e) {
|
|
79
|
+
log.error(`Error destroying persisted module ${moduleId}:`, e);
|
|
80
|
+
}
|
|
81
|
+
this.globalContext.unregisterModule(moduleId);
|
|
82
|
+
}
|
|
83
|
+
this.persistedModules.clear();
|
|
84
|
+
});
|
|
85
|
+
this.navPromise = cleanup;
|
|
86
|
+
await cleanup;
|
|
87
|
+
this.router.dispose();
|
|
88
|
+
}
|
|
89
|
+
async waitForNavigation() {
|
|
90
|
+
try {
|
|
91
|
+
await this.navPromise;
|
|
92
|
+
} catch {}
|
|
93
|
+
}
|
|
94
|
+
getActiveModule() {
|
|
95
|
+
return this.activeModule;
|
|
96
|
+
}
|
|
97
|
+
getActiveRoute() {
|
|
98
|
+
return this.activeRoute;
|
|
99
|
+
}
|
|
100
|
+
handleRouteChange(routeState) {
|
|
101
|
+
this.navPromise = this.navPromise.catch((e) => {
|
|
102
|
+
log.error("Previous navigation failed:", e);
|
|
103
|
+
}).then(() => this.processRouteChange(routeState));
|
|
104
|
+
}
|
|
105
|
+
async processRouteChange(routeState) {
|
|
106
|
+
const path = routeState.currentPath;
|
|
107
|
+
const matched = this.matchRoute(path);
|
|
108
|
+
if (!matched) {
|
|
109
|
+
log.debug(`No route matched for path: ${path}`);
|
|
110
|
+
await this.unmountActive();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (this.activeRoute && this.activeRoute.path === matched.path) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await this.unmountActive();
|
|
117
|
+
await this.mount(matched);
|
|
118
|
+
}
|
|
119
|
+
matchRoute(path) {
|
|
120
|
+
for (const route of this.routes) {
|
|
121
|
+
if (this.router.matchPath(route.path, path) !== null) {
|
|
122
|
+
return route;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
async mount(route) {
|
|
128
|
+
const definition = route.module || this.registry.get(route.component);
|
|
129
|
+
if (!definition) {
|
|
130
|
+
log.debug(`No module definition found for component: ${route.component}`);
|
|
131
|
+
this.activeRoute = route;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const namedDef = {
|
|
135
|
+
...definition,
|
|
136
|
+
name: definition.name || route.component.toLowerCase()
|
|
137
|
+
};
|
|
138
|
+
const moduleId = namedDef.name.toLowerCase();
|
|
139
|
+
const persisted = this.persistedModules.get(moduleId);
|
|
140
|
+
if (persisted) {
|
|
141
|
+
log.debug(`Restoring persisted module: ${moduleId} for route: ${route.path}`);
|
|
142
|
+
this.activeModule = persisted;
|
|
143
|
+
this.activeRoute = route;
|
|
144
|
+
this.persistedModules.delete(moduleId);
|
|
145
|
+
await persisted.activate();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
log.debug(`Mounting module: ${namedDef.name} for route: ${route.path}`);
|
|
149
|
+
const instance = new HypenModuleInstance(this.engine, namedDef, this.router, this.globalContext);
|
|
150
|
+
this.globalContext.registerModule(moduleId, instance);
|
|
151
|
+
this.activeModule = instance;
|
|
152
|
+
this.activeRoute = route;
|
|
153
|
+
await instance.activate();
|
|
154
|
+
}
|
|
155
|
+
async unmountActive() {
|
|
156
|
+
if (!this.activeModule || !this.activeRoute) {
|
|
157
|
+
this.activeModule = null;
|
|
158
|
+
this.activeRoute = null;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const active = this.activeModule;
|
|
162
|
+
const route = this.activeRoute;
|
|
163
|
+
const definition = route.module || this.registry.get(route.component);
|
|
164
|
+
const moduleId = (definition?.name || route.component).toLowerCase();
|
|
165
|
+
const persist = definition != null && definition.persist !== false;
|
|
166
|
+
this.activeModule = null;
|
|
167
|
+
this.activeRoute = null;
|
|
168
|
+
try {
|
|
169
|
+
await active.deactivate();
|
|
170
|
+
} catch (e) {
|
|
171
|
+
log.error(`Error deactivating module ${moduleId}:`, e);
|
|
172
|
+
}
|
|
173
|
+
if (persist) {
|
|
174
|
+
log.debug(`Persisting module: ${moduleId}`);
|
|
175
|
+
this.persistedModules.delete(moduleId);
|
|
176
|
+
this.persistedModules.set(moduleId, active);
|
|
177
|
+
await this.evictPersistedOverflow();
|
|
178
|
+
} else {
|
|
179
|
+
log.debug(`Unmounting module: ${moduleId}`);
|
|
180
|
+
try {
|
|
181
|
+
await active.destroy();
|
|
182
|
+
} catch (e) {
|
|
183
|
+
log.error(`Error destroying module ${moduleId}:`, e);
|
|
184
|
+
}
|
|
185
|
+
this.globalContext.unregisterModule(moduleId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async evictPersistedOverflow() {
|
|
189
|
+
while (this.persistedModules.size > this.maxPersistedModules) {
|
|
190
|
+
const oldest = this.persistedModules.keys().next();
|
|
191
|
+
if (oldest.done)
|
|
192
|
+
break;
|
|
193
|
+
const oldestId = oldest.value;
|
|
194
|
+
const evicted = this.persistedModules.get(oldestId);
|
|
195
|
+
this.persistedModules.delete(oldestId);
|
|
196
|
+
log.debug(`Evicting persisted module (LRU): ${oldestId}`);
|
|
197
|
+
try {
|
|
198
|
+
await evicted.destroy();
|
|
199
|
+
} catch (e) {
|
|
200
|
+
log.error(`Error destroying evicted module ${oldestId}:`, e);
|
|
201
|
+
}
|
|
202
|
+
this.globalContext.unregisterModule(oldestId);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export { DEFAULT_MAX_PERSISTED_MODULES, ManagedRouter };
|
|
208
|
+
|
|
209
|
+
//# debugId=2744EFD6F71F4CB364756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/managed-router.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Managed Router — orchestrates module mount/unmount on route changes.\n *\n * When the router navigates to a route:\n * 1. Deactivates and unmounts the previous module (either persisting it for\n * later reuse or destroying it).\n * 2. Mounts the new module (creating it fresh or restoring from cache) and\n * activates it.\n *\n * Module names are used as state prefixes (lowercased) for isolation.\n *\n * ## Persistence (default: on for module-backed routes)\n *\n * By default, any route whose `component` resolves to a registered module\n * definition (or provides one inline via `route.module`) has its module\n * instance **persisted** across navigations. This preserves module state\n * (e.g. fetched data, form inputs, scroll position in state) so that\n * navigating away and back doesn't re-trigger the initial \"loading\" state\n * that usually lives in `onCreated`.\n *\n * Opt out by setting `persist: false` on the module definition. Routes\n * without a module definition are unchanged — they have nothing to persist.\n *\n * ## Persistence cache bound (LRU)\n *\n * The persisted-module cache is an insertion-ordered LRU with a default\n * cap of `DEFAULT_MAX_PERSISTED_MODULES` (= 10). Persisting a module\n * past the cap evicts the least-recently-persisted entry and tears it\n * down via `instance.destroy()` + `unregisterModule()` — the same path a\n * `persist: false` module takes on unmount. Restoring from the cache\n * counts as a touch (the entry is removed on mount and re-inserted on\n * next unmount, placing it at the MRU end).\n *\n * The cap matches the engine's `DEFAULT_ROUTER_CACHE_SIZE` for the\n * Router IR subtree cache, so SDK-level module retention and\n * engine-level DOM-subtree retention stay in lockstep by default.\n * Override via the `maxPersistedModules` constructor option. Pass\n * `Infinity` to disable eviction (only advisable for bounded route\n * sets — otherwise a long session will leak).\n *\n * ## Lifecycle on navigation\n *\n * First visit to a route:\n * `construct → onCreated → onActivated`\n * Navigate away:\n * `onDeactivated` (then either persist in cache OR `onDestroyed`)\n * Revisit (persisted):\n * `onActivated` (only — `onCreated` does not re-run)\n *\n * Use `onActivated` for per-navigation side effects like refreshing data;\n * use `onCreated` for one-time module setup.\n */\n\nimport type { IEngine, HypenModuleDefinition } from \"./app.js\";\nimport { HypenModuleInstance, HypenApp } from \"./app.js\";\nimport type { HypenRouter, RouteState } from \"./router.js\";\nimport type { HypenGlobalContext } from \"./context.js\";\nimport { createLogger } from \"./logger.js\";\n\nconst log = createLogger(\"ManagedRouter\");\n\n/**\n * Default upper bound on `persistedModules`. Mirrors\n * `DEFAULT_ROUTER_CACHE_SIZE` in the engine's Router IR node so the SDK\n * module cache and the engine subtree cache evict in lockstep.\n */\nexport const DEFAULT_MAX_PERSISTED_MODULES = 10;\n\nexport interface RouteDefinition {\n /** Route path pattern (e.g., \"/\", \"/profile/:id\") */\n path: string;\n /** Component name — used to look up in app registry */\n component: string;\n /** Inline module definition (alternative to registry lookup) */\n module?: HypenModuleDefinition;\n}\n\nexport interface ManagedRouterOptions {\n /**\n * Maximum number of persisted module instances retained across\n * navigations. When persistence would push the cache past this cap,\n * the least-recently-persisted entry is destroyed. Defaults to\n * `DEFAULT_MAX_PERSISTED_MODULES`. Pass `Infinity` to disable\n * eviction.\n */\n maxPersistedModules?: number;\n}\n\nexport class ManagedRouter {\n private router: HypenRouter;\n private engine: IEngine;\n private registry: HypenApp;\n private globalContext: HypenGlobalContext;\n private routes: RouteDefinition[] = [];\n private activeModule: HypenModuleInstance | null = null;\n private activeRoute: RouteDefinition | null = null;\n private unsubscribe: (() => void) | null = null;\n /**\n * Cached instances for module-backed routes, keyed by the lowercase\n * module id. Entries are populated on unmount (when `persist` is truthy)\n * and consulted on mount to restore state across navigations.\n *\n * Persistence is the default for any route whose `component` or `module`\n * resolves to a module definition; opt out by setting `persist: false`\n * on the definition.\n *\n * Bounded by `maxPersistedModules`. Map insertion order drives LRU\n * eviction: restoring from the cache removes the entry (so the next\n * persist re-inserts it at the MRU end), and persisting past the cap\n * destroys the oldest entry.\n */\n private persistedModules = new Map<string, HypenModuleInstance>();\n private readonly maxPersistedModules: number;\n /**\n * Serialized chain of in-flight route transitions. Because mount /\n * unmount are now async (they await `onActivated` / `onDeactivated`),\n * back-to-back navigations must not interleave. Each `handleRouteChange`\n * appends to this chain so transitions run strictly in order.\n */\n private navPromise: Promise<void> = Promise.resolve();\n\n constructor(\n router: HypenRouter,\n engine: IEngine,\n registry: HypenApp,\n globalContext: HypenGlobalContext,\n options: ManagedRouterOptions = {}\n ) {\n this.router = router;\n this.engine = engine;\n this.registry = registry;\n this.globalContext = globalContext;\n const cap = options.maxPersistedModules ?? DEFAULT_MAX_PERSISTED_MODULES;\n // Guard against obviously-invalid caps — a zero or negative limit would\n // mean \"never persist\", which the call site should express via\n // `persist: false` on definitions instead.\n this.maxPersistedModules = cap > 0 ? cap : DEFAULT_MAX_PERSISTED_MODULES;\n }\n\n /**\n * Add a route definition.\n */\n addRoute(route: RouteDefinition): this {\n this.routes.push(route);\n return this;\n }\n\n /**\n * Start listening for route changes and mount the initial route.\n *\n * Also installs the `@router.*` engine action handlers so DSL authors can\n * write `.onClick(@router.push, to: \"/x\")` / `@router.back` /\n * `@router.replace` / `@router.forward` and have them dispatched against\n * this session's `HypenRouter` without any per-example wiring. The\n * reserved namespace is set up by `hypen-engine` in `ir/expand.rs`.\n */\n start(): void {\n this.unsubscribe = this.router.onNavigate((routeState: RouteState) => {\n this.handleRouteChange(routeState);\n });\n this.installRouterActions();\n }\n\n /**\n * Register handlers for the reserved `router.*` action namespace.\n *\n * All navigations go through `queueMicrotask` so the router state mutation\n * lands after the engine finishes dispatching the action — synchronous\n * writes would re-enter the engine's WASM state proxy, which the Rust side\n * rejects (\"recursive use of an object\").\n */\n private installRouterActions(): void {\n const router = this.router;\n const defer = (fn: () => void) => queueMicrotask(fn);\n const readTo = (action: { payload?: unknown } | null | undefined): string | null => {\n const payload = action?.payload as Record<string, unknown> | null | undefined;\n const to = payload?.to;\n return typeof to === \"string\" && to.length > 0 ? to : null;\n };\n\n this.engine.onAction(\"router.push\", (action) => {\n const to = readTo(action);\n if (to) defer(() => router.push(to));\n });\n this.engine.onAction(\"router.replace\", (action) => {\n const to = readTo(action);\n if (to) defer(() => router.replace(to));\n });\n this.engine.onAction(\"router.back\", () => {\n defer(() => router.back());\n });\n this.engine.onAction(\"router.forward\", () => {\n defer(() => router.forward());\n });\n }\n\n /**\n * Stop listening and unmount the active module.\n * Also destroys all persisted modules.\n *\n * Waits for any in-flight navigation to settle before tearing down so\n * lifecycle hooks always complete in order.\n */\n async stop(): Promise<void> {\n if (this.unsubscribe) {\n this.unsubscribe();\n this.unsubscribe = null;\n }\n\n // Wait for any in-flight navigation before tearing down.\n const cleanup = this.navPromise\n .catch(() => {\n /* previous navigation errors already reported */\n })\n .then(async () => {\n await this.unmountActive();\n\n // Destroy all persisted modules on full stop. Instances in this\n // map are inactive (they were deactivated when persisted), so\n // `destroy()` just fires `onDestroyed`.\n for (const [moduleId, instance] of this.persistedModules) {\n log.debug(`Destroying persisted module on stop: ${moduleId}`);\n try {\n await instance.destroy();\n } catch (e) {\n log.error(`Error destroying persisted module ${moduleId}:`, e);\n }\n this.globalContext.unregisterModule(moduleId);\n }\n this.persistedModules.clear();\n });\n\n this.navPromise = cleanup;\n await cleanup;\n\n // Clean up router event listeners\n this.router.dispose();\n }\n\n /**\n * Returns a promise that resolves once all currently-queued route\n * transitions have finished. Useful in tests to await lifecycle\n * ordering without reaching into internals.\n */\n async waitForNavigation(): Promise<void> {\n try {\n await this.navPromise;\n } catch {\n /* navigation errors are logged; callers don't need to rethrow */\n }\n }\n\n /**\n * Get the currently active module instance.\n */\n getActiveModule(): HypenModuleInstance | null {\n return this.activeModule;\n }\n\n /**\n * Get the currently active route.\n */\n getActiveRoute(): RouteDefinition | null {\n return this.activeRoute;\n }\n\n private handleRouteChange(routeState: RouteState): void {\n // Chain onto the in-flight navigation promise so transitions run\n // strictly in order. Async activate/deactivate hooks otherwise make\n // it possible for a late `A → B` to finish before an earlier `B → A`.\n this.navPromise = this.navPromise\n .catch((e) => {\n log.error(\"Previous navigation failed:\", e);\n })\n .then(() => this.processRouteChange(routeState));\n }\n\n private async processRouteChange(routeState: RouteState): Promise<void> {\n const path = routeState.currentPath;\n const matched = this.matchRoute(path);\n\n if (!matched) {\n log.debug(`No route matched for path: ${path}`);\n await this.unmountActive();\n return;\n }\n\n // If same route, no need to remount. Still a no-op for same-path\n // navigations (e.g. reselecting the active tab).\n if (this.activeRoute && this.activeRoute.path === matched.path) {\n return;\n }\n\n // Unmount old, mount new.\n await this.unmountActive();\n await this.mount(matched);\n }\n\n private matchRoute(path: string): RouteDefinition | null {\n for (const route of this.routes) {\n if (this.router.matchPath(route.path, path) !== null) {\n return route;\n }\n }\n return null;\n }\n\n private async mount(route: RouteDefinition): Promise<void> {\n // Look up module definition: inline first, then registry\n const definition = route.module || this.registry.get(route.component);\n if (!definition) {\n log.debug(`No module definition found for component: ${route.component}`);\n this.activeRoute = route;\n return;\n }\n\n // Ensure the definition has a name for state namespacing\n const namedDef = {\n ...definition,\n name: definition.name || route.component.toLowerCase(),\n };\n\n const moduleId = namedDef.name!.toLowerCase();\n\n // Check for a persisted instance first. If we hit the cache, we reuse\n // its state verbatim — `onCreated` has already fired (once) and\n // activate() below will fire `onActivated` without re-running the\n // one-time setup.\n const persisted = this.persistedModules.get(moduleId);\n if (persisted) {\n log.debug(`Restoring persisted module: ${moduleId} for route: ${route.path}`);\n this.activeModule = persisted;\n this.activeRoute = route;\n // Remove from the cache while it's active so that a concurrent\n // navigation can't double-mount the same instance.\n this.persistedModules.delete(moduleId);\n await persisted.activate();\n return;\n }\n\n log.debug(`Mounting module: ${namedDef.name} for route: ${route.path}`);\n\n const instance = new HypenModuleInstance(\n this.engine,\n namedDef as HypenModuleDefinition<object>,\n this.router,\n this.globalContext\n );\n\n // Register in global context under the module name (lowercase)\n this.globalContext.registerModule(moduleId, instance);\n\n this.activeModule = instance;\n this.activeRoute = route;\n\n // Fire `onActivated` after `onCreated` resolves. `activate()` awaits\n // the instance's internal readiness promise, so this is safe even\n // though the constructor kicks off `onCreated` asynchronously.\n await instance.activate();\n }\n\n private async unmountActive(): Promise<void> {\n if (!this.activeModule || !this.activeRoute) {\n this.activeModule = null;\n this.activeRoute = null;\n return;\n }\n\n const active = this.activeModule;\n const route = this.activeRoute;\n const definition = route.module || this.registry.get(route.component);\n const moduleId = (definition?.name || route.component).toLowerCase();\n\n // Persistence default: any route backed by a module definition\n // persists unless the definition explicitly opts out.\n //\n // definition present & persist !== false → cache the instance\n // definition present & persist === false → destroy the instance\n // definition missing → nothing to persist\n //\n // This is the new default behavior — prior to this change, persist\n // was opt-in via `persist: true`. Modules that relied on a fresh\n // instance on every navigation should set `persist: false` or move\n // per-navigation work into the new `onActivated` hook.\n const persist = definition != null && definition.persist !== false;\n\n // Clear the active slot *before* firing lifecycle hooks so the\n // module cannot observe itself as \"active\" from inside onDeactivated.\n this.activeModule = null;\n this.activeRoute = null;\n\n // Always deactivate first — regardless of whether we persist or\n // destroy — so `onDeactivated → (onDestroyed)` ordering holds.\n try {\n await active.deactivate();\n } catch (e) {\n log.error(`Error deactivating module ${moduleId}:`, e);\n }\n\n if (persist) {\n log.debug(`Persisting module: ${moduleId}`);\n // If this id is already cached (e.g. after being destroyed and\n // re-activated without passing through mount(), though rare),\n // delete first so the re-insert places it at the MRU end.\n this.persistedModules.delete(moduleId);\n this.persistedModules.set(moduleId, active);\n await this.evictPersistedOverflow();\n // Keep registered in GlobalContext so other modules can still\n // access its state while it's off-screen.\n } else {\n log.debug(`Unmounting module: ${moduleId}`);\n try {\n await active.destroy();\n } catch (e) {\n log.error(`Error destroying module ${moduleId}:`, e);\n }\n this.globalContext.unregisterModule(moduleId);\n }\n }\n\n /**\n * Trim `persistedModules` down to `maxPersistedModules` by destroying\n * the oldest entries (FIFO in Map insertion order, which tracks LRU\n * because `mount()` removes on restore and `unmountActive()` re-inserts\n * at the MRU end on every persist).\n *\n * Errors during eviction are logged — one misbehaving `onDestroyed` hook\n * must not block the router from shrinking the cache on the next\n * navigation.\n */\n private async evictPersistedOverflow(): Promise<void> {\n while (this.persistedModules.size > this.maxPersistedModules) {\n const oldest = this.persistedModules.keys().next();\n if (oldest.done) break;\n const oldestId = oldest.value;\n const evicted = this.persistedModules.get(oldestId)!;\n this.persistedModules.delete(oldestId);\n log.debug(`Evicting persisted module (LRU): ${oldestId}`);\n try {\n await evicted.destroy();\n } catch (e) {\n log.error(`Error destroying evicted module ${oldestId}:`, e);\n }\n this.globalContext.unregisterModule(oldestId);\n }\n }\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "2744EFD6F71F4CB364756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|