@hypen-space/core 0.4.82 → 0.4.84
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 +10 -4
- package/dist/app.d.ts +78 -1
- package/dist/app.js +102 -46
- package/dist/app.js.map +6 -5
- package/dist/components/builtin.js +102 -46
- package/dist/components/builtin.js.map +6 -5
- package/dist/engine-base.d.ts +19 -0
- package/dist/engine-base.js +32 -1
- package/dist/engine-base.js.map +3 -3
- package/dist/index.browser.js +117 -85
- package/dist/index.browser.js.map +7 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +318 -85
- package/dist/index.js.map +9 -7
- package/dist/managed-router.d.ts +121 -5
- package/dist/portable.d.ts +62 -0
- package/dist/router.d.ts +7 -1
- package/dist/router.js +60 -81
- package/dist/router.js.map +6 -5
- package/dist/state.js +49 -46
- package/dist/state.js.map +5 -4
- package/dist/types.d.ts +12 -1
- package/package.json +7 -1
- package/src/app.ts +131 -1
- package/src/engine-base.ts +55 -0
- package/src/index.ts +13 -0
- package/src/managed-router.ts +285 -35
- package/src/portable.ts +113 -0
- package/src/router.ts +25 -50
- package/src/state.ts +11 -70
- package/src/types.ts +23 -1
package/README.md
CHANGED
|
@@ -122,6 +122,8 @@ interface UserState {
|
|
|
122
122
|
loading: boolean;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
interface LoadUserPayload { id: string; }
|
|
126
|
+
|
|
125
127
|
const userModule = app
|
|
126
128
|
.defineState<UserState>({ user: null, loading: false })
|
|
127
129
|
|
|
@@ -131,15 +133,16 @@ const userModule = app
|
|
|
131
133
|
// State changes are auto-synced via Proxy
|
|
132
134
|
})
|
|
133
135
|
|
|
134
|
-
//
|
|
135
|
-
.
|
|
136
|
-
|
|
137
|
-
state.user = await fetchUser(
|
|
136
|
+
// Typed action handler: declare the payload type as a generic so
|
|
137
|
+
// `action.payload` is statically typed instead of `unknown`.
|
|
138
|
+
.onAction<LoadUserPayload>("loadUser", async ({ state, action, context }) => {
|
|
139
|
+
state.user = await fetchUser(action.payload.id);
|
|
138
140
|
state.loading = false;
|
|
139
141
|
// State changes are auto-synced via Proxy
|
|
140
142
|
// context.router is available for programmatic navigation
|
|
141
143
|
})
|
|
142
144
|
|
|
145
|
+
// Actions without a payload: omit the generic.
|
|
143
146
|
.onAction("logout", ({ state }) => {
|
|
144
147
|
state.user = null;
|
|
145
148
|
})
|
|
@@ -153,6 +156,9 @@ const userModule = app
|
|
|
153
156
|
```
|
|
154
157
|
|
|
155
158
|
State mutations are automatically tracked via Proxy and synced to the engine.
|
|
159
|
+
Action payloads default to `unknown` — always pass a payload type to
|
|
160
|
+
`.onAction<P>(...)` when the action carries data so you get end-to-end
|
|
161
|
+
type safety instead of manual casting.
|
|
156
162
|
|
|
157
163
|
## State
|
|
158
164
|
|
package/dist/app.d.ts
CHANGED
|
@@ -112,7 +112,7 @@ export interface ErrorContext<T> {
|
|
|
112
112
|
/** The action name if error occurred in an action handler */
|
|
113
113
|
actionName?: string;
|
|
114
114
|
/** The lifecycle phase if error occurred in a lifecycle handler */
|
|
115
|
-
lifecycle?: "created" | "destroyed" | "disconnect" | "reconnect" | "expire";
|
|
115
|
+
lifecycle?: "created" | "activated" | "deactivated" | "destroyed" | "disconnect" | "reconnect" | "expire";
|
|
116
116
|
}
|
|
117
117
|
/**
|
|
118
118
|
* Error handler return type - controls error propagation
|
|
@@ -155,6 +155,20 @@ export interface HypenModuleDefinition<T = unknown> {
|
|
|
155
155
|
stateStore?: StateStore<T>;
|
|
156
156
|
handlers: {
|
|
157
157
|
onCreated?: LifecycleHandler<T>;
|
|
158
|
+
/**
|
|
159
|
+
* Called every time the module becomes the active route target.
|
|
160
|
+
* Runs after `onCreated` on first mount, and again on each re-mount
|
|
161
|
+
* from the ManagedRouter's persistence cache. Use for data refresh,
|
|
162
|
+
* subscription (re)connects, or any "screen became visible" work.
|
|
163
|
+
*/
|
|
164
|
+
onActivated?: LifecycleHandler<T>;
|
|
165
|
+
/**
|
|
166
|
+
* Called every time the module stops being the active route target.
|
|
167
|
+
* Runs before the module is cached for persistence OR before
|
|
168
|
+
* `onDestroyed` if the module is being torn down. Use for pausing
|
|
169
|
+
* timers, tearing down ephemeral subscriptions, etc.
|
|
170
|
+
*/
|
|
171
|
+
onDeactivated?: LifecycleHandler<T>;
|
|
158
172
|
onAction: Map<string, ActionHandler<T, any>>;
|
|
159
173
|
onDestroyed?: LifecycleHandler<T>;
|
|
160
174
|
/** Called when client disconnects (session persists for TTL) */
|
|
@@ -178,6 +192,8 @@ export declare class HypenAppBuilder<T> {
|
|
|
178
192
|
private initialState;
|
|
179
193
|
private options;
|
|
180
194
|
private createdHandler?;
|
|
195
|
+
private activatedHandler?;
|
|
196
|
+
private deactivatedHandler?;
|
|
181
197
|
private actionHandlers;
|
|
182
198
|
private destroyedHandler?;
|
|
183
199
|
private disconnectHandler?;
|
|
@@ -215,6 +231,39 @@ export declare class HypenAppBuilder<T> {
|
|
|
215
231
|
* ```
|
|
216
232
|
*/
|
|
217
233
|
onAction<P = unknown>(name: string, fn: ActionHandler<T, P>): this;
|
|
234
|
+
/**
|
|
235
|
+
* Register a handler that runs every time the module becomes the active
|
|
236
|
+
* route target.
|
|
237
|
+
*
|
|
238
|
+
* Unlike `onCreated`, which only runs once per module instance, `onActivated`
|
|
239
|
+
* runs on **every** mount — the first one (right after `onCreated`) and
|
|
240
|
+
* every subsequent re-entry when a cached module is restored by
|
|
241
|
+
* `ManagedRouter`. Use this hook for data refresh, (re)connecting
|
|
242
|
+
* subscriptions, starting timers, and any "screen became visible" work
|
|
243
|
+
* that should happen on every navigation to this route.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```typescript
|
|
247
|
+
* app
|
|
248
|
+
* .defineState({ items: [], loading: false })
|
|
249
|
+
* .onActivated(async (state) => {
|
|
250
|
+
* state.loading = true;
|
|
251
|
+
* state.items = await fetchItems();
|
|
252
|
+
* state.loading = false;
|
|
253
|
+
* });
|
|
254
|
+
* ```
|
|
255
|
+
*/
|
|
256
|
+
onActivated(fn: LifecycleHandler<T>): this;
|
|
257
|
+
/**
|
|
258
|
+
* Register a handler that runs every time the module stops being the
|
|
259
|
+
* active route target.
|
|
260
|
+
*
|
|
261
|
+
* Runs before the module is cached for persistence (when the user
|
|
262
|
+
* navigates to another route) OR before `onDestroyed` if the module is
|
|
263
|
+
* being torn down. Use this for pausing timers, unsubscribing from
|
|
264
|
+
* ephemeral streams, and any "screen became hidden" cleanup.
|
|
265
|
+
*/
|
|
266
|
+
onDeactivated(fn: LifecycleHandler<T>): this;
|
|
218
267
|
/**
|
|
219
268
|
* Register a handler for module destruction
|
|
220
269
|
*/
|
|
@@ -434,6 +483,13 @@ export declare class HypenModuleInstance<T extends object = any> {
|
|
|
434
483
|
private definition;
|
|
435
484
|
private state;
|
|
436
485
|
private isDestroyed;
|
|
486
|
+
/**
|
|
487
|
+
* True when the module is currently the active route target (i.e.
|
|
488
|
+
* `onActivated` has fired more recently than `onDeactivated`).
|
|
489
|
+
* Used to make `activate()` / `deactivate()` idempotent so the
|
|
490
|
+
* `ManagedRouter` can call them safely regardless of state.
|
|
491
|
+
*/
|
|
492
|
+
private isActive;
|
|
437
493
|
private router;
|
|
438
494
|
private globalContext?;
|
|
439
495
|
private stateChangeCallbacks;
|
|
@@ -456,6 +512,27 @@ export declare class HypenModuleInstance<T extends object = any> {
|
|
|
456
512
|
* Call this before renderSource to ensure state is fully initialized.
|
|
457
513
|
*/
|
|
458
514
|
waitForReady(): Promise<void>;
|
|
515
|
+
/**
|
|
516
|
+
* Mark the module as the active route target and fire `onActivated`.
|
|
517
|
+
*
|
|
518
|
+
* Idempotent: calling `activate()` on an already-active module is a
|
|
519
|
+
* no-op. Internally awaits `waitForReady()` so that `onCreated` always
|
|
520
|
+
* runs to completion before `onActivated` fires, regardless of who
|
|
521
|
+
* calls this method and when.
|
|
522
|
+
*
|
|
523
|
+
* Called by `ManagedRouter` on every route mount — both fresh
|
|
524
|
+
* constructions and re-mounts from the persistence cache.
|
|
525
|
+
*/
|
|
526
|
+
activate(): Promise<void>;
|
|
527
|
+
/**
|
|
528
|
+
* Mark the module as no longer the active route target and fire
|
|
529
|
+
* `onDeactivated`.
|
|
530
|
+
*
|
|
531
|
+
* Idempotent: calling `deactivate()` on an inactive module is a no-op.
|
|
532
|
+
* Called by `ManagedRouter` before persisting a module for later reuse
|
|
533
|
+
* OR before destroying it.
|
|
534
|
+
*/
|
|
535
|
+
deactivate(): Promise<void>;
|
|
459
536
|
/**
|
|
460
537
|
* Create the global context API for this module
|
|
461
538
|
*/
|
package/dist/app.js
CHANGED
|
@@ -262,6 +262,53 @@ class DataSourceManager {
|
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// src/portable.ts
|
|
266
|
+
function notInstalled(name) {
|
|
267
|
+
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.`);
|
|
268
|
+
}
|
|
269
|
+
var current = {
|
|
270
|
+
diffState: () => notInstalled("diffState"),
|
|
271
|
+
matchPath: () => notInstalled("matchPath"),
|
|
272
|
+
pathGet: () => notInstalled("pathGet"),
|
|
273
|
+
pathHas: () => notInstalled("pathHas"),
|
|
274
|
+
pathSet: () => notInstalled("pathSet"),
|
|
275
|
+
pathDelete: () => notInstalled("pathDelete"),
|
|
276
|
+
encodeUriComponent: () => notInstalled("encodeUriComponent"),
|
|
277
|
+
decodeUriComponent: () => notInstalled("decodeUriComponent"),
|
|
278
|
+
parseQuery: () => notInstalled("parseQuery"),
|
|
279
|
+
buildUrl: () => notInstalled("buildUrl")
|
|
280
|
+
};
|
|
281
|
+
function setPortableImpl(impl) {
|
|
282
|
+
if (impl === null) {
|
|
283
|
+
current = {
|
|
284
|
+
diffState: () => notInstalled("diffState"),
|
|
285
|
+
matchPath: () => notInstalled("matchPath"),
|
|
286
|
+
pathGet: () => notInstalled("pathGet"),
|
|
287
|
+
pathHas: () => notInstalled("pathHas"),
|
|
288
|
+
pathSet: () => notInstalled("pathSet"),
|
|
289
|
+
pathDelete: () => notInstalled("pathDelete"),
|
|
290
|
+
encodeUriComponent: () => notInstalled("encodeUriComponent"),
|
|
291
|
+
decodeUriComponent: () => notInstalled("decodeUriComponent"),
|
|
292
|
+
parseQuery: () => notInstalled("parseQuery"),
|
|
293
|
+
buildUrl: () => notInstalled("buildUrl")
|
|
294
|
+
};
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
current = impl;
|
|
298
|
+
}
|
|
299
|
+
var portable = {
|
|
300
|
+
diffState: (o, n, b) => current.diffState(o, n, b),
|
|
301
|
+
matchPath: (p, path) => current.matchPath(p, path),
|
|
302
|
+
pathGet: (v, p) => current.pathGet(v, p),
|
|
303
|
+
pathHas: (v, p) => current.pathHas(v, p),
|
|
304
|
+
pathSet: (v, p, nv) => current.pathSet(v, p, nv),
|
|
305
|
+
pathDelete: (v, p) => current.pathDelete(v, p),
|
|
306
|
+
encodeUriComponent: (s) => current.encodeUriComponent(s),
|
|
307
|
+
decodeUriComponent: (s) => current.decodeUriComponent(s),
|
|
308
|
+
parseQuery: (f) => current.parseQuery(f),
|
|
309
|
+
buildUrl: (p, q) => current.buildUrl(p, q)
|
|
310
|
+
};
|
|
311
|
+
|
|
265
312
|
// src/state.ts
|
|
266
313
|
var IS_PROXY = Symbol.for("hypen.isProxy");
|
|
267
314
|
var RAW_TARGET = Symbol.for("hypen.rawTarget");
|
|
@@ -321,51 +368,7 @@ function deepClone(obj) {
|
|
|
321
368
|
return cloneInternal(obj);
|
|
322
369
|
}
|
|
323
370
|
function diffState(oldState, newState, basePath = "") {
|
|
324
|
-
|
|
325
|
-
const newValues = {};
|
|
326
|
-
function diff(oldVal, newVal, path) {
|
|
327
|
-
if (oldVal === newVal)
|
|
328
|
-
return;
|
|
329
|
-
if (typeof oldVal !== "object" || typeof newVal !== "object" || oldVal === null || newVal === null) {
|
|
330
|
-
if (oldVal !== newVal) {
|
|
331
|
-
paths.push(path);
|
|
332
|
-
newValues[path] = newVal;
|
|
333
|
-
}
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (Array.isArray(oldVal) || Array.isArray(newVal)) {
|
|
337
|
-
if (!Array.isArray(oldVal) || !Array.isArray(newVal) || oldVal.length !== newVal.length) {
|
|
338
|
-
paths.push(path);
|
|
339
|
-
newValues[path] = newVal;
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
for (let i = 0;i < newVal.length; i++) {
|
|
343
|
-
const itemPath = path ? `${path}.${i}` : `${i}`;
|
|
344
|
-
diff(oldVal[i], newVal[i], itemPath);
|
|
345
|
-
}
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const oldKeys = new Set(Object.keys(oldVal));
|
|
349
|
-
const newKeys = new Set(Object.keys(newVal));
|
|
350
|
-
for (const key of newKeys) {
|
|
351
|
-
const propPath = path ? `${path}.${key}` : key;
|
|
352
|
-
if (!oldKeys.has(key)) {
|
|
353
|
-
paths.push(propPath);
|
|
354
|
-
newValues[propPath] = newVal[key];
|
|
355
|
-
} else {
|
|
356
|
-
diff(oldVal[key], newVal[key], propPath);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
for (const key of oldKeys) {
|
|
360
|
-
if (!newKeys.has(key)) {
|
|
361
|
-
const propPath = path ? `${path}.${key}` : key;
|
|
362
|
-
paths.push(propPath);
|
|
363
|
-
newValues[propPath] = undefined;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
diff(oldState, newState, basePath);
|
|
368
|
-
return { paths, newValues };
|
|
371
|
+
return portable.diffState(oldState, newState, basePath);
|
|
369
372
|
}
|
|
370
373
|
function createObservableState(initialState, options) {
|
|
371
374
|
const opts = options || { onChange: () => {} };
|
|
@@ -714,6 +717,8 @@ class HypenAppBuilder {
|
|
|
714
717
|
initialState;
|
|
715
718
|
options;
|
|
716
719
|
createdHandler;
|
|
720
|
+
activatedHandler;
|
|
721
|
+
deactivatedHandler;
|
|
717
722
|
actionHandlers = new Map;
|
|
718
723
|
destroyedHandler;
|
|
719
724
|
disconnectHandler;
|
|
@@ -737,6 +742,14 @@ class HypenAppBuilder {
|
|
|
737
742
|
this.actionHandlers.set(name, fn);
|
|
738
743
|
return this;
|
|
739
744
|
}
|
|
745
|
+
onActivated(fn) {
|
|
746
|
+
this.activatedHandler = fn;
|
|
747
|
+
return this;
|
|
748
|
+
}
|
|
749
|
+
onDeactivated(fn) {
|
|
750
|
+
this.deactivatedHandler = fn;
|
|
751
|
+
return this;
|
|
752
|
+
}
|
|
740
753
|
onDestroyed(fn) {
|
|
741
754
|
this.destroyedHandler = fn;
|
|
742
755
|
return this;
|
|
@@ -788,6 +801,8 @@ class HypenAppBuilder {
|
|
|
788
801
|
stateStore: this._stateStore,
|
|
789
802
|
handlers: {
|
|
790
803
|
onCreated: this.createdHandler,
|
|
804
|
+
onActivated: this.activatedHandler,
|
|
805
|
+
onDeactivated: this.deactivatedHandler,
|
|
791
806
|
onAction: this.actionHandlers,
|
|
792
807
|
onDestroyed: this.destroyedHandler,
|
|
793
808
|
onDisconnect: this.disconnectHandler,
|
|
@@ -845,6 +860,7 @@ class HypenModuleInstance {
|
|
|
845
860
|
definition;
|
|
846
861
|
state;
|
|
847
862
|
isDestroyed = false;
|
|
863
|
+
isActive = false;
|
|
848
864
|
router;
|
|
849
865
|
globalContext;
|
|
850
866
|
stateChangeCallbacks = [];
|
|
@@ -924,6 +940,43 @@ class HypenModuleInstance {
|
|
|
924
940
|
async waitForReady() {
|
|
925
941
|
await this._readyPromise;
|
|
926
942
|
}
|
|
943
|
+
async activate() {
|
|
944
|
+
if (this.isDestroyed || this.isActive)
|
|
945
|
+
return;
|
|
946
|
+
await this._readyPromise;
|
|
947
|
+
if (this.isDestroyed || this.isActive)
|
|
948
|
+
return;
|
|
949
|
+
this.isActive = true;
|
|
950
|
+
if (this.definition.handlers.onActivated) {
|
|
951
|
+
const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
|
|
952
|
+
try {
|
|
953
|
+
await this.definition.handlers.onActivated(this.state, context);
|
|
954
|
+
} catch (e) {
|
|
955
|
+
const error = e instanceof HypenError ? e : new ActionError("onActivated", e);
|
|
956
|
+
const shouldRethrow = await this.handleError(error, { lifecycle: "activated" });
|
|
957
|
+
if (shouldRethrow) {
|
|
958
|
+
throw error;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async deactivate() {
|
|
964
|
+
if (this.isDestroyed || !this.isActive)
|
|
965
|
+
return;
|
|
966
|
+
this.isActive = false;
|
|
967
|
+
if (this.definition.handlers.onDeactivated) {
|
|
968
|
+
const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
|
|
969
|
+
try {
|
|
970
|
+
await this.definition.handlers.onDeactivated(this.state, context);
|
|
971
|
+
} catch (e) {
|
|
972
|
+
const error = e instanceof HypenError ? e : new ActionError("onDeactivated", e);
|
|
973
|
+
const shouldRethrow = await this.handleError(error, { lifecycle: "deactivated" });
|
|
974
|
+
if (shouldRethrow) {
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
927
980
|
createGlobalContextAPI() {
|
|
928
981
|
if (!this.globalContext) {
|
|
929
982
|
throw new Error("Global context not available");
|
|
@@ -1072,6 +1125,9 @@ class HypenModuleInstance {
|
|
|
1072
1125
|
async destroy() {
|
|
1073
1126
|
if (this.isDestroyed)
|
|
1074
1127
|
return;
|
|
1128
|
+
if (this.isActive) {
|
|
1129
|
+
await this.deactivate();
|
|
1130
|
+
}
|
|
1075
1131
|
if (this.currentPersistKey && this.stateStore) {
|
|
1076
1132
|
clearTimeout(this.persistDebounceTimer);
|
|
1077
1133
|
const snapshot = getStateSnapshot(this.state);
|
|
@@ -1116,4 +1172,4 @@ export {
|
|
|
1116
1172
|
HypenApp
|
|
1117
1173
|
};
|
|
1118
1174
|
|
|
1119
|
-
//# debugId=
|
|
1175
|
+
//# debugId=94DE9279313C0D1764756E2164756E21
|