@alepha/react 0.5.0 → 0.5.1

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.
@@ -0,0 +1,926 @@
1
+ import { __descriptor, NotImplementedError, KIND, EventEmitter, $logger, $inject, Alepha, $hook } from '@alepha/core';
2
+ import { createContext, useContext, useState, useEffect, createElement, useMemo } from 'react';
3
+ import { HttpClient } from '@alepha/server';
4
+ import { hydrateRoot, createRoot } from 'react-dom/client';
5
+ import { compile, match } from 'path-to-regexp';
6
+
7
+ const pageDescriptorKey = "PAGE";
8
+ const $page = (options) => {
9
+ __descriptor(pageDescriptorKey);
10
+ return {
11
+ [KIND]: pageDescriptorKey,
12
+ options,
13
+ render: () => {
14
+ throw new NotImplementedError(pageDescriptorKey);
15
+ },
16
+ go: () => {
17
+ throw new NotImplementedError(pageDescriptorKey);
18
+ },
19
+ createAnchorProps: () => {
20
+ throw new NotImplementedError(pageDescriptorKey);
21
+ }
22
+ };
23
+ };
24
+ $page[KIND] = pageDescriptorKey;
25
+
26
+ const RouterContext = createContext(
27
+ void 0
28
+ );
29
+
30
+ const RouterLayerContext = createContext(void 0);
31
+
32
+ const NestedView = (props) => {
33
+ const app = useContext(RouterContext);
34
+ const layer = useContext(RouterLayerContext);
35
+ const index = layer?.index ?? 0;
36
+ const [view, setView] = useState(
37
+ app?.state.layers[index]?.element
38
+ );
39
+ useEffect(() => {
40
+ if (app?.alepha.isBrowser()) {
41
+ return app?.router.on("end", ({ layers }) => {
42
+ setView(layers[index]?.element);
43
+ });
44
+ }
45
+ }, [app]);
46
+ return view ?? props.children ?? null;
47
+ };
48
+
49
+ class RedirectException extends Error {
50
+ constructor(page) {
51
+ super("Redirection");
52
+ this.page = page;
53
+ }
54
+ }
55
+ class Router extends EventEmitter {
56
+ log = $logger();
57
+ alepha = $inject(Alepha);
58
+ pages = [];
59
+ notFoundPageRoute;
60
+ /**
61
+ * Get the page by name.
62
+ *
63
+ * @param name - Page name
64
+ * @return PageRoute
65
+ */
66
+ page(name) {
67
+ const found = this.pages.find((it) => it.name === name);
68
+ if (!found) {
69
+ throw new Error(`Page ${name} not found`);
70
+ }
71
+ return found;
72
+ }
73
+ /**
74
+ *
75
+ */
76
+ root(state, opts = {}) {
77
+ return createElement(
78
+ RouterContext.Provider,
79
+ {
80
+ value: {
81
+ state,
82
+ router: this,
83
+ alepha: this.alepha,
84
+ session: opts.user ? { user: opts.user } : void 0
85
+ }
86
+ },
87
+ state.layers[0]?.element
88
+ );
89
+ }
90
+ /**
91
+ *
92
+ * @param url
93
+ * @param options
94
+ */
95
+ async render(url, options = {}) {
96
+ const [pathname, search = ""] = url.split("?");
97
+ const state = {
98
+ pathname,
99
+ search,
100
+ layers: []
101
+ };
102
+ this.emit("begin", void 0);
103
+ try {
104
+ let layers = await this.match(url, options);
105
+ if (layers.length === 0) {
106
+ if (this.notFoundPageRoute) {
107
+ layers = await this.createLayers(url, this.notFoundPageRoute);
108
+ } else {
109
+ layers.push({
110
+ name: "not-found",
111
+ element: "Not Found",
112
+ index: 0,
113
+ path: "/"
114
+ });
115
+ }
116
+ }
117
+ state.layers = layers;
118
+ this.emit("success", void 0);
119
+ } catch (e) {
120
+ if (e instanceof RedirectException) {
121
+ return {
122
+ element: null,
123
+ layers: [],
124
+ redirect: typeof e.page === "string" ? e.page : this.href(e.page)
125
+ };
126
+ }
127
+ this.log.error(e);
128
+ state.layers = [
129
+ {
130
+ name: "error",
131
+ element: this.renderError(e),
132
+ index: 0,
133
+ path: "/"
134
+ }
135
+ ];
136
+ this.emit("error", e);
137
+ }
138
+ if (options.state) {
139
+ options.state.layers = state.layers;
140
+ options.state.pathname = state.pathname;
141
+ options.state.search = state.search;
142
+ this.emit("end", options.state);
143
+ return {
144
+ element: this.root(options.state, options),
145
+ layers: options.state.layers
146
+ };
147
+ }
148
+ this.emit("end", state);
149
+ return {
150
+ element: this.root(state, options),
151
+ layers: state.layers
152
+ };
153
+ }
154
+ /**
155
+ *
156
+ * @param url
157
+ * @param options
158
+ * @protected
159
+ */
160
+ async match(url, options = {}) {
161
+ const pages = this.pages;
162
+ const previous = options.previous;
163
+ const [pathname, search] = url.split("?");
164
+ for (const route of pages) {
165
+ if (route.children?.find((it) => !it.path || it.path === "/")) continue;
166
+ if (!route.match) continue;
167
+ const match2 = route.match.exec(pathname);
168
+ if (match2) {
169
+ const params = match2.params ?? {};
170
+ const query = {};
171
+ if (search) {
172
+ for (const [key, value] of new URLSearchParams(search).entries()) {
173
+ query[key] = String(value);
174
+ }
175
+ }
176
+ return await this.createLayers(
177
+ url,
178
+ route,
179
+ params,
180
+ query,
181
+ previous,
182
+ options.user
183
+ );
184
+ }
185
+ }
186
+ return [];
187
+ }
188
+ /**
189
+ * Create layers for the given route.
190
+ *
191
+ * @param route
192
+ * @param params
193
+ * @param query
194
+ * @param previous
195
+ * @param user
196
+ * @protected
197
+ */
198
+ async createLayers(url, route, params = {}, query = {}, previous = [], user) {
199
+ const layers = [];
200
+ let context = {};
201
+ const stack = [{ route }];
202
+ let parent = route.parent;
203
+ while (parent) {
204
+ stack.unshift({ route: parent });
205
+ parent = parent.parent;
206
+ }
207
+ let forceRefresh = false;
208
+ for (let i = 0; i < stack.length; i++) {
209
+ const it = stack[i];
210
+ const route2 = it.route;
211
+ const config = {};
212
+ try {
213
+ config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, query) : {};
214
+ } catch (e) {
215
+ it.error = e;
216
+ break;
217
+ }
218
+ try {
219
+ config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, params) : {};
220
+ } catch (e) {
221
+ it.error = e;
222
+ break;
223
+ }
224
+ it.config = {
225
+ ...config
226
+ };
227
+ if (!route2.resolve) {
228
+ continue;
229
+ }
230
+ if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
231
+ const url2 = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
232
+ const prev = JSON.stringify({
233
+ part: url2(previous[i].part),
234
+ params: previous[i].config?.params ?? {}
235
+ });
236
+ const curr = JSON.stringify({
237
+ part: url2(route2.path),
238
+ params: config.params ?? {}
239
+ });
240
+ if (prev === curr) {
241
+ it.props = previous[i].props;
242
+ context = {
243
+ ...context,
244
+ ...it.props
245
+ };
246
+ continue;
247
+ }
248
+ forceRefresh = true;
249
+ }
250
+ try {
251
+ const props = await route2.resolve?.({
252
+ ...config,
253
+ ...context,
254
+ user,
255
+ url
256
+ }) ?? {};
257
+ it.props = {
258
+ ...props
259
+ };
260
+ context = {
261
+ ...context,
262
+ ...props
263
+ };
264
+ } catch (e) {
265
+ if (e instanceof RedirectException) {
266
+ throw e;
267
+ }
268
+ this.log.error(e);
269
+ it.error = e;
270
+ break;
271
+ }
272
+ }
273
+ let acc = "";
274
+ for (let i = 0; i < stack.length; i++) {
275
+ const it = stack[i];
276
+ const props = it.props ?? {};
277
+ const params2 = { ...it.config?.params };
278
+ for (const key of Object.keys(params2)) {
279
+ params2[key] = String(params2[key]);
280
+ }
281
+ acc += "/";
282
+ acc += it.route.path ? compile(it.route.path)(params2) : "";
283
+ const path = acc.replace(/\/+/, "/");
284
+ if (it.error) {
285
+ const errorHandler = this.getErrorHandler(it.route);
286
+ const element = errorHandler ? errorHandler({
287
+ ...it.config,
288
+ error: it.error,
289
+ url
290
+ }) : this.renderError(it.error);
291
+ layers.push({
292
+ props,
293
+ name: it.route.name,
294
+ part: it.route.path,
295
+ config: it.config,
296
+ element: this.renderView(i + 1, path, element),
297
+ index: i + 1,
298
+ path
299
+ });
300
+ break;
301
+ }
302
+ const layer = await this.createElement(it.route, {
303
+ ...props,
304
+ ...context
305
+ });
306
+ layers.push({
307
+ name: it.route.name,
308
+ props,
309
+ part: it.route.path,
310
+ config: it.config,
311
+ element: this.renderView(i + 1, path, layer),
312
+ index: i + 1,
313
+ path
314
+ });
315
+ }
316
+ return layers;
317
+ }
318
+ /**
319
+ *
320
+ * @param route
321
+ * @protected
322
+ */
323
+ getErrorHandler(route) {
324
+ if (route.errorHandler) return route.errorHandler;
325
+ let parent = route.parent;
326
+ while (parent) {
327
+ if (parent.errorHandler) return parent.errorHandler;
328
+ parent = parent.parent;
329
+ }
330
+ }
331
+ /**
332
+ *
333
+ * @param page
334
+ * @param props
335
+ * @protected
336
+ */
337
+ async createElement(page, props) {
338
+ if (page.lazy) {
339
+ const component = await page.lazy();
340
+ return createElement(component.default, props);
341
+ }
342
+ if (page.component) {
343
+ return createElement(page.component, props);
344
+ }
345
+ return void 0;
346
+ }
347
+ /**
348
+ *
349
+ * @param e
350
+ * @protected
351
+ */
352
+ renderError(e) {
353
+ return createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
354
+ }
355
+ /**
356
+ * Render an empty view.
357
+ *
358
+ * @protected
359
+ */
360
+ renderEmptyView() {
361
+ return createElement(NestedView, {});
362
+ }
363
+ /**
364
+ * Create a valid href for the given page.
365
+ * @param page
366
+ * @param params
367
+ */
368
+ href(page, params = {}) {
369
+ const found = this.pages.find((it) => it.name === page.options.name);
370
+ if (!found) {
371
+ throw new Error(`Page ${page.options.name} not found`);
372
+ }
373
+ let url = found.path ?? "";
374
+ let parent = found.parent;
375
+ while (parent) {
376
+ url = `${parent.path ?? ""}/${url}`;
377
+ parent = parent.parent;
378
+ }
379
+ url = compile(url)(params);
380
+ return url.replace(/\/\/+/g, "/") || "/";
381
+ }
382
+ /**
383
+ *
384
+ * @param index
385
+ * @param path
386
+ * @param view
387
+ * @protected
388
+ */
389
+ renderView(index, path, view = this.renderEmptyView()) {
390
+ return createElement(
391
+ RouterLayerContext.Provider,
392
+ {
393
+ value: {
394
+ index,
395
+ path
396
+ }
397
+ },
398
+ view
399
+ );
400
+ }
401
+ /**
402
+ *
403
+ * @param entry
404
+ */
405
+ add(entry) {
406
+ if (this.alepha.isReady()) {
407
+ throw new Error("Router is already initialized");
408
+ }
409
+ if (entry.notFoundHandler) {
410
+ this.notFoundPageRoute = {
411
+ name: "not-found",
412
+ component: entry.notFoundHandler
413
+ };
414
+ }
415
+ entry.name ??= this.nextId();
416
+ const page = entry;
417
+ page.match = this.createMatchFunction(page);
418
+ this.pages.push(page);
419
+ if (page.children) {
420
+ for (const child of page.children) {
421
+ child.parent = page;
422
+ this.add(child);
423
+ }
424
+ }
425
+ }
426
+ /**
427
+ * Create a match function for the given page.
428
+ *
429
+ * @param page
430
+ * @protected
431
+ */
432
+ createMatchFunction(page) {
433
+ let url = page.path ?? "/";
434
+ let target = page.parent;
435
+ while (target) {
436
+ url = `${target.path ?? ""}/${url}`;
437
+ target = target.parent;
438
+ }
439
+ let path = url.replace(/\/\/+/g, "/");
440
+ if (path.endsWith("/")) {
441
+ path = path.slice(0, -1);
442
+ }
443
+ if (path.includes("?")) {
444
+ return {
445
+ exec: match(path.split("?")[0]),
446
+ path
447
+ };
448
+ }
449
+ return {
450
+ exec: match(path),
451
+ path
452
+ };
453
+ }
454
+ /**
455
+ *
456
+ */
457
+ empty() {
458
+ return this.pages.length === 0;
459
+ }
460
+ /**
461
+ *
462
+ * @protected
463
+ */
464
+ _next = 0;
465
+ /**
466
+ *
467
+ * @protected
468
+ */
469
+ nextId() {
470
+ this._next += 1;
471
+ return `P${this._next}`;
472
+ }
473
+ }
474
+
475
+ class PageDescriptorProvider {
476
+ alepha = $inject(Alepha);
477
+ router = $inject(Router);
478
+ configure = $hook({
479
+ name: "configure",
480
+ handler: () => {
481
+ const pages = this.alepha.getDescriptorValues($page);
482
+ for (const { value, key } of pages) {
483
+ value.options.name ??= key;
484
+ if (pages.find((it) => it.value.options.children?.().includes(value))) {
485
+ continue;
486
+ }
487
+ this.router.add(this.map(pages, value));
488
+ }
489
+ }
490
+ });
491
+ /**
492
+ * Transform
493
+ * @param pages
494
+ * @param target
495
+ * @protected
496
+ */
497
+ map(pages, target) {
498
+ const children = target.options.children?.() ?? [];
499
+ for (const it of pages) {
500
+ if (it.value.options.parent === target) {
501
+ children.push(it.value);
502
+ }
503
+ }
504
+ return {
505
+ ...target.options,
506
+ parent: void 0,
507
+ children: children.map((it) => this.map(pages, it))
508
+ };
509
+ }
510
+ }
511
+
512
+ class ReactBrowserProvider {
513
+ log = $logger();
514
+ client = $inject(HttpClient);
515
+ router = $inject(Router);
516
+ root;
517
+ transitioning;
518
+ state = { layers: [], pathname: "", search: "" };
519
+ /**
520
+ *
521
+ */
522
+ get document() {
523
+ return window.document;
524
+ }
525
+ /**
526
+ *
527
+ */
528
+ get history() {
529
+ return window.history;
530
+ }
531
+ /**
532
+ *
533
+ */
534
+ get url() {
535
+ return window.location.pathname + window.location.search;
536
+ }
537
+ /**
538
+ *
539
+ * @param props
540
+ */
541
+ async invalidate(props) {
542
+ const previous = [];
543
+ if (props) {
544
+ const [key] = Object.keys(props);
545
+ const value = props[key];
546
+ for (const layer of this.state.layers) {
547
+ if (layer.props?.[key]) {
548
+ previous.push({
549
+ ...layer,
550
+ props: {
551
+ ...layer.props,
552
+ [key]: value
553
+ }
554
+ });
555
+ break;
556
+ }
557
+ previous.push(layer);
558
+ }
559
+ }
560
+ await this.render({ previous });
561
+ }
562
+ /**
563
+ *
564
+ * @param url
565
+ * @param options
566
+ */
567
+ async go(url, options = {}) {
568
+ const result = await this.render({
569
+ url
570
+ });
571
+ if (result.url !== url) {
572
+ this.history.replaceState({}, "", result.url);
573
+ return;
574
+ }
575
+ if (options.replace) {
576
+ this.history.replaceState({}, "", url);
577
+ return;
578
+ }
579
+ this.history.pushState({}, "", url);
580
+ }
581
+ /**
582
+ *
583
+ * @param options
584
+ * @protected
585
+ */
586
+ async render(options = {}) {
587
+ const previous = options.previous ?? this.state.layers;
588
+ const url = options.url ?? this.url;
589
+ this.transitioning = { to: url };
590
+ const result = await this.router.render(url, {
591
+ previous,
592
+ state: this.state
593
+ });
594
+ if (result.redirect) {
595
+ return await this.render({ url: result.redirect });
596
+ }
597
+ this.transitioning = void 0;
598
+ return { url };
599
+ }
600
+ /**
601
+ * Get embedded layers from the server.
602
+ *
603
+ * @protected
604
+ */
605
+ getEmbeddedCache() {
606
+ try {
607
+ if ("__ssr" in window && typeof window.__ssr === "object") {
608
+ return window.__ssr;
609
+ }
610
+ } catch (error) {
611
+ console.error(error);
612
+ }
613
+ }
614
+ /**
615
+ *
616
+ * @protected
617
+ */
618
+ getRootElement() {
619
+ const root = this.document.getElementById("root");
620
+ if (root) {
621
+ return root;
622
+ }
623
+ const div = this.document.createElement("div");
624
+ div.id = "root";
625
+ this.document.body.appendChild(div);
626
+ return div;
627
+ }
628
+ // -------------------------------------------------------------------------------------------------------------------
629
+ /**
630
+ *
631
+ * @protected
632
+ */
633
+ ready = $hook({
634
+ name: "ready",
635
+ handler: async () => {
636
+ const cache = this.getEmbeddedCache();
637
+ const previous = cache?.layers ?? [];
638
+ const session = cache?.session ?? await this.client.of().session();
639
+ await this.render({ previous });
640
+ const element = this.router.root(this.state, session);
641
+ if (previous.length > 0) {
642
+ this.root = hydrateRoot(this.getRootElement(), element);
643
+ this.log.info("Hydrated root element");
644
+ } else {
645
+ this.root = createRoot(this.getRootElement());
646
+ this.root.render(element);
647
+ this.log.info("Created root element");
648
+ }
649
+ window.addEventListener("popstate", () => {
650
+ this.render();
651
+ });
652
+ }
653
+ });
654
+ /**
655
+ *
656
+ * @protected
657
+ */
658
+ stop = $hook({
659
+ name: "stop",
660
+ handler: async () => {
661
+ if (this.root) {
662
+ this.root.unmount();
663
+ this.log.info("Unmounted root element");
664
+ }
665
+ }
666
+ });
667
+ }
668
+
669
+ class RouterHookApi {
670
+ constructor(state, layer, browser) {
671
+ this.state = state;
672
+ this.layer = layer;
673
+ this.browser = browser;
674
+ }
675
+ /**
676
+ *
677
+ */
678
+ get current() {
679
+ return this.state;
680
+ }
681
+ /**
682
+ *
683
+ */
684
+ get pathname() {
685
+ return this.state.pathname;
686
+ }
687
+ /**
688
+ *
689
+ */
690
+ get query() {
691
+ const query = {};
692
+ for (const [key, value] of new URLSearchParams(
693
+ this.state.search
694
+ ).entries()) {
695
+ query[key] = String(value);
696
+ }
697
+ return query;
698
+ }
699
+ /**
700
+ *
701
+ */
702
+ async back() {
703
+ this.browser?.history.back();
704
+ }
705
+ /**
706
+ *
707
+ */
708
+ async forward() {
709
+ this.browser?.history.forward();
710
+ }
711
+ /**
712
+ *
713
+ * @param props
714
+ */
715
+ async invalidate(props) {
716
+ await this.browser?.invalidate(props);
717
+ }
718
+ /**
719
+ * Create a valid href for the given pathname.
720
+ *
721
+ * @param pathname
722
+ * @param layer
723
+ */
724
+ createHref(pathname, layer = this.layer) {
725
+ if (typeof pathname === "object") {
726
+ pathname = pathname.options.path ?? "";
727
+ }
728
+ return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
729
+ }
730
+ /**
731
+ *
732
+ * @param path
733
+ * @param options
734
+ */
735
+ async go(path, options = {}) {
736
+ return await this.browser?.go(this.createHref(path, this.layer), options);
737
+ }
738
+ /**
739
+ *
740
+ * @param path
741
+ */
742
+ createAnchorProps(path) {
743
+ const href = this.createHref(path, this.layer);
744
+ return {
745
+ href,
746
+ onClick: (ev) => {
747
+ ev.stopPropagation();
748
+ ev.preventDefault();
749
+ this.go(path).catch(console.error);
750
+ }
751
+ };
752
+ }
753
+ /**
754
+ * Set query params.
755
+ *
756
+ * @param record
757
+ * @param options
758
+ */
759
+ setQueryParams(record, options = {}) {
760
+ const search = new URLSearchParams(
761
+ options.merge ? {
762
+ ...this.query,
763
+ ...record
764
+ } : {
765
+ ...record
766
+ }
767
+ ).toString();
768
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
769
+ if (options.push) {
770
+ window.history.pushState({}, "", state);
771
+ } else {
772
+ window.history.replaceState({}, "", state);
773
+ }
774
+ }
775
+ }
776
+
777
+ const useRouter = () => {
778
+ const ctx = useContext(RouterContext);
779
+ const layer = useContext(RouterLayerContext);
780
+ if (!ctx || !layer) {
781
+ throw new Error("useRouter must be used within a RouterProvider");
782
+ }
783
+ return useMemo(
784
+ () => new RouterHookApi(
785
+ ctx.state,
786
+ layer,
787
+ ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
788
+ ),
789
+ [ctx.router, layer]
790
+ );
791
+ };
792
+
793
+ const useActive = (path) => {
794
+ const router = useRouter();
795
+ const ctx = useContext(RouterContext);
796
+ const layer = useContext(RouterLayerContext);
797
+ if (!ctx || !layer) {
798
+ throw new Error("useRouter must be used within a RouterProvider");
799
+ }
800
+ let name;
801
+ if (typeof path === "object" && path.options.name) {
802
+ name = path.options.name;
803
+ }
804
+ const [current, setCurrent] = useState(ctx.state.pathname);
805
+ const href = useMemo(() => router.createHref(path, layer), [path, layer]);
806
+ const [isPending, setPending] = useState(false);
807
+ const isActive = current === href;
808
+ useEffect(
809
+ () => ctx.router.on("end", ({ pathname }) => setCurrent(pathname)),
810
+ []
811
+ );
812
+ return {
813
+ name,
814
+ isPending,
815
+ isActive,
816
+ anchorProps: {
817
+ href,
818
+ onClick: (ev) => {
819
+ ev.stopPropagation();
820
+ ev.preventDefault();
821
+ if (isActive) return;
822
+ if (isPending) return;
823
+ setPending(true);
824
+ router.go(href).then(() => {
825
+ setPending(false);
826
+ });
827
+ }
828
+ }
829
+ };
830
+ };
831
+
832
+ const useInject = (classEntry) => {
833
+ const ctx = useContext(RouterContext);
834
+ if (!ctx) {
835
+ throw new Error("useRouter must be used within a <RouterProvider>");
836
+ }
837
+ return ctx.alepha.get(classEntry);
838
+ };
839
+
840
+ const useClient = () => {
841
+ return useInject(HttpClient);
842
+ };
843
+
844
+ const useQueryParams = (schema, options = {}) => {
845
+ const ctx = useContext(RouterContext);
846
+ if (!ctx) {
847
+ throw new Error("useQueryParams must be used within a RouterProvider");
848
+ }
849
+ const key = options.key ?? "q";
850
+ const router = useRouter();
851
+ const querystring = router.query[key];
852
+ const [queryParams, setQueryParams] = useState(
853
+ decode(ctx.alepha, schema, router.query[key])
854
+ );
855
+ useEffect(() => {
856
+ setQueryParams(decode(ctx.alepha, schema, querystring));
857
+ }, [querystring]);
858
+ return [
859
+ queryParams,
860
+ (queryParams2) => {
861
+ setQueryParams(queryParams2);
862
+ router.setQueryParams(
863
+ { [key]: encode(ctx.alepha, schema, queryParams2) },
864
+ {
865
+ merge: true
866
+ }
867
+ );
868
+ }
869
+ ];
870
+ };
871
+ const encode = (alepha, schema, data) => {
872
+ return btoa(JSON.stringify(alepha.parse(schema, data)));
873
+ };
874
+ const decode = (alepha, schema, data) => {
875
+ try {
876
+ return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
877
+ } catch (error) {
878
+ return {};
879
+ }
880
+ };
881
+
882
+ const useRouterEvents = (opts = {}) => {
883
+ const ctx = useContext(RouterContext);
884
+ const layer = useContext(RouterLayerContext);
885
+ if (!ctx || !layer) {
886
+ throw new Error("useRouter must be used within a RouterProvider");
887
+ }
888
+ useEffect(() => {
889
+ const subs = [];
890
+ const onBegin = opts.onBegin;
891
+ const onEnd = opts.onEnd;
892
+ const onError = opts.onError;
893
+ if (onBegin) {
894
+ subs.push(ctx.router.on("begin", onBegin));
895
+ }
896
+ if (onEnd) {
897
+ subs.push(ctx.router.on("end", onEnd));
898
+ }
899
+ if (onError) {
900
+ subs.push(ctx.router.on("error", onError));
901
+ }
902
+ return () => {
903
+ for (const sub of subs) {
904
+ sub();
905
+ }
906
+ };
907
+ }, []);
908
+ };
909
+
910
+ const useRouterState = () => {
911
+ const ctx = useContext(RouterContext);
912
+ const layer = useContext(RouterLayerContext);
913
+ if (!ctx || !layer) {
914
+ throw new Error("useRouter must be used within a RouterProvider");
915
+ }
916
+ const [state, setState] = useState(ctx.state);
917
+ useEffect(
918
+ () => ctx.router.on("end", (it) => {
919
+ setState({ ...it });
920
+ }),
921
+ []
922
+ );
923
+ return state;
924
+ };
925
+
926
+ export { $page as $, NestedView as N, PageDescriptorProvider as P, Router as R, RouterContext as a, RouterLayerContext as b, useClient as c, useInject as d, useQueryParams as e, RouterHookApi as f, useRouter as g, useRouterEvents as h, useRouterState as i, RedirectException as j, ReactBrowserProvider as k, pageDescriptorKey as p, useActive as u };