@hypen-space/core 0.4.46 → 0.4.82
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 +8 -9
- package/dist/app.d.ts +56 -6
- package/dist/app.js +77 -8
- package/dist/app.js.map +4 -4
- package/dist/components/builtin.js +77 -8
- package/dist/components/builtin.js.map +4 -4
- package/dist/datasource.d.ts +4 -4
- package/dist/datasource.js.map +1 -1
- package/dist/engine-base.d.ts +193 -0
- package/dist/engine-base.js +540 -0
- package/dist/engine-base.js.map +12 -0
- package/dist/hypen.d.ts +24 -19
- package/dist/hypen.js +7 -7
- package/dist/hypen.js.map +3 -3
- package/dist/index.browser.js +86 -14
- package/dist/index.browser.js.map +6 -6
- package/dist/index.d.ts +3 -2
- package/dist/index.js +92 -20
- package/dist/index.js.map +7 -7
- package/dist/persistence.d.ts +21 -0
- package/dist/remote/client.d.ts +2 -0
- package/dist/remote/client.js +3 -2
- package/dist/remote/client.js.map +3 -3
- package/dist/remote/index.js +3 -2
- package/dist/remote/index.js.map +3 -3
- package/dist/remote/types.d.ts +2 -0
- package/dist/router.js +8 -6
- package/dist/router.js.map +3 -3
- package/package.json +7 -1
- package/src/app.ts +177 -29
- package/src/datasource.ts +4 -4
- package/src/engine-base.ts +358 -0
- package/src/hypen.ts +34 -28
- package/src/index.ts +8 -2
- package/src/persistence.ts +25 -0
- package/src/remote/client.ts +3 -0
- package/src/remote/types.ts +2 -0
- package/src/router.ts +13 -6
- package/src/types.ts +1 -0
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Hypen lets you write UI templates in a clean, declarative syntax:
|
|
|
8
8
|
|
|
9
9
|
```hypen
|
|
10
10
|
Column {
|
|
11
|
-
Text("Hello,
|
|
11
|
+
Text("Hello, @{state.user.name}!")
|
|
12
12
|
Button(onClick: @actions.logout) {
|
|
13
13
|
Text("Sign Out")
|
|
14
14
|
}
|
|
@@ -18,7 +18,7 @@ Column {
|
|
|
18
18
|
The `@hypen-space/core` package provides the engine that:
|
|
19
19
|
|
|
20
20
|
1. **Parses** your Hypen templates
|
|
21
|
-
2. **Tracks** reactive state bindings (like
|
|
21
|
+
2. **Tracks** reactive state bindings (like `@{state.user.name}`)
|
|
22
22
|
3. **Generates patches** when state changes (instead of re-rendering everything)
|
|
23
23
|
4. **Dispatches actions** triggered from the UI (like `@actions.logout`)
|
|
24
24
|
|
|
@@ -44,11 +44,11 @@ Hypen templates describe your UI structure. Components can have:
|
|
|
44
44
|
|
|
45
45
|
### State Bindings
|
|
46
46
|
|
|
47
|
-
Use
|
|
47
|
+
Use `@{state.path}` to bind template values to your module's state:
|
|
48
48
|
|
|
49
49
|
```hypen
|
|
50
|
-
Text("Count:
|
|
51
|
-
Text("User:
|
|
50
|
+
Text("Count: @{state.count}")
|
|
51
|
+
Text("User: @{state.user.name}")
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
When state changes, only the affected parts of the UI update.
|
|
@@ -101,7 +101,7 @@ engine.setModule("counter", counter.actions, counter.stateKeys, counter.initialS
|
|
|
101
101
|
|
|
102
102
|
engine.renderSource(`
|
|
103
103
|
Column {
|
|
104
|
-
Text("Count:
|
|
104
|
+
Text("Count: @{state.count}")
|
|
105
105
|
Row {
|
|
106
106
|
Button(onClick: @actions.decrement) { Text("-") }
|
|
107
107
|
Button(onClick: @actions.increment) { Text("+") }
|
|
@@ -358,8 +358,8 @@ export default app
|
|
|
358
358
|
.defineState({ items: ["A", "B", "C"] })
|
|
359
359
|
.ui(hypen`
|
|
360
360
|
Column {
|
|
361
|
-
ForEach(items:
|
|
362
|
-
Text("
|
|
361
|
+
ForEach(items: @{state.items}) {
|
|
362
|
+
Text("@{index}: @{item}")
|
|
363
363
|
}
|
|
364
364
|
}
|
|
365
365
|
`);
|
|
@@ -526,7 +526,6 @@ events.emit("userLogin", { userId: "123" });
|
|
|
526
526
|
| `@hypen-space/core/components` | Built-in Router, Route, Link |
|
|
527
527
|
| `@hypen-space/core/result` | Result type for error handling |
|
|
528
528
|
| `@hypen-space/core/disposable` | Disposable pattern utilities |
|
|
529
|
-
| `@hypen-space/core/retry` | Retry with backoff |
|
|
530
529
|
| `@hypen-space/core/logger` | Structured logging |
|
|
531
530
|
| `@hypen-space/core/events` | Typed event emitter |
|
|
532
531
|
| `@hypen-space/server` | Node.js WASM engine, loader, discovery, RemoteServer |
|
package/dist/app.d.ts
CHANGED
|
@@ -6,10 +6,21 @@ import type { Action } from "./types.js";
|
|
|
6
6
|
import type { Session } from "./remote/types.js";
|
|
7
7
|
import { HypenError } from "./result.js";
|
|
8
8
|
import { type DataSourcePlugin } from "./datasource.js";
|
|
9
|
+
import type { StateStore } from "./persistence.js";
|
|
9
10
|
export interface IEngine {
|
|
10
11
|
setModule(name: string, actions: string[], stateKeys: string[], initialState: unknown): void;
|
|
12
|
+
registerModule(name: string, actions: string[], stateKeys: string[], initialState: unknown): void;
|
|
11
13
|
onAction(actionName: string, handler: (action: Action) => void | Promise<void>): void;
|
|
12
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Apply a sparse state update.
|
|
16
|
+
*
|
|
17
|
+
* @param scope Lowercase module name to target a named module registered
|
|
18
|
+
* via `registerModule`. Pass `null` (or empty string) to
|
|
19
|
+
* target the primary module set via `setModule`.
|
|
20
|
+
* @param paths Changed state paths (relative to the targeted module).
|
|
21
|
+
* @param values Map of `path -> new value`.
|
|
22
|
+
*/
|
|
23
|
+
updateStateSparse(scope: string | null, paths: string[], values: Record<string, unknown>): void;
|
|
13
24
|
}
|
|
14
25
|
import type { HypenRouter } from "./router.js";
|
|
15
26
|
import type { HypenGlobalContext, ModuleReference } from "./context.js";
|
|
@@ -137,6 +148,11 @@ export interface HypenModuleDefinition<T = unknown> {
|
|
|
137
148
|
plugin: DataSourcePlugin;
|
|
138
149
|
config: unknown;
|
|
139
150
|
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Pluggable state store for persistence across sessions.
|
|
153
|
+
* Set via the `.persist(store)` builder method.
|
|
154
|
+
*/
|
|
155
|
+
stateStore?: StateStore<T>;
|
|
140
156
|
handlers: {
|
|
141
157
|
onCreated?: LifecycleHandler<T>;
|
|
142
158
|
onAction: Map<string, ActionHandler<T, any>>;
|
|
@@ -171,6 +187,7 @@ export declare class HypenAppBuilder<T> {
|
|
|
171
187
|
private template?;
|
|
172
188
|
private _registry?;
|
|
173
189
|
private dataSourceEntries;
|
|
190
|
+
private _stateStore?;
|
|
174
191
|
constructor(initialState: T, options?: {
|
|
175
192
|
persist?: boolean;
|
|
176
193
|
version?: number;
|
|
@@ -248,6 +265,21 @@ export declare class HypenAppBuilder<T> {
|
|
|
248
265
|
* @returns Builder for chaining
|
|
249
266
|
*/
|
|
250
267
|
onError(fn: ErrorHandler<T>): this;
|
|
268
|
+
/**
|
|
269
|
+
* Attach a persistent state store.
|
|
270
|
+
* The store handles save/restore lifecycle automatically.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* import { durableObjectStore, withKey } from "@hypen-space/cf";
|
|
275
|
+
*
|
|
276
|
+
* app
|
|
277
|
+
* .defineState({ user: null, todos: [] }, { name: "TodoList" })
|
|
278
|
+
* .persist(durableObjectStore(withKey(state => state.user?.id)))
|
|
279
|
+
* .build();
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
persist(store: StateStore<T>): this;
|
|
251
283
|
/**
|
|
252
284
|
* Register a data source plugin for live database subscriptions.
|
|
253
285
|
*
|
|
@@ -269,10 +301,10 @@ export declare class HypenAppBuilder<T> {
|
|
|
269
301
|
* .build();
|
|
270
302
|
* ```
|
|
271
303
|
*
|
|
272
|
-
* In Hypen DSL, bind to data source tables with
|
|
304
|
+
* In Hypen DSL, bind to data source tables with `@provider.table`:
|
|
273
305
|
* ```hypen
|
|
274
|
-
* ForEach(items:
|
|
275
|
-
* Text("
|
|
306
|
+
* ForEach(items: @spacetime.message, key: "id") {
|
|
307
|
+
* Text("@{item.text}")
|
|
276
308
|
* }
|
|
277
309
|
* ```
|
|
278
310
|
*/
|
|
@@ -293,7 +325,7 @@ export declare class HypenAppBuilder<T> {
|
|
|
293
325
|
* })
|
|
294
326
|
* .ui(hypen`
|
|
295
327
|
* Column {
|
|
296
|
-
* Text("Count:
|
|
328
|
+
* Text("Count: @{state.count}")
|
|
297
329
|
* Button { Text("+") }
|
|
298
330
|
* .onClick("@actions.increment")
|
|
299
331
|
* }
|
|
@@ -407,7 +439,16 @@ export declare class HypenModuleInstance<T extends object = any> {
|
|
|
407
439
|
private stateChangeCallbacks;
|
|
408
440
|
private dataSourceManager?;
|
|
409
441
|
private dataSourceAccessor;
|
|
410
|
-
|
|
442
|
+
private stateStore?;
|
|
443
|
+
private currentPersistKey;
|
|
444
|
+
private persistDebounceTimer;
|
|
445
|
+
private sessionId;
|
|
446
|
+
/**
|
|
447
|
+
* Lowercase name used to address this module in the engine. Empty string
|
|
448
|
+
* for anonymous modules (which back the engine's primary module slot).
|
|
449
|
+
*/
|
|
450
|
+
private moduleKey;
|
|
451
|
+
constructor(engine: IEngine, definition: HypenModuleDefinition<T>, router?: HypenRouter | null, globalContext?: HypenGlobalContext, sessionId?: string);
|
|
411
452
|
/** Promise that resolves when onCreated handler has completed */
|
|
412
453
|
private _readyPromise;
|
|
413
454
|
/**
|
|
@@ -430,6 +471,15 @@ export declare class HypenModuleInstance<T extends object = any> {
|
|
|
430
471
|
* @returns true if the error should be rethrown
|
|
431
472
|
*/
|
|
432
473
|
private handleError;
|
|
474
|
+
/**
|
|
475
|
+
* Re-evaluate the persistence key and debounce saves on state mutation.
|
|
476
|
+
* Handles key transitions (null->value, value->null, value->different value).
|
|
477
|
+
*/
|
|
478
|
+
private persistIfNeeded;
|
|
479
|
+
/**
|
|
480
|
+
* Activate persistence for the given key: load stored state and merge.
|
|
481
|
+
*/
|
|
482
|
+
private activatePersistence;
|
|
433
483
|
/**
|
|
434
484
|
* Call the onCreated handler and connect data source plugins
|
|
435
485
|
*/
|
package/dist/app.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
869
|
+
this.engine.updateStateSparse(moduleKey || null, change.paths, change.newValues);
|
|
856
870
|
this.stateChangeCallbacks.forEach((cb) => cb());
|
|
857
|
-
|
|
858
|
-
|
|
871
|
+
this.persistIfNeeded();
|
|
872
|
+
}
|
|
859
873
|
});
|
|
860
874
|
const snapshot = getStateSnapshot(this.state);
|
|
861
|
-
|
|
862
|
-
|
|
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();
|
|
@@ -1047,4 +1116,4 @@ export {
|
|
|
1047
1116
|
HypenApp
|
|
1048
1117
|
};
|
|
1049
1118
|
|
|
1050
|
-
//# debugId=
|
|
1119
|
+
//# debugId=B8AA1FCB3E096F2264756E2164756E21
|