@hypen-space/core 0.4.46 → 0.4.81

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/hypen.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Enables single-file components with inline UI templates.
5
5
  *
6
- * The `hypen` tagged template preserves ${state.x} and ${item.x} bindings
6
+ * The `hypen` tagged template preserves @{state.x} and @{item.x} bindings
7
7
  * instead of interpolating them, allowing you to write:
8
8
  *
9
9
  * @example
@@ -17,7 +17,7 @@
17
17
  * })
18
18
  * .ui(hypen`
19
19
  * Column {
20
- * Text("Count: ${state.count}")
20
+ * Text("Count: @{state.count}")
21
21
  * Button { Text("+") }
22
22
  * .onClick("@actions.increment")
23
23
  * }
@@ -27,12 +27,18 @@
27
27
  /**
28
28
  * Proxy for state bindings.
29
29
  *
30
- * Use in hypen templates to create reactive state bindings:
30
+ * In hypen templates, prefer the direct `@{state.x}` syntax. This proxy
31
+ * exists for cases where you want to interpolate JS values into templates
32
+ * — e.g., `hypen\`Text("${state.user.name}")\`` produces the same string
33
+ * as `hypen\`Text("@{state.user.name}")\``.
31
34
  *
32
35
  * @example
33
36
  * ```typescript
37
+ * // Preferred: direct syntax
38
+ * hypen`Text("Hello, @{state.user.name}")`
39
+ *
40
+ * // Also works: proxy interpolation
34
41
  * hypen`Text("Hello, ${state.user.name}")`
35
- * // Produces: Text("Hello, ${state.user.name}")
36
42
  * ```
37
43
  */
38
44
  export declare const state: any;
@@ -45,7 +51,7 @@ export declare const state: any;
45
51
  * ```typescript
46
52
  * hypen`
47
53
  * List(@state.items) {
48
- * Text("${item.name}: ${item.price}")
54
+ * Text("@{item.name}: @{item.price}")
49
55
  * }
50
56
  * `
51
57
  * ```
@@ -60,7 +66,7 @@ export declare const item: any;
60
66
  * ```typescript
61
67
  * hypen`
62
68
  * List(@state.items) {
63
- * Text("Item #${index}: ${item.name}")
69
+ * Text("Item #@{index}: @{item.name}")
64
70
  * }
65
71
  * `
66
72
  * ```
@@ -69,36 +75,35 @@ export declare const index: any;
69
75
  /**
70
76
  * Tagged template literal for Hypen DSL templates.
71
77
  *
72
- * Works with the `state`, `item`, and `index` binding proxies to preserve
73
- * binding syntax in the output string.
78
+ * Prefer the direct `@{state.x}` / `@{item.x}` syntax. The `state`, `item`,
79
+ * and `index` proxies are available for interpolating JS variables or
80
+ * mixing dynamic content with bindings.
74
81
  *
75
82
  * @example
76
83
  * ```typescript
77
84
  * import { hypen, state, item } from "@hypen-space/core";
78
85
  *
79
- * // Simple state binding
80
- * const t1 = hypen`Text("Count: ${state.count}")`;
81
- * // Result: 'Text("Count: ${state.count}")'
86
+ * // Preferred: direct binding syntax
87
+ * const t1 = hypen`Text("Count: @{state.count}")`;
82
88
  *
83
- * // Nested state binding
84
- * const t2 = hypen`Text("Hello, ${state.user.profile.name}")`;
85
- * // Result: 'Text("Hello, ${state.user.profile.name}")'
89
+ * // Nested paths work the same way
90
+ * const t2 = hypen`Text("Hello, @{state.user.profile.name}")`;
86
91
  *
87
92
  * // List with item binding
88
93
  * const t3 = hypen`
89
94
  * List(@state.products) {
90
- * Text("${item.name} - $${item.price}")
95
+ * Text("@{item.name} - $@{item.price}")
91
96
  * }
92
97
  * `;
93
98
  *
94
- * // Complex expressions (use regular JS interpolation for static values)
99
+ * // Interpolate JS variables alongside bindings
95
100
  * const title = "My App";
96
- * const t4 = hypen`Text("${title}: ${state.count}")`;
97
- * // Result: 'Text("My App: ${state.count}")'
101
+ * const t4 = hypen`Text("${title}: @{state.count}")`;
102
+ * // Result: 'Text("My App: @{state.count}")'
98
103
  * ```
99
104
  *
100
105
  * @param strings - Template literal string parts
101
- * @param expressions - Interpolated expressions (proxies return binding strings)
106
+ * @param expressions - Interpolated JS values or binding proxies
102
107
  * @returns The template string with bindings preserved
103
108
  */
104
109
  export declare function hypen(strings: TemplateStringsArray, ...expressions: unknown[]): string;
package/dist/hypen.js CHANGED
@@ -26,13 +26,13 @@ function createBindingProxy(root) {
26
26
  const handler = {
27
27
  get(_, prop) {
28
28
  if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
29
- return () => `\${${root}}`;
29
+ return () => `@{${root}}`;
30
30
  }
31
31
  if (typeof prop === "symbol") {
32
32
  return;
33
33
  }
34
34
  if (prop === "toJSON") {
35
- return () => `\${${root}}`;
35
+ return () => `@{${root}}`;
36
36
  }
37
37
  return createBindingProxy(`${root}.${prop}`);
38
38
  },
@@ -54,10 +54,10 @@ function createBindingProxy(root) {
54
54
  var state = createBindingProxy("state");
55
55
  var item = createBindingProxy("item");
56
56
  var index = {
57
- [Symbol.toPrimitive]: () => "${index}",
58
- toString: () => "${index}",
59
- valueOf: () => "${index}",
60
- toJSON: () => "${index}"
57
+ [Symbol.toPrimitive]: () => "@{index}",
58
+ toString: () => "@{index}",
59
+ valueOf: () => "@{index}",
60
+ toJSON: () => "@{index}"
61
61
  };
62
62
  function hypen(strings, ...expressions) {
63
63
  let result = strings[0];
@@ -75,4 +75,4 @@ export {
75
75
  hypen
76
76
  };
77
77
 
78
- //# debugId=00831BA93E2ED31164756E2164756E21
78
+ //# debugId=E5F0C092159197CD64756E2164756E21
package/dist/hypen.js.map CHANGED
@@ -2,9 +2,9 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/hypen.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * Hypen Tagged Template Literal\n *\n * Enables single-file components with inline UI templates.\n *\n * The `hypen` tagged template preserves ${state.x} and ${item.x} bindings\n * instead of interpolating them, allowing you to write:\n *\n * @example\n * ```typescript\n * import { app, hypen, state } from \"@hypen-space/core\";\n *\n * export default app\n * .defineState({ count: 0 })\n * .onAction(\"increment\", ({ state }) => {\n * state.count += 1;\n * })\n * .ui(hypen`\n * Column {\n * Text(\"Count: ${state.count}\")\n * Button { Text(\"+\") }\n * .onClick(\"@actions.increment\")\n * }\n * `);\n * ```\n */\n\n/**\n * Creates a proxy that captures property access as a binding path string.\n *\n * When used in a template literal, the proxy's toString() returns the\n * full binding expression (e.g., \"${state.user.name}\").\n *\n * @example\n * ```typescript\n * const state = createBindingProxy('state');\n * `${state.user.name}` // Returns: \"${state.user.name}\"\n * ```\n */\nfunction createBindingProxy(root: string): any {\n const handler: ProxyHandler<object> = {\n get(_, prop: string | symbol): any {\n // Handle Symbol.toPrimitive, toString, and valueOf for string conversion\n if (\n prop === Symbol.toPrimitive ||\n prop === \"toString\" ||\n prop === \"valueOf\"\n ) {\n return () => `\\${${root}}`;\n }\n\n // Handle other Symbol properties (e.g., Symbol.toStringTag)\n if (typeof prop === \"symbol\") {\n return undefined;\n }\n\n // Handle JSON.stringify\n if (prop === \"toJSON\") {\n return () => `\\${${root}}`;\n }\n\n // Chain to nested property: state.user -> state.user.name\n return createBindingProxy(`${root}.${prop}`);\n },\n\n // Support for `in` operator\n has() {\n return true;\n },\n\n // Support for Object.keys() - return empty to avoid enumeration issues\n ownKeys() {\n return [];\n },\n\n getOwnPropertyDescriptor() {\n return {\n configurable: true,\n enumerable: true,\n };\n },\n };\n\n return new Proxy({} as any, handler);\n}\n\n/**\n * Proxy for state bindings.\n *\n * Use in hypen templates to create reactive state bindings:\n *\n * @example\n * ```typescript\n * hypen`Text(\"Hello, ${state.user.name}\")`\n * // Produces: Text(\"Hello, ${state.user.name}\")\n * ```\n */\nexport const state: any = createBindingProxy(\"state\");\n\n/**\n * Proxy for item bindings in list iteration.\n *\n * Use inside List components to reference the current item:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"${item.name}: ${item.price}\")\n * }\n * `\n * ```\n */\nexport const item: any = createBindingProxy(\"item\");\n\n/**\n * Proxy for index in list iteration.\n *\n * Use inside List components to reference the current index:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"Item #${index}: ${item.name}\")\n * }\n * `\n * ```\n */\nexport const index: any = {\n [Symbol.toPrimitive]: () => \"${index}\",\n toString: () => \"${index}\",\n valueOf: () => \"${index}\",\n toJSON: () => \"${index}\",\n};\n\n/**\n * Tagged template literal for Hypen DSL templates.\n *\n * Works with the `state`, `item`, and `index` binding proxies to preserve\n * binding syntax in the output string.\n *\n * @example\n * ```typescript\n * import { hypen, state, item } from \"@hypen-space/core\";\n *\n * // Simple state binding\n * const t1 = hypen`Text(\"Count: ${state.count}\")`;\n * // Result: 'Text(\"Count: ${state.count}\")'\n *\n * // Nested state binding\n * const t2 = hypen`Text(\"Hello, ${state.user.profile.name}\")`;\n * // Result: 'Text(\"Hello, ${state.user.profile.name}\")'\n *\n * // List with item binding\n * const t3 = hypen`\n * List(@state.products) {\n * Text(\"${item.name} - $${item.price}\")\n * }\n * `;\n *\n * // Complex expressions (use regular JS interpolation for static values)\n * const title = \"My App\";\n * const t4 = hypen`Text(\"${title}: ${state.count}\")`;\n * // Result: 'Text(\"My App: ${state.count}\")'\n * ```\n *\n * @param strings - Template literal string parts\n * @param expressions - Interpolated expressions (proxies return binding strings)\n * @returns The template string with bindings preserved\n */\nexport function hypen(\n strings: TemplateStringsArray,\n ...expressions: unknown[]\n): string {\n let result = strings[0];\n\n for (let i = 0; i < expressions.length; i++) {\n const expr = expressions[i];\n\n // Convert expression to string\n // Binding proxies will return \"${state.x}\" via their toString()\n result += String(expr);\n result += strings[i + 1]!;\n }\n\n return result!.trim();\n}\n\n/**\n * Type helper for defining state shape.\n * Use with state proxy for better IDE support in complex scenarios.\n *\n * @example\n * ```typescript\n * type MyState = { user: { name: string; age: number } };\n * const typedState = state as StateProxy<MyState>;\n * ```\n */\nexport type StateProxy<T> = {\n [K in keyof T]: T[K] extends object\n ? StateProxy<T[K]> & { toString(): string }\n : { toString(): string };\n} & { toString(): string };\n\n/**\n * Type helper for item proxy in lists.\n */\nexport type ItemProxy<T> = StateProxy<T>;\n"
5
+ "/**\n * Hypen Tagged Template Literal\n *\n * Enables single-file components with inline UI templates.\n *\n * The `hypen` tagged template preserves @{state.x} and @{item.x} bindings\n * instead of interpolating them, allowing you to write:\n *\n * @example\n * ```typescript\n * import { app, hypen, state } from \"@hypen-space/core\";\n *\n * export default app\n * .defineState({ count: 0 })\n * .onAction(\"increment\", ({ state }) => {\n * state.count += 1;\n * })\n * .ui(hypen`\n * Column {\n * Text(\"Count: @{state.count}\")\n * Button { Text(\"+\") }\n * .onClick(\"@actions.increment\")\n * }\n * `);\n * ```\n */\n\n/**\n * Creates a proxy that captures property access as a binding path string.\n *\n * When used in a template literal, the proxy's toString() returns the\n * full binding expression (e.g., \"@{state.user.name}\"). This lets you\n * interpolate JS variables into hypen templates without losing type safety.\n *\n * @example\n * ```typescript\n * const state = createBindingProxy('state');\n * String(state.user.name) // Returns: \"@{state.user.name}\"\n * ```\n */\nfunction createBindingProxy(root: string): any {\n const handler: ProxyHandler<object> = {\n get(_, prop: string | symbol): any {\n // Handle Symbol.toPrimitive, toString, and valueOf for string conversion\n if (\n prop === Symbol.toPrimitive ||\n prop === \"toString\" ||\n prop === \"valueOf\"\n ) {\n return () => `@{${root}}`;\n }\n\n // Handle other Symbol properties (e.g., Symbol.toStringTag)\n if (typeof prop === \"symbol\") {\n return undefined;\n }\n\n // Handle JSON.stringify\n if (prop === \"toJSON\") {\n return () => `@{${root}}`;\n }\n\n // Chain to nested property: state.user -> state.user.name\n return createBindingProxy(`${root}.${prop}`);\n },\n\n // Support for `in` operator\n has() {\n return true;\n },\n\n // Support for Object.keys() - return empty to avoid enumeration issues\n ownKeys() {\n return [];\n },\n\n getOwnPropertyDescriptor() {\n return {\n configurable: true,\n enumerable: true,\n };\n },\n };\n\n return new Proxy({} as any, handler);\n}\n\n/**\n * Proxy for state bindings.\n *\n * In hypen templates, prefer the direct `@{state.x}` syntax. This proxy\n * exists for cases where you want to interpolate JS values into templates\n * — e.g., `hypen\\`Text(\"${state.user.name}\")\\`` produces the same string\n * as `hypen\\`Text(\"@{state.user.name}\")\\``.\n *\n * @example\n * ```typescript\n * // Preferred: direct syntax\n * hypen`Text(\"Hello, @{state.user.name}\")`\n *\n * // Also works: proxy interpolation\n * hypen`Text(\"Hello, ${state.user.name}\")`\n * ```\n */\nexport const state: any = createBindingProxy(\"state\");\n\n/**\n * Proxy for item bindings in list iteration.\n *\n * Use inside List components to reference the current item:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"@{item.name}: @{item.price}\")\n * }\n * `\n * ```\n */\nexport const item: any = createBindingProxy(\"item\");\n\n/**\n * Proxy for index in list iteration.\n *\n * Use inside List components to reference the current index:\n *\n * @example\n * ```typescript\n * hypen`\n * List(@state.items) {\n * Text(\"Item #@{index}: @{item.name}\")\n * }\n * `\n * ```\n */\nexport const index: any = {\n [Symbol.toPrimitive]: () => \"@{index}\",\n toString: () => \"@{index}\",\n valueOf: () => \"@{index}\",\n toJSON: () => \"@{index}\",\n};\n\n/**\n * Tagged template literal for Hypen DSL templates.\n *\n * Prefer the direct `@{state.x}` / `@{item.x}` syntax. The `state`, `item`,\n * and `index` proxies are available for interpolating JS variables or\n * mixing dynamic content with bindings.\n *\n * @example\n * ```typescript\n * import { hypen, state, item } from \"@hypen-space/core\";\n *\n * // Preferred: direct binding syntax\n * const t1 = hypen`Text(\"Count: @{state.count}\")`;\n *\n * // Nested paths work the same way\n * const t2 = hypen`Text(\"Hello, @{state.user.profile.name}\")`;\n *\n * // List with item binding\n * const t3 = hypen`\n * List(@state.products) {\n * Text(\"@{item.name} - $@{item.price}\")\n * }\n * `;\n *\n * // Interpolate JS variables alongside bindings\n * const title = \"My App\";\n * const t4 = hypen`Text(\"${title}: @{state.count}\")`;\n * // Result: 'Text(\"My App: @{state.count}\")'\n * ```\n *\n * @param strings - Template literal string parts\n * @param expressions - Interpolated JS values or binding proxies\n * @returns The template string with bindings preserved\n */\nexport function hypen(\n strings: TemplateStringsArray,\n ...expressions: unknown[]\n): string {\n let result = strings[0];\n\n for (let i = 0; i < expressions.length; i++) {\n const expr = expressions[i];\n\n // Convert expression to string\n // Binding proxies will return \"@{state.x}\" via their toString()\n result += String(expr);\n result += strings[i + 1]!;\n }\n\n return result!.trim();\n}\n\n/**\n * Type helper for defining state shape.\n * Use with state proxy for better IDE support in complex scenarios.\n *\n * @example\n * ```typescript\n * type MyState = { user: { name: string; age: number } };\n * const typedState = state as StateProxy<MyState>;\n * ```\n */\nexport type StateProxy<T> = {\n [K in keyof T]: T[K] extends object\n ? StateProxy<T[K]> & { toString(): string }\n : { toString(): string };\n} & { toString(): string };\n\n/**\n * Type helper for item proxy in lists.\n */\nexport type ItemProxy<T> = StateProxy<T>;\n"
6
6
  ],
7
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAS,kBAAkB,CAAC,MAAmB;AAAA,EAC7C,MAAM,UAAgC;AAAA,IACpC,GAAG,CAAC,GAAG,MAA4B;AAAA,MAEjC,IACE,SAAS,OAAO,eAChB,SAAS,cACT,SAAS,WACT;AAAA,QACA,OAAO,MAAM,MAAM;AAAA,MACrB;AAAA,MAGA,IAAI,OAAO,SAAS,UAAU;AAAA,QAC5B;AAAA,MACF;AAAA,MAGA,IAAI,SAAS,UAAU;AAAA,QACrB,OAAO,MAAM,MAAM;AAAA,MACrB;AAAA,MAGA,OAAO,mBAAmB,GAAG,QAAQ,MAAM;AAAA;AAAA,IAI7C,GAAG,GAAG;AAAA,MACJ,OAAO;AAAA;AAAA,IAIT,OAAO,GAAG;AAAA,MACR,OAAO,CAAC;AAAA;AAAA,IAGV,wBAAwB,GAAG;AAAA,MACzB,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA;AAAA,EAEJ;AAAA,EAEA,OAAO,IAAI,MAAM,CAAC,GAAU,OAAO;AAAA;AAc9B,IAAM,QAAa,mBAAmB,OAAO;AAgB7C,IAAM,OAAY,mBAAmB,MAAM;AAgB3C,IAAM,QAAa;AAAA,GACvB,OAAO,cAAc,MAAM;AAAA,EAC5B,UAAU,MAAM;AAAA,EAChB,SAAS,MAAM;AAAA,EACf,QAAQ,MAAM;AAChB;AAqCO,SAAS,KAAK,CACnB,YACG,aACK;AAAA,EACR,IAAI,SAAS,QAAQ;AAAA,EAErB,SAAS,IAAI,EAAG,IAAI,YAAY,QAAQ,KAAK;AAAA,IAC3C,MAAM,OAAO,YAAY;AAAA,IAIzB,UAAU,OAAO,IAAI;AAAA,IACrB,UAAU,QAAQ,IAAI;AAAA,EACxB;AAAA,EAEA,OAAO,OAAQ,KAAK;AAAA;",
8
- "debugId": "00831BA93E2ED31164756E2164756E21",
7
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAS,kBAAkB,CAAC,MAAmB;AAAA,EAC7C,MAAM,UAAgC;AAAA,IACpC,GAAG,CAAC,GAAG,MAA4B;AAAA,MAEjC,IACE,SAAS,OAAO,eAChB,SAAS,cACT,SAAS,WACT;AAAA,QACA,OAAO,MAAM,KAAK;AAAA,MACpB;AAAA,MAGA,IAAI,OAAO,SAAS,UAAU;AAAA,QAC5B;AAAA,MACF;AAAA,MAGA,IAAI,SAAS,UAAU;AAAA,QACrB,OAAO,MAAM,KAAK;AAAA,MACpB;AAAA,MAGA,OAAO,mBAAmB,GAAG,QAAQ,MAAM;AAAA;AAAA,IAI7C,GAAG,GAAG;AAAA,MACJ,OAAO;AAAA;AAAA,IAIT,OAAO,GAAG;AAAA,MACR,OAAO,CAAC;AAAA;AAAA,IAGV,wBAAwB,GAAG;AAAA,MACzB,OAAO;AAAA,QACL,cAAc;AAAA,QACd,YAAY;AAAA,MACd;AAAA;AAAA,EAEJ;AAAA,EAEA,OAAO,IAAI,MAAM,CAAC,GAAU,OAAO;AAAA;AAoB9B,IAAM,QAAa,mBAAmB,OAAO;AAgB7C,IAAM,OAAY,mBAAmB,MAAM;AAgB3C,IAAM,QAAa;AAAA,GACvB,OAAO,cAAc,MAAM;AAAA,EAC5B,UAAU,MAAM;AAAA,EAChB,SAAS,MAAM;AAAA,EACf,QAAQ,MAAM;AAChB;AAoCO,SAAS,KAAK,CACnB,YACG,aACK;AAAA,EACR,IAAI,SAAS,QAAQ;AAAA,EAErB,SAAS,IAAI,EAAG,IAAI,YAAY,QAAQ,KAAK;AAAA,IAC3C,MAAM,OAAO,YAAY;AAAA,IAIzB,UAAU,OAAO,IAAI;AAAA,IACrB,UAAU,QAAQ,IAAI;AAAA,EACxB;AAAA,EAEA,OAAO,OAAQ,KAAK;AAAA;",
8
+ "debugId": "E5F0C092159197CD64756E2164756E21",
9
9
  "names": []
10
10
  }
@@ -723,6 +723,7 @@ class HypenAppBuilder {
723
723
  template;
724
724
  _registry;
725
725
  dataSourceEntries = [];
726
+ _stateStore;
726
727
  constructor(initialState, options, registry) {
727
728
  this.initialState = initialState;
728
729
  this.options = options || {};
@@ -756,6 +757,10 @@ class HypenAppBuilder {
756
757
  this.errorHandler = fn;
757
758
  return this;
758
759
  }
760
+ persist(store) {
761
+ this._stateStore = store;
762
+ return this;
763
+ }
759
764
  useDataSource(plugin, config2) {
760
765
  this.dataSourceEntries.push({ plugin, config: config2 });
761
766
  return this;
@@ -780,6 +785,7 @@ class HypenAppBuilder {
780
785
  initialState: this.initialState,
781
786
  template: this.template,
782
787
  dataSources: this.dataSourceEntries.length > 0 ? this.dataSourceEntries : undefined,
788
+ stateStore: this._stateStore,
783
789
  handlers: {
784
790
  onCreated: this.createdHandler,
785
791
  onAction: this.actionHandlers,
@@ -844,22 +850,33 @@ class HypenModuleInstance {
844
850
  stateChangeCallbacks = [];
845
851
  dataSourceManager;
846
852
  dataSourceAccessor = {};
847
- constructor(engine, definition, router, globalContext) {
853
+ stateStore;
854
+ currentPersistKey = null;
855
+ persistDebounceTimer;
856
+ sessionId;
857
+ moduleKey = "";
858
+ constructor(engine, definition, router, globalContext, sessionId) {
848
859
  this.engine = engine;
849
860
  this.definition = definition;
850
861
  this.router = router ?? null;
851
862
  this.globalContext = globalContext;
852
- const statePrefix = (definition.name || "").toLowerCase();
863
+ this.sessionId = sessionId ?? crypto.randomUUID();
864
+ this.stateStore = definition.stateStore;
865
+ const moduleKey = (definition.name || "").toLowerCase();
866
+ this.moduleKey = moduleKey;
853
867
  this.state = createObservableState(definition.initialState, {
854
868
  onChange: (change) => {
855
- this.engine.notifyStateChange(change.paths, change.newValues);
869
+ this.engine.updateStateSparse(moduleKey || null, change.paths, change.newValues);
856
870
  this.stateChangeCallbacks.forEach((cb) => cb());
857
- },
858
- pathPrefix: statePrefix || undefined
871
+ this.persistIfNeeded();
872
+ }
859
873
  });
860
874
  const snapshot = getStateSnapshot(this.state);
861
- const prefixedState = statePrefix ? { [statePrefix]: snapshot } : snapshot;
862
- this.engine.setModule(definition.name || "AnonymousModule", definition.actions, definition.stateKeys, prefixedState);
875
+ if (moduleKey) {
876
+ this.engine.registerModule(moduleKey, definition.actions, definition.stateKeys, snapshot);
877
+ } else {
878
+ this.engine.setModule("AnonymousModule", definition.actions, definition.stateKeys, snapshot);
879
+ }
863
880
  for (const [actionName, handler] of definition.handlers.onAction) {
864
881
  log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
865
882
  this.engine.onAction(actionName, async (action) => {
@@ -969,7 +986,54 @@ class HypenModuleInstance {
969
986
  log2.error(`${context.actionName ? `Action "${context.actionName}"` : `Lifecycle "${context.lifecycle}"`} error:`, error);
970
987
  return false;
971
988
  }
989
+ persistIfNeeded() {
990
+ if (!this.stateStore)
991
+ return;
992
+ const store = this.stateStore;
993
+ const newKey = store.resolveKey(this.state, this.definition.name || "AnonymousModule", this.sessionId);
994
+ if (newKey && !this.currentPersistKey) {
995
+ this.activatePersistence(newKey);
996
+ return;
997
+ }
998
+ if (!newKey && this.currentPersistKey) {
999
+ this.currentPersistKey = null;
1000
+ return;
1001
+ }
1002
+ if (newKey && newKey !== this.currentPersistKey) {
1003
+ this.activatePersistence(newKey);
1004
+ return;
1005
+ }
1006
+ if (!this.currentPersistKey)
1007
+ return;
1008
+ clearTimeout(this.persistDebounceTimer);
1009
+ this.persistDebounceTimer = setTimeout(() => {
1010
+ const snapshot = getStateSnapshot(this.state);
1011
+ store.save(this.currentPersistKey, snapshot);
1012
+ }, 50);
1013
+ }
1014
+ async activatePersistence(key) {
1015
+ this.currentPersistKey = key;
1016
+ const stored = await this.stateStore.load(key);
1017
+ if (stored) {
1018
+ const merged = { ...this.definition.initialState, ...stored };
1019
+ Object.assign(this.state, merged);
1020
+ } else {
1021
+ const snapshot = getStateSnapshot(this.state);
1022
+ await this.stateStore.save(key, snapshot);
1023
+ }
1024
+ }
972
1025
  async callCreatedHandler() {
1026
+ if (this.stateStore) {
1027
+ const key = this.stateStore.resolveKey(this.state, this.definition.name || "AnonymousModule", this.sessionId);
1028
+ if (key) {
1029
+ this.currentPersistKey = key;
1030
+ const stored = await this.stateStore.load(key);
1031
+ if (stored) {
1032
+ const merged = { ...this.definition.initialState, ...stored };
1033
+ Object.assign(this.state, merged);
1034
+ }
1035
+ }
1036
+ }
973
1037
  if (this.definition.dataSources?.length) {
974
1038
  const dsEngine = this.engine;
975
1039
  this.dataSourceManager = new DataSourceManager(dsEngine);
@@ -1008,6 +1072,11 @@ class HypenModuleInstance {
1008
1072
  async destroy() {
1009
1073
  if (this.isDestroyed)
1010
1074
  return;
1075
+ if (this.currentPersistKey && this.stateStore) {
1076
+ clearTimeout(this.persistDebounceTimer);
1077
+ const snapshot = getStateSnapshot(this.state);
1078
+ await this.stateStore.save(this.currentPersistKey, snapshot);
1079
+ }
1011
1080
  if (this.dataSourceManager) {
1012
1081
  try {
1013
1082
  await this.dataSourceManager.disconnectAll();
@@ -1336,11 +1405,13 @@ class HypenRouter {
1336
1405
  } else {
1337
1406
  window.history.pushState(null, "", url);
1338
1407
  }
1339
- const hashChangeEvent = new HashChangeEvent("hashchange", {
1340
- oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
1341
- newURL: window.location.href
1342
- });
1343
- window.dispatchEvent(hashChangeEvent);
1408
+ try {
1409
+ const hashChangeEvent = new HashChangeEvent("hashchange", {
1410
+ oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
1411
+ newURL: window.location.href
1412
+ });
1413
+ window.dispatchEvent(hashChangeEvent);
1414
+ } catch {}
1344
1415
  }
1345
1416
  } finally {
1346
1417
  this.isUpdating = false;
@@ -1812,7 +1883,8 @@ class RemoteEngine {
1812
1883
  const hello = {
1813
1884
  type: "hello",
1814
1885
  sessionId: this.currentSessionId ?? this.sessionOptions?.id,
1815
- props: this.sessionOptions?.props
1886
+ props: this.sessionOptions?.props,
1887
+ persistKey: this.sessionOptions?.persistKey
1816
1888
  };
1817
1889
  this.ws.send(JSON.stringify(hello));
1818
1890
  }
@@ -2082,4 +2154,4 @@ export {
2082
2154
  BaseRenderer
2083
2155
  };
2084
2156
 
2085
- //# debugId=1B989D60F3E5B0FC64756E2164756E21
2157
+ //# debugId=DD07DC2DABBCE3FD64756E2164756E21