@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/src/app.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface ErrorContext<T> {
|
|
|
140
140
|
/** The action name if error occurred in an action handler */
|
|
141
141
|
actionName?: string;
|
|
142
142
|
/** The lifecycle phase if error occurred in a lifecycle handler */
|
|
143
|
-
lifecycle?: "created" | "destroyed" | "disconnect" | "reconnect" | "expire";
|
|
143
|
+
lifecycle?: "created" | "activated" | "deactivated" | "destroyed" | "disconnect" | "reconnect" | "expire";
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
@@ -181,6 +181,20 @@ export interface HypenModuleDefinition<T = unknown> {
|
|
|
181
181
|
stateStore?: StateStore<T>;
|
|
182
182
|
handlers: {
|
|
183
183
|
onCreated?: LifecycleHandler<T>;
|
|
184
|
+
/**
|
|
185
|
+
* Called every time the module becomes the active route target.
|
|
186
|
+
* Runs after `onCreated` on first mount, and again on each re-mount
|
|
187
|
+
* from the ManagedRouter's persistence cache. Use for data refresh,
|
|
188
|
+
* subscription (re)connects, or any "screen became visible" work.
|
|
189
|
+
*/
|
|
190
|
+
onActivated?: LifecycleHandler<T>;
|
|
191
|
+
/**
|
|
192
|
+
* Called every time the module stops being the active route target.
|
|
193
|
+
* Runs before the module is cached for persistence OR before
|
|
194
|
+
* `onDestroyed` if the module is being torn down. Use for pausing
|
|
195
|
+
* timers, tearing down ephemeral subscriptions, etc.
|
|
196
|
+
*/
|
|
197
|
+
onDeactivated?: LifecycleHandler<T>;
|
|
184
198
|
onAction: Map<string, ActionHandler<T, any>>;
|
|
185
199
|
onDestroyed?: LifecycleHandler<T>;
|
|
186
200
|
/** Called when client disconnects (session persists for TTL) */
|
|
@@ -206,6 +220,8 @@ export class HypenAppBuilder<T> {
|
|
|
206
220
|
private initialState: T;
|
|
207
221
|
private options: { persist?: boolean; version?: number; name?: string };
|
|
208
222
|
private createdHandler?: LifecycleHandler<T>;
|
|
223
|
+
private activatedHandler?: LifecycleHandler<T>;
|
|
224
|
+
private deactivatedHandler?: LifecycleHandler<T>;
|
|
209
225
|
private actionHandlers: Map<string, ActionHandler<T, any>> = new Map();
|
|
210
226
|
private destroyedHandler?: LifecycleHandler<T>;
|
|
211
227
|
private disconnectHandler?: DisconnectHandler<T>;
|
|
@@ -257,6 +273,47 @@ export class HypenAppBuilder<T> {
|
|
|
257
273
|
return this;
|
|
258
274
|
}
|
|
259
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Register a handler that runs every time the module becomes the active
|
|
278
|
+
* route target.
|
|
279
|
+
*
|
|
280
|
+
* Unlike `onCreated`, which only runs once per module instance, `onActivated`
|
|
281
|
+
* runs on **every** mount — the first one (right after `onCreated`) and
|
|
282
|
+
* every subsequent re-entry when a cached module is restored by
|
|
283
|
+
* `ManagedRouter`. Use this hook for data refresh, (re)connecting
|
|
284
|
+
* subscriptions, starting timers, and any "screen became visible" work
|
|
285
|
+
* that should happen on every navigation to this route.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* ```typescript
|
|
289
|
+
* app
|
|
290
|
+
* .defineState({ items: [], loading: false })
|
|
291
|
+
* .onActivated(async (state) => {
|
|
292
|
+
* state.loading = true;
|
|
293
|
+
* state.items = await fetchItems();
|
|
294
|
+
* state.loading = false;
|
|
295
|
+
* });
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
onActivated(fn: LifecycleHandler<T>): this {
|
|
299
|
+
this.activatedHandler = fn;
|
|
300
|
+
return this;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Register a handler that runs every time the module stops being the
|
|
305
|
+
* active route target.
|
|
306
|
+
*
|
|
307
|
+
* Runs before the module is cached for persistence (when the user
|
|
308
|
+
* navigates to another route) OR before `onDestroyed` if the module is
|
|
309
|
+
* being torn down. Use this for pausing timers, unsubscribing from
|
|
310
|
+
* ephemeral streams, and any "screen became hidden" cleanup.
|
|
311
|
+
*/
|
|
312
|
+
onDeactivated(fn: LifecycleHandler<T>): this {
|
|
313
|
+
this.deactivatedHandler = fn;
|
|
314
|
+
return this;
|
|
315
|
+
}
|
|
316
|
+
|
|
260
317
|
/**
|
|
261
318
|
* Register a handler for module destruction
|
|
262
319
|
*/
|
|
@@ -453,6 +510,8 @@ export class HypenAppBuilder<T> {
|
|
|
453
510
|
stateStore: this._stateStore,
|
|
454
511
|
handlers: {
|
|
455
512
|
onCreated: this.createdHandler,
|
|
513
|
+
onActivated: this.activatedHandler,
|
|
514
|
+
onDeactivated: this.deactivatedHandler,
|
|
456
515
|
onAction: this.actionHandlers,
|
|
457
516
|
onDestroyed: this.destroyedHandler,
|
|
458
517
|
onDisconnect: this.disconnectHandler,
|
|
@@ -582,6 +641,13 @@ export class HypenModuleInstance<T extends object = any> {
|
|
|
582
641
|
private definition: HypenModuleDefinition<T>;
|
|
583
642
|
private state: T;
|
|
584
643
|
private isDestroyed = false;
|
|
644
|
+
/**
|
|
645
|
+
* True when the module is currently the active route target (i.e.
|
|
646
|
+
* `onActivated` has fired more recently than `onDeactivated`).
|
|
647
|
+
* Used to make `activate()` / `deactivate()` idempotent so the
|
|
648
|
+
* `ManagedRouter` can call them safely regardless of state.
|
|
649
|
+
*/
|
|
650
|
+
private isActive = false;
|
|
585
651
|
private router: HypenRouter | null;
|
|
586
652
|
private globalContext?: HypenGlobalContext;
|
|
587
653
|
private stateChangeCallbacks: Array<() => void> = [];
|
|
@@ -718,6 +784,63 @@ export class HypenModuleInstance<T extends object = any> {
|
|
|
718
784
|
await this._readyPromise;
|
|
719
785
|
}
|
|
720
786
|
|
|
787
|
+
/**
|
|
788
|
+
* Mark the module as the active route target and fire `onActivated`.
|
|
789
|
+
*
|
|
790
|
+
* Idempotent: calling `activate()` on an already-active module is a
|
|
791
|
+
* no-op. Internally awaits `waitForReady()` so that `onCreated` always
|
|
792
|
+
* runs to completion before `onActivated` fires, regardless of who
|
|
793
|
+
* calls this method and when.
|
|
794
|
+
*
|
|
795
|
+
* Called by `ManagedRouter` on every route mount — both fresh
|
|
796
|
+
* constructions and re-mounts from the persistence cache.
|
|
797
|
+
*/
|
|
798
|
+
async activate(): Promise<void> {
|
|
799
|
+
if (this.isDestroyed || this.isActive) return;
|
|
800
|
+
// Ensure onCreated finishes before onActivated — critical on fresh
|
|
801
|
+
// mounts where the constructor kicks off onCreated asynchronously.
|
|
802
|
+
await this._readyPromise;
|
|
803
|
+
if (this.isDestroyed || this.isActive) return;
|
|
804
|
+
this.isActive = true;
|
|
805
|
+
if (this.definition.handlers.onActivated) {
|
|
806
|
+
const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
|
|
807
|
+
try {
|
|
808
|
+
await this.definition.handlers.onActivated(this.state, context);
|
|
809
|
+
} catch (e) {
|
|
810
|
+
const error = e instanceof HypenError ? e : new ActionError("onActivated", e);
|
|
811
|
+
const shouldRethrow = await this.handleError(error, { lifecycle: "activated" });
|
|
812
|
+
if (shouldRethrow) {
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Mark the module as no longer the active route target and fire
|
|
821
|
+
* `onDeactivated`.
|
|
822
|
+
*
|
|
823
|
+
* Idempotent: calling `deactivate()` on an inactive module is a no-op.
|
|
824
|
+
* Called by `ManagedRouter` before persisting a module for later reuse
|
|
825
|
+
* OR before destroying it.
|
|
826
|
+
*/
|
|
827
|
+
async deactivate(): Promise<void> {
|
|
828
|
+
if (this.isDestroyed || !this.isActive) return;
|
|
829
|
+
this.isActive = false;
|
|
830
|
+
if (this.definition.handlers.onDeactivated) {
|
|
831
|
+
const context = this.globalContext ? this.createGlobalContextAPI() : undefined;
|
|
832
|
+
try {
|
|
833
|
+
await this.definition.handlers.onDeactivated(this.state, context);
|
|
834
|
+
} catch (e) {
|
|
835
|
+
const error = e instanceof HypenError ? e : new ActionError("onDeactivated", e);
|
|
836
|
+
const shouldRethrow = await this.handleError(error, { lifecycle: "deactivated" });
|
|
837
|
+
if (shouldRethrow) {
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
721
844
|
/**
|
|
722
845
|
* Create the global context API for this module
|
|
723
846
|
*/
|
|
@@ -963,6 +1086,13 @@ export class HypenModuleInstance<T extends object = any> {
|
|
|
963
1086
|
async destroy(): Promise<void> {
|
|
964
1087
|
if (this.isDestroyed) return;
|
|
965
1088
|
|
|
1089
|
+
// If this module is still marked as active (e.g. destroy() was called
|
|
1090
|
+
// without a preceding deactivate()), fire onDeactivated first so the
|
|
1091
|
+
// lifecycle order is always: ...onActivated → onDeactivated → onDestroyed.
|
|
1092
|
+
if (this.isActive) {
|
|
1093
|
+
await this.deactivate();
|
|
1094
|
+
}
|
|
1095
|
+
|
|
966
1096
|
// Flush any pending persistence writes before shutdown
|
|
967
1097
|
if (this.currentPersistKey && this.stateStore) {
|
|
968
1098
|
clearTimeout(this.persistDebounceTimer);
|
package/src/engine-base.ts
CHANGED
|
@@ -227,6 +227,61 @@ export abstract class BaseEngine {
|
|
|
227
227
|
return action;
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Parse a Hypen DSL source and return every `Router { Route ... }`
|
|
232
|
+
* block found in it. The SDK uses this to auto-wire a ManagedRouter
|
|
233
|
+
* against the template without the user having to repeat the route
|
|
234
|
+
* table in code.
|
|
235
|
+
*
|
|
236
|
+
* `moduleScope` is the enclosing `module X { ... }` scope (lowercased)
|
|
237
|
+
* — `null` for the document root. `elementNames` is BFS-ordered so
|
|
238
|
+
* the SDK can pick the first name that matches a registered
|
|
239
|
+
* `HypenApp` module (wrappers like Column / Row come before the
|
|
240
|
+
* real route component).
|
|
241
|
+
*/
|
|
242
|
+
discoverRouters(source: string): Array<{
|
|
243
|
+
moduleScope: string | null;
|
|
244
|
+
routes: Array<{ path: string; elementNames: string[] }>;
|
|
245
|
+
}> {
|
|
246
|
+
const engine = this.ensureInitialized();
|
|
247
|
+
const raw = (engine as any).discoverRouters?.(source);
|
|
248
|
+
if (!raw) return [];
|
|
249
|
+
// wasm-bindgen returns Maps; normalize to plain objects for
|
|
250
|
+
// ergonomic downstream consumption. The nested structures were
|
|
251
|
+
// encoded with serde_wasm_bindgen so plain objects is the right
|
|
252
|
+
// post-decode shape for most consumers.
|
|
253
|
+
const mapToObj = (v: unknown): any => {
|
|
254
|
+
if (v instanceof Map) {
|
|
255
|
+
const obj: Record<string, unknown> = {};
|
|
256
|
+
for (const [k, val] of v.entries()) obj[String(k)] = mapToObj(val);
|
|
257
|
+
return obj;
|
|
258
|
+
}
|
|
259
|
+
if (Array.isArray(v)) return v.map(mapToObj);
|
|
260
|
+
if (v && typeof v === "object") {
|
|
261
|
+
const obj: Record<string, unknown> = {};
|
|
262
|
+
for (const [k, val] of Object.entries(v)) obj[k] = mapToObj(val);
|
|
263
|
+
return obj;
|
|
264
|
+
}
|
|
265
|
+
return v;
|
|
266
|
+
};
|
|
267
|
+
const normalized = mapToObj(raw) as Array<{
|
|
268
|
+
module_scope: string | null;
|
|
269
|
+
moduleScope?: string | null;
|
|
270
|
+
routes: Array<{
|
|
271
|
+
path: string;
|
|
272
|
+
element_names?: string[];
|
|
273
|
+
elementNames?: string[];
|
|
274
|
+
}>;
|
|
275
|
+
}>;
|
|
276
|
+
return normalized.map((r) => ({
|
|
277
|
+
moduleScope: r.moduleScope ?? r.module_scope ?? null,
|
|
278
|
+
routes: r.routes.map((route) => ({
|
|
279
|
+
path: route.path,
|
|
280
|
+
elementNames: route.elementNames ?? route.element_names ?? [],
|
|
281
|
+
})),
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
|
|
230
285
|
/**
|
|
231
286
|
* Initialize the primary module.
|
|
232
287
|
*/
|
package/src/index.ts
CHANGED
|
@@ -132,6 +132,13 @@ export type {
|
|
|
132
132
|
StateObserverOptions,
|
|
133
133
|
} from "./state.js";
|
|
134
134
|
|
|
135
|
+
// Portable DI seam: `@hypen-space/server` and `@hypen-space/web-engine`
|
|
136
|
+
// call `setPortableImpl` at their module-init time to route core's
|
|
137
|
+
// diff / matchPath / path-ops / URL helpers through the Rust engine's
|
|
138
|
+
// canonical implementations. Standalone core use gets TS fallbacks.
|
|
139
|
+
export { setPortableImpl, portable } from "./portable.js";
|
|
140
|
+
export type { PortableImpl } from "./portable.js";
|
|
141
|
+
|
|
135
142
|
// ============================================================================
|
|
136
143
|
// RENDERER ABSTRACTION
|
|
137
144
|
// ============================================================================
|
|
@@ -150,6 +157,12 @@ export type {
|
|
|
150
157
|
RouteChangeCallback,
|
|
151
158
|
} from "./router.js";
|
|
152
159
|
|
|
160
|
+
// Route-based module lifecycle orchestrator. Wraps HypenRouter and
|
|
161
|
+
// mounts/unmounts modules as the route changes, with optional per-module
|
|
162
|
+
// persistence to avoid tearing down state on every navigation.
|
|
163
|
+
export { ManagedRouter } from "./managed-router.js";
|
|
164
|
+
export type { RouteDefinition } from "./managed-router.js";
|
|
165
|
+
|
|
153
166
|
// ============================================================================
|
|
154
167
|
// EVENTS
|
|
155
168
|
// ============================================================================
|