@jsenv/navi 0.0.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.
Files changed (123) hide show
  1. package/index.js +51 -0
  2. package/package.json +38 -0
  3. package/src/action_private_properties.js +11 -0
  4. package/src/action_proxy_test.html +353 -0
  5. package/src/action_run_states.js +5 -0
  6. package/src/actions.js +1377 -0
  7. package/src/browser_integration/browser_integration.js +191 -0
  8. package/src/browser_integration/document_back_and_forward.js +17 -0
  9. package/src/browser_integration/document_loading_signal.js +100 -0
  10. package/src/browser_integration/document_state_signal.js +9 -0
  11. package/src/browser_integration/document_url_signal.js +9 -0
  12. package/src/browser_integration/use_is_visited.js +19 -0
  13. package/src/browser_integration/via_history.js +199 -0
  14. package/src/browser_integration/via_navigation.js +168 -0
  15. package/src/components/action_execution/form_context.js +8 -0
  16. package/src/components/action_execution/render_actionable_component.jsx +27 -0
  17. package/src/components/action_execution/use_action.js +330 -0
  18. package/src/components/action_execution/use_execute_action.js +161 -0
  19. package/src/components/action_renderer.jsx +136 -0
  20. package/src/components/collect_form_element_values.js +79 -0
  21. package/src/components/demos/0_button_demo.html +155 -0
  22. package/src/components/demos/1_checkbox_demo.html +257 -0
  23. package/src/components/demos/2_input_textual_demo.html +354 -0
  24. package/src/components/demos/3_radio_demo.html +222 -0
  25. package/src/components/demos/4_select_demo.html +104 -0
  26. package/src/components/demos/5_list_scrollable_demo.html +153 -0
  27. package/src/components/demos/action/0_button_demo.html +204 -0
  28. package/src/components/demos/action/10_shortcuts_demo.html +189 -0
  29. package/src/components/demos/action/11_nested_shortcuts_demo.html +401 -0
  30. package/src/components/demos/action/1_input_text_demo.html +461 -0
  31. package/src/components/demos/action/2_form_multiple.html +303 -0
  32. package/src/components/demos/action/3_details_demo.html +172 -0
  33. package/src/components/demos/action/4_input_checkbox_demo.html +611 -0
  34. package/src/components/demos/action/6_checkbox_list_demo.html +109 -0
  35. package/src/components/demos/action/7_radio_list_demo.html +217 -0
  36. package/src/components/demos/action/8_editable_text_demo.html +442 -0
  37. package/src/components/demos/action/9_link_demo.html +172 -0
  38. package/src/components/demos/demo.md +0 -0
  39. package/src/components/demos/route/basic/basic.html +14 -0
  40. package/src/components/demos/route/basic/basic_route_demo.jsx +224 -0
  41. package/src/components/demos/route/multi/multi.html +14 -0
  42. package/src/components/demos/route/multi/multi_route_demo.jsx +277 -0
  43. package/src/components/details/details.jsx +248 -0
  44. package/src/components/details/summary_marker.jsx +141 -0
  45. package/src/components/editable_text/editable_text.jsx +96 -0
  46. package/src/components/error_boundary_context.js +9 -0
  47. package/src/components/form.jsx +144 -0
  48. package/src/components/input/button.jsx +333 -0
  49. package/src/components/input/checkbox_list.jsx +294 -0
  50. package/src/components/input/field.jsx +61 -0
  51. package/src/components/input/field_css.js +118 -0
  52. package/src/components/input/input.jsx +15 -0
  53. package/src/components/input/input_checkbox.jsx +370 -0
  54. package/src/components/input/input_radio.jsx +299 -0
  55. package/src/components/input/input_textual.jsx +338 -0
  56. package/src/components/input/radio_list.jsx +283 -0
  57. package/src/components/input/select.jsx +273 -0
  58. package/src/components/input/use_form_event.js +20 -0
  59. package/src/components/input/use_on_change.js +12 -0
  60. package/src/components/link/link.jsx +291 -0
  61. package/src/components/loader/loader_background.jsx +324 -0
  62. package/src/components/loader/loading_spinner.jsx +68 -0
  63. package/src/components/loader/network_speed.js +83 -0
  64. package/src/components/loader/rectangle_loading.jsx +225 -0
  65. package/src/components/route.jsx +15 -0
  66. package/src/components/selection/selection.js +5 -0
  67. package/src/components/selection/selection_context.jsx +262 -0
  68. package/src/components/shortcut/os.js +9 -0
  69. package/src/components/shortcut/shortcut_context.jsx +390 -0
  70. package/src/components/use_action_events.js +37 -0
  71. package/src/components/use_auto_focus.js +43 -0
  72. package/src/components/use_debounce_true.js +31 -0
  73. package/src/components/use_focus_group.js +19 -0
  74. package/src/components/use_initial_value.js +104 -0
  75. package/src/components/use_is_visited.js +19 -0
  76. package/src/components/use_ref_array.js +38 -0
  77. package/src/components/use_signal_sync.js +50 -0
  78. package/src/components/use_state_array.js +40 -0
  79. package/src/docs/actions.md +228 -0
  80. package/src/docs/demos/resource/action_status.jsx +42 -0
  81. package/src/docs/demos/resource/demo.md +1 -0
  82. package/src/docs/demos/resource/resource_demo_0.html +84 -0
  83. package/src/docs/demos/resource/resource_demo_10_post_gc.html +364 -0
  84. package/src/docs/demos/resource/resource_demo_11_describe_many.html +362 -0
  85. package/src/docs/demos/resource/resource_demo_2.html +173 -0
  86. package/src/docs/demos/resource/resource_demo_3_filtered_users.html +415 -0
  87. package/src/docs/demos/resource/resource_demo_4_details.html +284 -0
  88. package/src/docs/demos/resource/resource_demo_5_renderer_lazy.html +115 -0
  89. package/src/docs/demos/resource/resource_demo_6_gc.html +217 -0
  90. package/src/docs/demos/resource/resource_demo_7_child_gc.html +240 -0
  91. package/src/docs/demos/resource/resource_demo_8_proxy_gc.html +319 -0
  92. package/src/docs/demos/resource/resource_demo_9_describe_one.html +472 -0
  93. package/src/docs/demos/resource/tata.jsx +3 -0
  94. package/src/docs/demos/resource/toto.jsx +3 -0
  95. package/src/docs/demos/user_nav/user_nav.html +12 -0
  96. package/src/docs/demos/user_nav/user_nav.jsx +330 -0
  97. package/src/docs/resource_dependencies.md +103 -0
  98. package/src/docs/resource_with_params.md +80 -0
  99. package/src/notes.md +13 -0
  100. package/src/route/route.js +518 -0
  101. package/src/route/route.test.html +228 -0
  102. package/src/store/array_signal_store.js +537 -0
  103. package/src/store/local_storage_signal.js +17 -0
  104. package/src/store/resource_graph.js +1303 -0
  105. package/src/store/tests/resource_graph_autoreload_demo.html +12 -0
  106. package/src/store/tests/resource_graph_autoreload_demo.jsx +964 -0
  107. package/src/store/tests/resource_graph_dependencies.test.js +95 -0
  108. package/src/store/value_in_local_storage.js +187 -0
  109. package/src/symbol_object_signal.js +1 -0
  110. package/src/use_action_data.js +10 -0
  111. package/src/use_action_status.js +47 -0
  112. package/src/utils/add_many_event_listeners.js +15 -0
  113. package/src/utils/array_add_remove.js +61 -0
  114. package/src/utils/array_signal.js +15 -0
  115. package/src/utils/compare_two_js_values.js +172 -0
  116. package/src/utils/execute_with_cleanup.js +21 -0
  117. package/src/utils/get_caller_info.js +85 -0
  118. package/src/utils/iterable_weak_set.js +62 -0
  119. package/src/utils/js_value_weak_map.js +162 -0
  120. package/src/utils/js_value_weak_map_demo.html +690 -0
  121. package/src/utils/merge_two_js_values.js +53 -0
  122. package/src/utils/stringify_for_display.js +150 -0
  123. package/src/utils/weak_effect.js +48 -0
@@ -0,0 +1,518 @@
1
+ import { batch, computed, effect, signal } from "@preact/signals";
2
+ import { createAction } from "../actions.js";
3
+ import {
4
+ SYMBOL_IDENTITY,
5
+ compareTwoJsValues,
6
+ } from "../utils/compare_two_js_values.js";
7
+
8
+ let baseUrl = import.meta.dev
9
+ ? new URL(window.HTML_ROOT_PATHNAME, window.location).href
10
+ : window.location.origin;
11
+
12
+ export const setBaseUrl = (value) => {
13
+ baseUrl = new URL(value, window.location).href;
14
+ };
15
+
16
+ const DEBUG = false;
17
+ const NO_PARAMS = { [SYMBOL_IDENTITY]: Symbol("no_params") };
18
+ // Controls what happens to actions when their route becomes inactive:
19
+ // 'abort' - Cancel the action immediately when route deactivates
20
+ // 'keep-loading' - Allow action to continue running after route deactivation
21
+ //
22
+ // The 'keep-loading' strategy could act like preloading, keeping data ready for potential return.
23
+ // However, since route reactivation triggers action reload anyway, the old data won't be used
24
+ // so it's better to abort the action to avoid unnecessary resource usage.
25
+ const ROUTE_DEACTIVATION_STRATEGY = "abort"; // 'abort', 'keep-loading'
26
+
27
+ const routeSet = new Set();
28
+ // Store previous route states to detect changes
29
+ const routePreviousStateMap = new WeakMap();
30
+ // Store abort controllers per action to control their lifecycle based on route state
31
+ const actionAbortControllerWeakMap = new WeakMap();
32
+ export const updateRoutes = (
33
+ url,
34
+ {
35
+ // state
36
+ replace,
37
+ isVisited,
38
+ },
39
+ ) => {
40
+ const routeMatchInfoSet = new Set();
41
+ for (const route of routeSet) {
42
+ const routePrivateProperties = getRoutePrivateProperties(route);
43
+ const { urlPattern } = routePrivateProperties;
44
+
45
+ // Get previous state
46
+ const previousState = routePreviousStateMap.get(route) || {
47
+ active: false,
48
+ params: NO_PARAMS,
49
+ };
50
+ const oldActive = previousState.active;
51
+ const oldParams = previousState.params;
52
+ // Check if the URL matches the route pattern
53
+ const match = urlPattern.exec(url);
54
+ const newActive = Boolean(match);
55
+ let newParams;
56
+ if (match) {
57
+ const extractedParams = extractParams(urlPattern, url);
58
+ if (compareTwoJsValues(oldParams, extractedParams)) {
59
+ // No change in parameters, keep the old params
60
+ newParams = oldParams;
61
+ } else {
62
+ newParams = extractedParams;
63
+ }
64
+ } else {
65
+ newParams = NO_PARAMS;
66
+ }
67
+
68
+ const routeMatchInfo = {
69
+ route,
70
+ routePrivateProperties,
71
+ oldActive,
72
+ newActive,
73
+ oldParams,
74
+ newParams,
75
+ };
76
+ routeMatchInfoSet.add(routeMatchInfo);
77
+ // Store current state for next comparison
78
+ routePreviousStateMap.set(route, {
79
+ active: newActive,
80
+ params: newParams,
81
+ });
82
+ }
83
+
84
+ // Apply all signal updates in a batch
85
+ const activeRouteSet = new Set();
86
+ batch(() => {
87
+ for (const {
88
+ route,
89
+ routePrivateProperties,
90
+ newActive,
91
+ newParams,
92
+ } of routeMatchInfoSet) {
93
+ const { activeSignal, paramsSignal, visitedSignal } =
94
+ routePrivateProperties;
95
+ const visited = isVisited(route.url);
96
+ activeSignal.value = newActive;
97
+ paramsSignal.value = newParams;
98
+ visitedSignal.value = visited;
99
+ route.active = newActive;
100
+ route.params = newParams;
101
+ route.visited = visited;
102
+ if (newActive) {
103
+ activeRouteSet.add(route);
104
+ }
105
+ }
106
+ });
107
+
108
+ // must be after paramsSignal.value update to ensure the proxy target is set
109
+ // (so after the batch call)
110
+ const toLoadSet = new Set();
111
+ const toReloadSet = new Set();
112
+ const abortSignalMap = new Map();
113
+ const routeLoadRequestedMap = new Map();
114
+
115
+ for (const {
116
+ route,
117
+ routePrivateProperties,
118
+ newActive,
119
+ oldActive,
120
+ newParams,
121
+ oldParams,
122
+ } of routeMatchInfoSet) {
123
+ const routeAction = route.action;
124
+ if (!routeAction) {
125
+ continue;
126
+ }
127
+
128
+ const becomesActive = newActive && !oldActive;
129
+ const becomesInactive = !newActive && oldActive;
130
+ const paramsChangedWhileActive =
131
+ newActive && oldActive && newParams !== oldParams;
132
+
133
+ // Handle actions for routes that become active
134
+ if (becomesActive) {
135
+ if (DEBUG) {
136
+ console.debug(
137
+ `Route ${routePrivateProperties.urlPattern} became active with params:`,
138
+ newParams,
139
+ );
140
+ }
141
+ const currentAction = routeAction.getCurrentAction();
142
+ if (replace) {
143
+ toLoadSet.add(currentAction);
144
+ } else {
145
+ toReloadSet.add(currentAction);
146
+ }
147
+ routeLoadRequestedMap.set(route, currentAction);
148
+
149
+ // Create a new abort controller for this action
150
+ const actionAbortController = new AbortController();
151
+ actionAbortControllerWeakMap.set(currentAction, actionAbortController);
152
+ abortSignalMap.set(currentAction, actionAbortController.signal);
153
+
154
+ continue;
155
+ }
156
+
157
+ // Handle actions for routes that become inactive - abort them
158
+ if (becomesInactive && ROUTE_DEACTIVATION_STRATEGY === "abort") {
159
+ const currentAction = routeAction.getCurrentAction();
160
+ const actionAbortController =
161
+ actionAbortControllerWeakMap.get(currentAction);
162
+ if (actionAbortController) {
163
+ actionAbortController.abort(`route no longer matching`);
164
+ actionAbortControllerWeakMap.delete(currentAction);
165
+ }
166
+ continue;
167
+ }
168
+
169
+ // Handle parameter changes while route stays active
170
+ if (paramsChangedWhileActive) {
171
+ if (DEBUG) {
172
+ console.debug(
173
+ `Route ${routePrivateProperties.urlPattern} params changed:`,
174
+ newParams,
175
+ );
176
+ }
177
+ const currentAction = routeAction.getCurrentAction();
178
+ if (!replace || currentAction.aborted || currentAction.error) {
179
+ toReloadSet.add(currentAction);
180
+ routeLoadRequestedMap.set(route, currentAction);
181
+ // Create a new abort controller for the reload
182
+ const actionAbortController = new AbortController();
183
+ actionAbortControllerWeakMap.set(currentAction, actionAbortController);
184
+ abortSignalMap.set(currentAction, actionAbortController.signal);
185
+ }
186
+ continue;
187
+ }
188
+ }
189
+
190
+ return {
191
+ loadSet: toLoadSet,
192
+ reloadSet: toReloadSet,
193
+ abortSignalMap,
194
+ routeLoadRequestedMap,
195
+ activeRouteSet,
196
+ };
197
+ };
198
+ const extractParams = (urlPattern, url) => {
199
+ const match = urlPattern.exec(url);
200
+ if (!match) {
201
+ return NO_PARAMS;
202
+ }
203
+ const params = {};
204
+
205
+ // Collect all parameters from URLPattern groups, handling both named and numbered groups
206
+ let wildcardOffset = 0;
207
+ for (const property of URL_PATTERN_PROPERTIES_WITH_GROUP_SET) {
208
+ const urlPartMatch = match[property];
209
+ if (urlPartMatch && urlPartMatch.groups) {
210
+ let localWildcardCount = 0;
211
+ for (const key of Object.keys(urlPartMatch.groups)) {
212
+ const value = urlPartMatch.groups[key];
213
+ const keyAsNumber = parseInt(key, 10);
214
+ if (!isNaN(keyAsNumber)) {
215
+ if (value) {
216
+ // Only include non-empty values
217
+ params[wildcardOffset + keyAsNumber] = decodeURIComponent(value);
218
+ localWildcardCount++;
219
+ }
220
+ } else {
221
+ // Named group (:param or {param})
222
+ params[key] = decodeURIComponent(value);
223
+ }
224
+ }
225
+ // Update wildcard offset for next URL part
226
+ wildcardOffset += localWildcardCount;
227
+ }
228
+ }
229
+ return params;
230
+ };
231
+ const URL_PATTERN_PROPERTIES_WITH_GROUP_SET = new Set([
232
+ "protocol",
233
+ "username",
234
+ "password",
235
+ "hostname",
236
+ "pathname",
237
+ "search",
238
+ "hash",
239
+ ]);
240
+
241
+ const routePrivatePropertiesMap = new Map();
242
+ const getRoutePrivateProperties = (route) => {
243
+ return routePrivatePropertiesMap.get(route);
244
+ };
245
+ const createRoute = (urlPatternInput) => {
246
+ const cleanupCallbackSet = new Set();
247
+ const cleanup = () => {
248
+ for (const cleanupCallback of cleanupCallbackSet) {
249
+ cleanupCallback();
250
+ }
251
+ cleanupCallbackSet.clear();
252
+ };
253
+
254
+ const route = {
255
+ isRoute: true,
256
+ active: false,
257
+ params: NO_PARAMS,
258
+ buildUrl: null,
259
+ bindAction: null,
260
+ relativeUrl: null,
261
+ url: null,
262
+ action: null,
263
+ cleanup,
264
+ toString: () => {
265
+ return `route "${urlPatternInput}"`;
266
+ },
267
+ replaceParams: undefined,
268
+ };
269
+ routeSet.add(route);
270
+
271
+ const routePrivateProperties = {
272
+ urlPattern: undefined,
273
+ activeSignal: null,
274
+ paramsSignal: null,
275
+ visitedSignal: null,
276
+ relativeUrlSignal: null,
277
+ urlSignal: null,
278
+ };
279
+ routePrivatePropertiesMap.set(route, routePrivateProperties);
280
+
281
+ const buildRelativeUrl = (params = {}) => {
282
+ let relativeUrl = urlPatternInput;
283
+ // Replace named parameters (:param and {param})
284
+ for (const key of Object.keys(params)) {
285
+ const value = params[key];
286
+ const encodedValue = encodeURIComponent(value);
287
+ relativeUrl = relativeUrl.replace(`:${key}`, encodedValue);
288
+ relativeUrl = relativeUrl.replace(`{${key}}`, encodedValue);
289
+ }
290
+ // Replace wildcards (*) with numbered parameters (0, 1, 2, etc.)
291
+ let wildcardIndex = 0;
292
+ relativeUrl = relativeUrl.replace(/\*/g, () => {
293
+ const paramKey = wildcardIndex.toString();
294
+ const replacement = params[paramKey]
295
+ ? encodeURIComponent(params[paramKey])
296
+ : "*";
297
+ wildcardIndex++;
298
+ return replacement;
299
+ });
300
+ return relativeUrl;
301
+ };
302
+ const buildUrl = (params = {}) => {
303
+ let relativeUrl = buildRelativeUrl(params);
304
+ if (relativeUrl[0] === "/") {
305
+ relativeUrl = relativeUrl.slice(1);
306
+ }
307
+ const url = new URL(relativeUrl, baseUrl).href;
308
+ return url;
309
+ };
310
+ route.buildUrl = buildUrl;
311
+
312
+ const activeSignal = signal(false);
313
+ const paramsSignal = signal(NO_PARAMS);
314
+ const visitedSignal = signal(false);
315
+ const relativeUrlSignal = computed(() => {
316
+ const params = paramsSignal.value;
317
+ const relativeUrl = buildRelativeUrl(params);
318
+ return relativeUrl;
319
+ });
320
+ const disposeRelativeUrlEffect = effect(() => {
321
+ route.relativeUrl = relativeUrlSignal.value;
322
+ });
323
+ cleanupCallbackSet.add(disposeRelativeUrlEffect);
324
+
325
+ const urlSignal = computed(() => {
326
+ const relativeUrl = relativeUrlSignal.value;
327
+ const url = new URL(relativeUrl, baseUrl).href;
328
+ return url;
329
+ });
330
+ const disposeUrlEffect = effect(() => {
331
+ route.url = urlSignal.value;
332
+ });
333
+ cleanupCallbackSet.add(disposeUrlEffect);
334
+
335
+ const replaceParams = (newParams) => {
336
+ const currentParams = paramsSignal.peek();
337
+ const updatedParams = { ...currentParams, ...newParams };
338
+ const updatedUrl = route.buildUrl(updatedParams);
339
+ if (route.action) {
340
+ route.action.replaceParams(updatedParams);
341
+ }
342
+ browserIntegration.goTo(updatedUrl, { replace: true });
343
+ };
344
+ route.replaceParams = replaceParams;
345
+
346
+ const bindAction = (action) => {
347
+ /*
348
+ *
349
+ * here I need to check the store for that action (if any)
350
+ * and listen store changes to do this:
351
+ *
352
+ * When we detect changes we want to update the route params
353
+ * so we'll need to use goTo(buildUrl(params), { replace: true })
354
+ *
355
+ * reinserted is useful because the item id might have changed
356
+ * but not the mutable key
357
+ *
358
+ */
359
+
360
+ const { store } = action.meta;
361
+ if (store) {
362
+ const { mutableIdKeys } = store;
363
+ if (mutableIdKeys.length) {
364
+ const mutableIdKey = mutableIdKeys[0];
365
+ const mutableIdValueSignal = computed(() => {
366
+ const params = paramsSignal.value;
367
+ const mutableIdValue = params[mutableIdKey];
368
+ return mutableIdValue;
369
+ });
370
+ const routeItemSignal = store.signalForMutableIdKey(
371
+ mutableIdKey,
372
+ mutableIdValueSignal,
373
+ );
374
+ store.observeProperties(routeItemSignal, (propertyMutations) => {
375
+ const mutableIdPropertyMutation = propertyMutations[mutableIdKey];
376
+ if (!mutableIdPropertyMutation) {
377
+ return;
378
+ }
379
+ route.replaceParams({
380
+ [mutableIdKey]: mutableIdPropertyMutation.newValue,
381
+ });
382
+ });
383
+ }
384
+ }
385
+
386
+ /*
387
+ store.registerPropertyLifecycle(activeItemSignal, key, {
388
+ changed: (value) => {
389
+ route.replaceParams({
390
+ [key]: value,
391
+ });
392
+ },
393
+ dropped: () => {
394
+ route.reload();
395
+ },
396
+ reinserted: () => {
397
+ // this will reload all routes which works but
398
+ // - most of the time only "route" is impacted, any other route could stay as is
399
+ // - we already have the data, reloading the route will refetch the backend which is unnecessary
400
+ // we could just remove routing error (which is cause by 404 likely)
401
+ // to actually let the data be displayed
402
+ // because they are available, but in reality the route has no data
403
+ // because the fetch failed
404
+ // so conceptually reloading is fine,
405
+ // the only thing that bothers me a little is that it reloads all routes
406
+ route.reload();
407
+ },
408
+ });
409
+ */
410
+
411
+ const actionBoundToThisRoute = action.bindParams(paramsSignal);
412
+ route.action = actionBoundToThisRoute;
413
+ return actionBoundToThisRoute;
414
+ };
415
+ route.bindAction = bindAction;
416
+
417
+ private_properties: {
418
+ // Remove leading slash from urlPattern to make it relative to baseUrl
419
+ const normalizedUrlPattern = urlPatternInput.startsWith("/")
420
+ ? urlPatternInput.slice(1)
421
+ : urlPatternInput;
422
+ const urlPattern = new URLPattern(normalizedUrlPattern, baseUrl, {
423
+ ignoreCase: true,
424
+ });
425
+ routePrivateProperties.urlPattern = urlPattern;
426
+ routePrivateProperties.activeSignal = activeSignal;
427
+ routePrivateProperties.paramsSignal = paramsSignal;
428
+ routePrivateProperties.visitedSignal = visitedSignal;
429
+ routePrivateProperties.relativeUrlSignal = relativeUrlSignal;
430
+ routePrivateProperties.urlSignal = urlSignal;
431
+ routePrivateProperties.cleanupCallbackSet = cleanupCallbackSet;
432
+ }
433
+
434
+ return route;
435
+ };
436
+ export const useRouteStatus = (route) => {
437
+ if (import.meta.dev && (!route || !route.isRoute)) {
438
+ throw new TypeError(
439
+ `useRouteStatus() requires a route object, but received ${route}.`,
440
+ );
441
+ }
442
+ const routePrivateProperties = getRoutePrivateProperties(route);
443
+ if (!routePrivateProperties) {
444
+ if (import.meta.dev) {
445
+ let errorMessage = `Cannot find route private properties for ${route}.`;
446
+
447
+ errorMessage += `\nThis might be caused by hot reloading - try refreshing the page.`;
448
+ throw new Error(errorMessage);
449
+ }
450
+ throw new Error(`Cannot find route private properties for ${route}`);
451
+ }
452
+
453
+ const { activeSignal, paramsSignal, visitedSignal } = routePrivateProperties;
454
+
455
+ const active = activeSignal.value;
456
+ const params = paramsSignal.value;
457
+ const visited = visitedSignal.value;
458
+
459
+ return {
460
+ active,
461
+ params,
462
+ visited,
463
+ };
464
+ };
465
+
466
+ let browserIntegration;
467
+ export const setBrowserIntegration = (integration) => {
468
+ browserIntegration = integration;
469
+ };
470
+
471
+ let onRouteDefined = () => {};
472
+ export const setOnRouteDefined = (v) => {
473
+ onRouteDefined = v;
474
+ };
475
+ /**
476
+ * Define all routes for the application.
477
+ *
478
+ * ⚠️ HOT RELOAD WARNING: When destructuring the returned routes, use 'let' instead of 'const'
479
+ * to allow hot reload to update the route references:
480
+ *
481
+ * ❌ const [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
482
+ * ✅ let [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
483
+ *
484
+ * @param {Object} routeDefinition - Object mapping URL patterns to actions
485
+ * @returns {Array} Array of route objects in the same order as the keys
486
+ */
487
+ // All routes MUST be created at once because any url can be accessed
488
+ // at any given time (url can be shared, reloaded, etc..)
489
+ // Later I'll consider adding ability to have dynamic import into the mix
490
+ // (An async function returning an action)
491
+ export const defineRoutes = (routeDefinition) => {
492
+ // Clean up existing routes
493
+ for (const route of routeSet) {
494
+ route.cleanup();
495
+ }
496
+ routeSet.clear();
497
+
498
+ const routeArray = [];
499
+ for (const key of Object.keys(routeDefinition)) {
500
+ const value = routeDefinition[key];
501
+ const route = createRoute(key);
502
+ if (value && value.isAction) {
503
+ route.bindAction(value);
504
+ } else if (typeof value === "function") {
505
+ const actionFromFunction = createAction(value);
506
+ route.bindAction(actionFromFunction);
507
+ } else if (value) {
508
+ route.bindAction(value);
509
+ }
510
+ routeArray.push(route);
511
+ }
512
+ onRouteDefined();
513
+
514
+ return routeArray;
515
+ };
516
+
517
+ // unit test exports
518
+ export { createRoute };