@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/index.d.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * })
17
17
  * .ui(hypen`
18
18
  * Column {
19
- * Text("Count: ${state.count}")
19
+ * Text("Count: @{state.count}")
20
20
  * Button { Text("+") }
21
21
  * .onClick("@actions.increment")
22
22
  * }
@@ -37,7 +37,7 @@
37
37
  * .build();
38
38
  *
39
39
  * // component.hypen (separate file)
40
- * // Column { Text("Count: ${state.count}") ... }
40
+ * // Column { Text("Count: @{state.count}") ... }
41
41
  * ```
42
42
  */
43
43
  export type { Patch, Action, RenderCallback, ActionHandler as EngineActionHandler, ResolvedComponent, ComponentResolver as EngineComponentResolver, } from "./types.js";
@@ -45,6 +45,7 @@ export { app, HypenApp, HypenAppBuilder, HypenModuleInstance } from "./app.js";
45
45
  export { hypen, state, item, index } from "./hypen.js";
46
46
  export type { StateProxy, ItemProxy } from "./hypen.js";
47
47
  export type { IEngine, ActionContext, GlobalContext, LifecycleHandler, ActionHandlerContext, ActionHandler, HypenModuleDefinition, HypenModule, DisconnectContext, ReconnectContext, ExpireContext, DisconnectHandler, ReconnectHandler, ExpireHandler, ErrorContext, ErrorHandler, ErrorHandlerResult, DataSourceAccessor, } from "./app.js";
48
+ export type { StateStore } from "./persistence.js";
48
49
  export { DataSourceManager } from "./datasource.js";
49
50
  export type { DataSourcePlugin, DataSourceStatus, DataSourceQuery, DataSourceSubscription, DataSourceChange, IDataSourceEngine, } from "./datasource.js";
50
51
  export { createObservableState, batchStateUpdates, getStateSnapshot, isStateProxy, unwrapProxy, } from "./state.js";
package/dist/index.js CHANGED
@@ -1063,6 +1063,7 @@ class HypenAppBuilder {
1063
1063
  template;
1064
1064
  _registry;
1065
1065
  dataSourceEntries = [];
1066
+ _stateStore;
1066
1067
  constructor(initialState, options, registry) {
1067
1068
  this.initialState = initialState;
1068
1069
  this.options = options || {};
@@ -1096,6 +1097,10 @@ class HypenAppBuilder {
1096
1097
  this.errorHandler = fn;
1097
1098
  return this;
1098
1099
  }
1100
+ persist(store) {
1101
+ this._stateStore = store;
1102
+ return this;
1103
+ }
1099
1104
  useDataSource(plugin, config2) {
1100
1105
  this.dataSourceEntries.push({ plugin, config: config2 });
1101
1106
  return this;
@@ -1120,6 +1125,7 @@ class HypenAppBuilder {
1120
1125
  initialState: this.initialState,
1121
1126
  template: this.template,
1122
1127
  dataSources: this.dataSourceEntries.length > 0 ? this.dataSourceEntries : undefined,
1128
+ stateStore: this._stateStore,
1123
1129
  handlers: {
1124
1130
  onCreated: this.createdHandler,
1125
1131
  onAction: this.actionHandlers,
@@ -1184,22 +1190,33 @@ class HypenModuleInstance {
1184
1190
  stateChangeCallbacks = [];
1185
1191
  dataSourceManager;
1186
1192
  dataSourceAccessor = {};
1187
- constructor(engine, definition, router, globalContext) {
1193
+ stateStore;
1194
+ currentPersistKey = null;
1195
+ persistDebounceTimer;
1196
+ sessionId;
1197
+ moduleKey = "";
1198
+ constructor(engine, definition, router, globalContext, sessionId) {
1188
1199
  this.engine = engine;
1189
1200
  this.definition = definition;
1190
1201
  this.router = router ?? null;
1191
1202
  this.globalContext = globalContext;
1192
- const statePrefix = (definition.name || "").toLowerCase();
1203
+ this.sessionId = sessionId ?? crypto.randomUUID();
1204
+ this.stateStore = definition.stateStore;
1205
+ const moduleKey = (definition.name || "").toLowerCase();
1206
+ this.moduleKey = moduleKey;
1193
1207
  this.state = createObservableState(definition.initialState, {
1194
1208
  onChange: (change) => {
1195
- this.engine.notifyStateChange(change.paths, change.newValues);
1209
+ this.engine.updateStateSparse(moduleKey || null, change.paths, change.newValues);
1196
1210
  this.stateChangeCallbacks.forEach((cb) => cb());
1197
- },
1198
- pathPrefix: statePrefix || undefined
1211
+ this.persistIfNeeded();
1212
+ }
1199
1213
  });
1200
1214
  const snapshot = getStateSnapshot(this.state);
1201
- const prefixedState = statePrefix ? { [statePrefix]: snapshot } : snapshot;
1202
- this.engine.setModule(definition.name || "AnonymousModule", definition.actions, definition.stateKeys, prefixedState);
1215
+ if (moduleKey) {
1216
+ this.engine.registerModule(moduleKey, definition.actions, definition.stateKeys, snapshot);
1217
+ } else {
1218
+ this.engine.setModule("AnonymousModule", definition.actions, definition.stateKeys, snapshot);
1219
+ }
1203
1220
  for (const [actionName, handler] of definition.handlers.onAction) {
1204
1221
  log2.debug(`Registering action handler: ${actionName} for module ${definition.name}`);
1205
1222
  this.engine.onAction(actionName, async (action) => {
@@ -1309,7 +1326,54 @@ class HypenModuleInstance {
1309
1326
  log2.error(`${context.actionName ? `Action "${context.actionName}"` : `Lifecycle "${context.lifecycle}"`} error:`, error);
1310
1327
  return false;
1311
1328
  }
1329
+ persistIfNeeded() {
1330
+ if (!this.stateStore)
1331
+ return;
1332
+ const store = this.stateStore;
1333
+ const newKey = store.resolveKey(this.state, this.definition.name || "AnonymousModule", this.sessionId);
1334
+ if (newKey && !this.currentPersistKey) {
1335
+ this.activatePersistence(newKey);
1336
+ return;
1337
+ }
1338
+ if (!newKey && this.currentPersistKey) {
1339
+ this.currentPersistKey = null;
1340
+ return;
1341
+ }
1342
+ if (newKey && newKey !== this.currentPersistKey) {
1343
+ this.activatePersistence(newKey);
1344
+ return;
1345
+ }
1346
+ if (!this.currentPersistKey)
1347
+ return;
1348
+ clearTimeout(this.persistDebounceTimer);
1349
+ this.persistDebounceTimer = setTimeout(() => {
1350
+ const snapshot = getStateSnapshot(this.state);
1351
+ store.save(this.currentPersistKey, snapshot);
1352
+ }, 50);
1353
+ }
1354
+ async activatePersistence(key) {
1355
+ this.currentPersistKey = key;
1356
+ const stored = await this.stateStore.load(key);
1357
+ if (stored) {
1358
+ const merged = { ...this.definition.initialState, ...stored };
1359
+ Object.assign(this.state, merged);
1360
+ } else {
1361
+ const snapshot = getStateSnapshot(this.state);
1362
+ await this.stateStore.save(key, snapshot);
1363
+ }
1364
+ }
1312
1365
  async callCreatedHandler() {
1366
+ if (this.stateStore) {
1367
+ const key = this.stateStore.resolveKey(this.state, this.definition.name || "AnonymousModule", this.sessionId);
1368
+ if (key) {
1369
+ this.currentPersistKey = key;
1370
+ const stored = await this.stateStore.load(key);
1371
+ if (stored) {
1372
+ const merged = { ...this.definition.initialState, ...stored };
1373
+ Object.assign(this.state, merged);
1374
+ }
1375
+ }
1376
+ }
1313
1377
  if (this.definition.dataSources?.length) {
1314
1378
  const dsEngine = this.engine;
1315
1379
  this.dataSourceManager = new DataSourceManager(dsEngine);
@@ -1348,6 +1412,11 @@ class HypenModuleInstance {
1348
1412
  async destroy() {
1349
1413
  if (this.isDestroyed)
1350
1414
  return;
1415
+ if (this.currentPersistKey && this.stateStore) {
1416
+ clearTimeout(this.persistDebounceTimer);
1417
+ const snapshot = getStateSnapshot(this.state);
1418
+ await this.stateStore.save(this.currentPersistKey, snapshot);
1419
+ }
1351
1420
  if (this.dataSourceManager) {
1352
1421
  try {
1353
1422
  await this.dataSourceManager.disconnectAll();
@@ -1386,13 +1455,13 @@ function createBindingProxy(root) {
1386
1455
  const handler = {
1387
1456
  get(_, prop) {
1388
1457
  if (prop === Symbol.toPrimitive || prop === "toString" || prop === "valueOf") {
1389
- return () => `\${${root}}`;
1458
+ return () => `@{${root}}`;
1390
1459
  }
1391
1460
  if (typeof prop === "symbol") {
1392
1461
  return;
1393
1462
  }
1394
1463
  if (prop === "toJSON") {
1395
- return () => `\${${root}}`;
1464
+ return () => `@{${root}}`;
1396
1465
  }
1397
1466
  return createBindingProxy(`${root}.${prop}`);
1398
1467
  },
@@ -1414,10 +1483,10 @@ function createBindingProxy(root) {
1414
1483
  var state = createBindingProxy("state");
1415
1484
  var item = createBindingProxy("item");
1416
1485
  var index = {
1417
- [Symbol.toPrimitive]: () => "${index}",
1418
- toString: () => "${index}",
1419
- valueOf: () => "${index}",
1420
- toJSON: () => "${index}"
1486
+ [Symbol.toPrimitive]: () => "@{index}",
1487
+ toString: () => "@{index}",
1488
+ valueOf: () => "@{index}",
1489
+ toJSON: () => "@{index}"
1421
1490
  };
1422
1491
  function hypen(strings, ...expressions) {
1423
1492
  let result = strings[0];
@@ -1724,11 +1793,13 @@ class HypenRouter {
1724
1793
  } else {
1725
1794
  window.history.pushState(null, "", url);
1726
1795
  }
1727
- const hashChangeEvent = new HashChangeEvent("hashchange", {
1728
- oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
1729
- newURL: window.location.href
1730
- });
1731
- window.dispatchEvent(hashChangeEvent);
1796
+ try {
1797
+ const hashChangeEvent = new HashChangeEvent("hashchange", {
1798
+ oldURL: window.location.href.replace(window.location.hash, "#" + oldPath),
1799
+ newURL: window.location.href
1800
+ });
1801
+ window.dispatchEvent(hashChangeEvent);
1802
+ } catch {}
1732
1803
  }
1733
1804
  } finally {
1734
1805
  this.isUpdating = false;
@@ -2200,7 +2271,8 @@ class RemoteEngine {
2200
2271
  const hello = {
2201
2272
  type: "hello",
2202
2273
  sessionId: this.currentSessionId ?? this.sessionOptions?.id,
2203
- props: this.sessionOptions?.props
2274
+ props: this.sessionOptions?.props,
2275
+ persistKey: this.sessionOptions?.persistKey
2204
2276
  };
2205
2277
  this.ws.send(JSON.stringify(hello));
2206
2278
  }
@@ -2838,4 +2910,4 @@ export {
2838
2910
  ActionError
2839
2911
  };
2840
2912
 
2841
- //# debugId=F652589D18AB457B64756E2164756E21
2913
+ //# debugId=ABC8BA06546F591964756E2164756E21