@hypen-space/core 0.4.81 → 0.4.83

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/dist/index.d.ts CHANGED
@@ -50,10 +50,14 @@ export { DataSourceManager } from "./datasource.js";
50
50
  export type { DataSourcePlugin, DataSourceStatus, DataSourceQuery, DataSourceSubscription, DataSourceChange, IDataSourceEngine, } from "./datasource.js";
51
51
  export { createObservableState, batchStateUpdates, getStateSnapshot, isStateProxy, unwrapProxy, } from "./state.js";
52
52
  export type { StatePath, StateChange, StateObserverOptions, } from "./state.js";
53
+ export { setPortableImpl, portable } from "./portable.js";
54
+ export type { PortableImpl } from "./portable.js";
53
55
  export { BaseRenderer, ConsoleRenderer } from "./renderer.js";
54
56
  export type { Renderer } from "./renderer.js";
55
57
  export { HypenRouter } from "./router.js";
56
58
  export type { RouteMatch, RouteState, RouteChangeCallback, } from "./router.js";
59
+ export { ManagedRouter } from "./managed-router.js";
60
+ export type { RouteDefinition } from "./managed-router.js";
57
61
  export { TypedEventEmitter, createEventEmitter } from "./events.js";
58
62
  export type { EventHandler, HypenFrameworkEvents } from "./events.js";
59
63
  export { HypenGlobalContext } from "./context.js";
package/dist/index.js CHANGED
@@ -602,6 +602,53 @@ class DataSourceManager {
602
602
  }
603
603
  }
604
604
 
605
+ // src/portable.ts
606
+ function notInstalled(name) {
607
+ throw new Error(`[@hypen-space/core] Portable helper "${name}" called before the engine was installed. ` + `Import @hypen-space/server, @hypen-space/web-engine, or call setPortableImpl() before use.`);
608
+ }
609
+ var current = {
610
+ diffState: () => notInstalled("diffState"),
611
+ matchPath: () => notInstalled("matchPath"),
612
+ pathGet: () => notInstalled("pathGet"),
613
+ pathHas: () => notInstalled("pathHas"),
614
+ pathSet: () => notInstalled("pathSet"),
615
+ pathDelete: () => notInstalled("pathDelete"),
616
+ encodeUriComponent: () => notInstalled("encodeUriComponent"),
617
+ decodeUriComponent: () => notInstalled("decodeUriComponent"),
618
+ parseQuery: () => notInstalled("parseQuery"),
619
+ buildUrl: () => notInstalled("buildUrl")
620
+ };
621
+ function setPortableImpl(impl) {
622
+ if (impl === null) {
623
+ current = {
624
+ diffState: () => notInstalled("diffState"),
625
+ matchPath: () => notInstalled("matchPath"),
626
+ pathGet: () => notInstalled("pathGet"),
627
+ pathHas: () => notInstalled("pathHas"),
628
+ pathSet: () => notInstalled("pathSet"),
629
+ pathDelete: () => notInstalled("pathDelete"),
630
+ encodeUriComponent: () => notInstalled("encodeUriComponent"),
631
+ decodeUriComponent: () => notInstalled("decodeUriComponent"),
632
+ parseQuery: () => notInstalled("parseQuery"),
633
+ buildUrl: () => notInstalled("buildUrl")
634
+ };
635
+ return;
636
+ }
637
+ current = impl;
638
+ }
639
+ var portable = {
640
+ diffState: (o, n, b) => current.diffState(o, n, b),
641
+ matchPath: (p, path) => current.matchPath(p, path),
642
+ pathGet: (v, p) => current.pathGet(v, p),
643
+ pathHas: (v, p) => current.pathHas(v, p),
644
+ pathSet: (v, p, nv) => current.pathSet(v, p, nv),
645
+ pathDelete: (v, p) => current.pathDelete(v, p),
646
+ encodeUriComponent: (s) => current.encodeUriComponent(s),
647
+ decodeUriComponent: (s) => current.decodeUriComponent(s),
648
+ parseQuery: (f) => current.parseQuery(f),
649
+ buildUrl: (p, q) => current.buildUrl(p, q)
650
+ };
651
+
605
652
  // src/state.ts
606
653
  var IS_PROXY = Symbol.for("hypen.isProxy");
607
654
  var RAW_TARGET = Symbol.for("hypen.rawTarget");
@@ -661,51 +708,7 @@ function deepClone(obj) {
661
708
  return cloneInternal(obj);
662
709
  }
663
710
  function diffState(oldState, newState, basePath = "") {
664
- const paths = [];
665
- const newValues = {};
666
- function diff(oldVal, newVal, path) {
667
- if (oldVal === newVal)
668
- return;
669
- if (typeof oldVal !== "object" || typeof newVal !== "object" || oldVal === null || newVal === null) {
670
- if (oldVal !== newVal) {
671
- paths.push(path);
672
- newValues[path] = newVal;
673
- }
674
- return;
675
- }
676
- if (Array.isArray(oldVal) || Array.isArray(newVal)) {
677
- if (!Array.isArray(oldVal) || !Array.isArray(newVal) || oldVal.length !== newVal.length) {
678
- paths.push(path);
679
- newValues[path] = newVal;
680
- return;
681
- }
682
- for (let i = 0;i < newVal.length; i++) {
683
- const itemPath = path ? `${path}.${i}` : `${i}`;
684
- diff(oldVal[i], newVal[i], itemPath);
685
- }
686
- return;
687
- }
688
- const oldKeys = new Set(Object.keys(oldVal));
689
- const newKeys = new Set(Object.keys(newVal));
690
- for (const key of newKeys) {
691
- const propPath = path ? `${path}.${key}` : key;
692
- if (!oldKeys.has(key)) {
693
- paths.push(propPath);
694
- newValues[propPath] = newVal[key];
695
- } else {
696
- diff(oldVal[key], newVal[key], propPath);
697
- }
698
- }
699
- for (const key of oldKeys) {
700
- if (!newKeys.has(key)) {
701
- const propPath = path ? `${path}.${key}` : key;
702
- paths.push(propPath);
703
- newValues[propPath] = undefined;
704
- }
705
- }
706
- }
707
- diff(oldState, newState, basePath);
708
- return { paths, newValues };
711
+ return portable.diffState(oldState, newState, basePath);
709
712
  }
710
713
  function createObservableState(initialState, options) {
711
714
  const opts = options || { onChange: () => {} };
@@ -1054,6 +1057,8 @@ class HypenAppBuilder {
1054
1057
  initialState;
1055
1058
  options;
1056
1059
  createdHandler;
1060
+ activatedHandler;
1061
+ deactivatedHandler;
1057
1062
  actionHandlers = new Map;
1058
1063
  destroyedHandler;
1059
1064
  disconnectHandler;
@@ -1077,6 +1082,14 @@ class HypenAppBuilder {
1077
1082
  this.actionHandlers.set(name, fn);
1078
1083
  return this;
1079
1084
  }
1085
+ onActivated(fn) {
1086
+ this.activatedHandler = fn;
1087
+ return this;
1088
+ }
1089
+ onDeactivated(fn) {
1090
+ this.deactivatedHandler = fn;
1091
+ return this;
1092
+ }
1080
1093
  onDestroyed(fn) {
1081
1094
  this.destroyedHandler = fn;
1082
1095
  return this;
@@ -1128,6 +1141,8 @@ class HypenAppBuilder {
1128
1141
  stateStore: this._stateStore,
1129
1142
  handlers: {
1130
1143
  onCreated: this.createdHandler,
1144
+ onActivated: this.activatedHandler,
1145
+ onDeactivated: this.deactivatedHandler,
1131
1146
  onAction: this.actionHandlers,
1132
1147
  onDestroyed: this.destroyedHandler,
1133
1148
  onDisconnect: this.disconnectHandler,
@@ -1185,6 +1200,7 @@ class HypenModuleInstance {
1185
1200
  definition;
1186
1201
  state;
1187
1202
  isDestroyed = false;
1203
+ isActive = false;
1188
1204
  router;
1189
1205
  globalContext;
1190
1206
  stateChangeCallbacks = [];
@@ -1264,6 +1280,43 @@ class HypenModuleInstance {
1264
1280
  async waitForReady() {
1265
1281
  await this._readyPromise;
1266
1282
  }
1283
+ async activate() {
1284
+ if (this.isDestroyed || this.isActive)
1285
+ return;
1286
+ await this._readyPromise;
1287
+ if (this.isDestroyed || this.isActive)
1288
+ return;
1289
+ this.isActive = true;
1290
+ if (this.definition.handlers.onActivated) {
1291
+ const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
1292
+ try {
1293
+ await this.definition.handlers.onActivated(this.state, context);
1294
+ } catch (e) {
1295
+ const error = e instanceof HypenError ? e : new ActionError("onActivated", e);
1296
+ const shouldRethrow = await this.handleError(error, { lifecycle: "activated" });
1297
+ if (shouldRethrow) {
1298
+ throw error;
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ async deactivate() {
1304
+ if (this.isDestroyed || !this.isActive)
1305
+ return;
1306
+ this.isActive = false;
1307
+ if (this.definition.handlers.onDeactivated) {
1308
+ const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
1309
+ try {
1310
+ await this.definition.handlers.onDeactivated(this.state, context);
1311
+ } catch (e) {
1312
+ const error = e instanceof HypenError ? e : new ActionError("onDeactivated", e);
1313
+ const shouldRethrow = await this.handleError(error, { lifecycle: "deactivated" });
1314
+ if (shouldRethrow) {
1315
+ throw error;
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1267
1320
  createGlobalContextAPI() {
1268
1321
  if (!this.globalContext) {
1269
1322
  throw new Error("Global context not available");
@@ -1412,6 +1465,9 @@ class HypenModuleInstance {
1412
1465
  async destroy() {
1413
1466
  if (this.isDestroyed)
1414
1467
  return;
1468
+ if (this.isActive) {
1469
+ await this.deactivate();
1470
+ }
1415
1471
  if (this.currentPersistKey && this.stateStore) {
1416
1472
  clearTimeout(this.persistDebounceTimer);
1417
1473
  const snapshot = getStateSnapshot(this.state);
@@ -1818,46 +1874,22 @@ class HypenRouter {
1818
1874
  return getStateSnapshot(this.state);
1819
1875
  }
1820
1876
  matchPath(pattern, path) {
1821
- if (!pattern || typeof pattern !== "string") {
1877
+ if (!pattern || typeof pattern !== "string")
1822
1878
  return null;
1823
- }
1824
- if (!path || typeof path !== "string") {
1879
+ if (!path || typeof path !== "string")
1825
1880
  return null;
1826
- }
1827
- if (pattern === path) {
1828
- return {
1829
- params: {},
1830
- query: this.state.query,
1831
- path
1832
- };
1833
- }
1834
- if (pattern.endsWith("/*")) {
1835
- const prefix = pattern.slice(0, -2);
1836
- if (path === prefix || path.startsWith(prefix + "/")) {
1837
- return {
1838
- params: {},
1839
- query: this.state.query,
1840
- path
1841
- };
1842
- }
1843
- return null;
1844
- }
1845
- const paramNames = [];
1846
- const regexPattern = pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
1847
- paramNames.push(name);
1848
- return "([^/]+)";
1849
- }).replace(/\*/g, ".*");
1850
- const regex = new RegExp(`^${regexPattern}$`);
1851
- const match2 = path.match(regex);
1852
- if (!match2)
1881
+ const cleanPath = path.split("?")[0] ?? path;
1882
+ const result = portable.matchPath(pattern, cleanPath);
1883
+ if (!result)
1853
1884
  return null;
1854
1885
  const params = {};
1855
- paramNames.forEach((name, i) => {
1856
- const value = match2[i + 1];
1857
- if (value !== undefined) {
1858
- params[name] = decodeURIComponent(value);
1886
+ for (const [name, raw] of Object.entries(result.params)) {
1887
+ try {
1888
+ params[name] = decodeURIComponent(raw);
1889
+ } catch {
1890
+ params[name] = raw;
1859
1891
  }
1860
- });
1892
+ }
1861
1893
  return {
1862
1894
  params,
1863
1895
  query: this.state.query,
@@ -2332,13 +2364,13 @@ class RemoteEngine {
2332
2364
  if (!state2 || typeof state2 !== "object")
2333
2365
  return;
2334
2366
  const parts = key.split(".");
2335
- let current = state2;
2367
+ let current2 = state2;
2336
2368
  for (const part of parts) {
2337
- if (current == null || typeof current !== "object")
2369
+ if (current2 == null || typeof current2 !== "object")
2338
2370
  return;
2339
- current = current[part];
2371
+ current2 = current2[part];
2340
2372
  }
2341
- return typeof current === "string" ? current : undefined;
2373
+ return typeof current2 === "string" ? current2 : undefined;
2342
2374
  }
2343
2375
  dispatchAction(action, payload) {
2344
2376
  if (this.state !== "connected" || !this.ws) {
@@ -2828,6 +2860,204 @@ var Link = app.defineState({
2828
2860
  });
2829
2861
  }
2830
2862
  }).build();
2863
+ // src/managed-router.ts
2864
+ var log10 = createLogger("ManagedRouter");
2865
+ var DEFAULT_MAX_PERSISTED_MODULES = 10;
2866
+
2867
+ class ManagedRouter {
2868
+ router;
2869
+ engine;
2870
+ registry;
2871
+ globalContext;
2872
+ routes = [];
2873
+ activeModule = null;
2874
+ activeRoute = null;
2875
+ unsubscribe = null;
2876
+ persistedModules = new Map;
2877
+ maxPersistedModules;
2878
+ navPromise = Promise.resolve();
2879
+ constructor(router, engine, registry, globalContext, options = {}) {
2880
+ this.router = router;
2881
+ this.engine = engine;
2882
+ this.registry = registry;
2883
+ this.globalContext = globalContext;
2884
+ const cap = options.maxPersistedModules ?? DEFAULT_MAX_PERSISTED_MODULES;
2885
+ this.maxPersistedModules = cap > 0 ? cap : DEFAULT_MAX_PERSISTED_MODULES;
2886
+ }
2887
+ addRoute(route) {
2888
+ this.routes.push(route);
2889
+ return this;
2890
+ }
2891
+ start() {
2892
+ this.unsubscribe = this.router.onNavigate((routeState) => {
2893
+ this.handleRouteChange(routeState);
2894
+ });
2895
+ this.installRouterActions();
2896
+ }
2897
+ installRouterActions() {
2898
+ const router = this.router;
2899
+ const defer = (fn) => queueMicrotask(fn);
2900
+ const readTo = (action) => {
2901
+ const payload = action?.payload;
2902
+ const to = payload?.to;
2903
+ return typeof to === "string" && to.length > 0 ? to : null;
2904
+ };
2905
+ this.engine.onAction("router.push", (action) => {
2906
+ const to = readTo(action);
2907
+ if (to)
2908
+ defer(() => router.push(to));
2909
+ });
2910
+ this.engine.onAction("router.replace", (action) => {
2911
+ const to = readTo(action);
2912
+ if (to)
2913
+ defer(() => router.replace(to));
2914
+ });
2915
+ this.engine.onAction("router.back", () => {
2916
+ defer(() => router.back());
2917
+ });
2918
+ this.engine.onAction("router.forward", () => {
2919
+ defer(() => router.forward());
2920
+ });
2921
+ }
2922
+ async stop() {
2923
+ if (this.unsubscribe) {
2924
+ this.unsubscribe();
2925
+ this.unsubscribe = null;
2926
+ }
2927
+ const cleanup = this.navPromise.catch(() => {}).then(async () => {
2928
+ await this.unmountActive();
2929
+ for (const [moduleId, instance] of this.persistedModules) {
2930
+ log10.debug(`Destroying persisted module on stop: ${moduleId}`);
2931
+ try {
2932
+ await instance.destroy();
2933
+ } catch (e) {
2934
+ log10.error(`Error destroying persisted module ${moduleId}:`, e);
2935
+ }
2936
+ this.globalContext.unregisterModule(moduleId);
2937
+ }
2938
+ this.persistedModules.clear();
2939
+ });
2940
+ this.navPromise = cleanup;
2941
+ await cleanup;
2942
+ this.router.dispose();
2943
+ }
2944
+ async waitForNavigation() {
2945
+ try {
2946
+ await this.navPromise;
2947
+ } catch {}
2948
+ }
2949
+ getActiveModule() {
2950
+ return this.activeModule;
2951
+ }
2952
+ getActiveRoute() {
2953
+ return this.activeRoute;
2954
+ }
2955
+ handleRouteChange(routeState) {
2956
+ this.navPromise = this.navPromise.catch((e) => {
2957
+ log10.error("Previous navigation failed:", e);
2958
+ }).then(() => this.processRouteChange(routeState));
2959
+ }
2960
+ async processRouteChange(routeState) {
2961
+ const path = routeState.currentPath;
2962
+ const matched = this.matchRoute(path);
2963
+ if (!matched) {
2964
+ log10.debug(`No route matched for path: ${path}`);
2965
+ await this.unmountActive();
2966
+ return;
2967
+ }
2968
+ if (this.activeRoute && this.activeRoute.path === matched.path) {
2969
+ return;
2970
+ }
2971
+ await this.unmountActive();
2972
+ await this.mount(matched);
2973
+ }
2974
+ matchRoute(path) {
2975
+ for (const route of this.routes) {
2976
+ if (this.router.matchPath(route.path, path) !== null) {
2977
+ return route;
2978
+ }
2979
+ }
2980
+ return null;
2981
+ }
2982
+ async mount(route) {
2983
+ const definition = route.module || this.registry.get(route.component);
2984
+ if (!definition) {
2985
+ log10.debug(`No module definition found for component: ${route.component}`);
2986
+ this.activeRoute = route;
2987
+ return;
2988
+ }
2989
+ const namedDef = {
2990
+ ...definition,
2991
+ name: definition.name || route.component.toLowerCase()
2992
+ };
2993
+ const moduleId = namedDef.name.toLowerCase();
2994
+ const persisted = this.persistedModules.get(moduleId);
2995
+ if (persisted) {
2996
+ log10.debug(`Restoring persisted module: ${moduleId} for route: ${route.path}`);
2997
+ this.activeModule = persisted;
2998
+ this.activeRoute = route;
2999
+ this.persistedModules.delete(moduleId);
3000
+ await persisted.activate();
3001
+ return;
3002
+ }
3003
+ log10.debug(`Mounting module: ${namedDef.name} for route: ${route.path}`);
3004
+ const instance = new HypenModuleInstance(this.engine, namedDef, this.router, this.globalContext);
3005
+ this.globalContext.registerModule(moduleId, instance);
3006
+ this.activeModule = instance;
3007
+ this.activeRoute = route;
3008
+ await instance.activate();
3009
+ }
3010
+ async unmountActive() {
3011
+ if (!this.activeModule || !this.activeRoute) {
3012
+ this.activeModule = null;
3013
+ this.activeRoute = null;
3014
+ return;
3015
+ }
3016
+ const active = this.activeModule;
3017
+ const route = this.activeRoute;
3018
+ const definition = route.module || this.registry.get(route.component);
3019
+ const moduleId = (definition?.name || route.component).toLowerCase();
3020
+ const persist = definition != null && definition.persist !== false;
3021
+ this.activeModule = null;
3022
+ this.activeRoute = null;
3023
+ try {
3024
+ await active.deactivate();
3025
+ } catch (e) {
3026
+ log10.error(`Error deactivating module ${moduleId}:`, e);
3027
+ }
3028
+ if (persist) {
3029
+ log10.debug(`Persisting module: ${moduleId}`);
3030
+ this.persistedModules.delete(moduleId);
3031
+ this.persistedModules.set(moduleId, active);
3032
+ await this.evictPersistedOverflow();
3033
+ } else {
3034
+ log10.debug(`Unmounting module: ${moduleId}`);
3035
+ try {
3036
+ await active.destroy();
3037
+ } catch (e) {
3038
+ log10.error(`Error destroying module ${moduleId}:`, e);
3039
+ }
3040
+ this.globalContext.unregisterModule(moduleId);
3041
+ }
3042
+ }
3043
+ async evictPersistedOverflow() {
3044
+ while (this.persistedModules.size > this.maxPersistedModules) {
3045
+ const oldest = this.persistedModules.keys().next();
3046
+ if (oldest.done)
3047
+ break;
3048
+ const oldestId = oldest.value;
3049
+ const evicted = this.persistedModules.get(oldestId);
3050
+ this.persistedModules.delete(oldestId);
3051
+ log10.debug(`Evicting persisted module (LRU): ${oldestId}`);
3052
+ try {
3053
+ await evicted.destroy();
3054
+ } catch (e) {
3055
+ log10.error(`Error destroying evicted module ${oldestId}:`, e);
3056
+ }
3057
+ this.globalContext.unregisterModule(oldestId);
3058
+ }
3059
+ }
3060
+ }
2831
3061
  export {
2832
3062
  withRetry,
2833
3063
  usingSync,
@@ -2837,10 +3067,12 @@ export {
2837
3067
  unwrapOr,
2838
3068
  unwrap,
2839
3069
  state,
3070
+ setPortableImpl,
2840
3071
  setLogLevel,
2841
3072
  setDebugMode,
2842
3073
  retryResult,
2843
3074
  retry,
3075
+ portable,
2844
3076
  match,
2845
3077
  mapErr,
2846
3078
  map,
@@ -2891,6 +3123,7 @@ export {
2891
3123
  RemoteEngine,
2892
3124
  ParseError,
2893
3125
  Ok,
3126
+ ManagedRouter,
2894
3127
  Logger,
2895
3128
  Link,
2896
3129
  HypenRouter,
@@ -2910,4 +3143,4 @@ export {
2910
3143
  ActionError
2911
3144
  };
2912
3145
 
2913
- //# debugId=ABC8BA06546F591964756E2164756E21
3146
+ //# debugId=A9CE0B874F21F46D64756E2164756E21