@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,95 @@
1
+ // Example test demonstrating the dependency system
2
+ import { resource } from "../resource_graph.js";
3
+
4
+ // Create test resources
5
+ const role = resource("role", {
6
+ GET_MANY: () =>
7
+ Promise.resolve([
8
+ { id: 1, name: "admin" },
9
+ { id: 2, name: "user" },
10
+ ]),
11
+ POST: (data) => Promise.resolve({ id: Date.now(), ...data }),
12
+ DELETE: (id) => Promise.resolve(id),
13
+ });
14
+
15
+ const database = resource("database", {
16
+ GET_MANY: () =>
17
+ Promise.resolve([
18
+ { id: 1, name: "main_db" },
19
+ { id: 2, name: "test_db" },
20
+ ]),
21
+ POST: (data) => Promise.resolve({ id: Date.now(), ...data }),
22
+ DELETE: (id) => Promise.resolve(id),
23
+ });
24
+
25
+ const tables = resource("tables", {
26
+ GET_MANY: () =>
27
+ Promise.resolve([
28
+ { id: 1, name: "users", database_id: 1 },
29
+ { id: 2, name: "roles", database_id: 1 },
30
+ ]),
31
+ POST: (data) => Promise.resolve({ id: Date.now(), ...data }),
32
+ DELETE: (id) => Promise.resolve(id),
33
+ });
34
+
35
+ // Create parameterized resource with dependencies
36
+ // This will autoreload when any of the dependency resources are modified
37
+ const ROLE_WITH_OWNERSHIP = role.withParams(
38
+ { owners: true },
39
+ {
40
+ dependencies: [role, database, tables],
41
+ },
42
+ );
43
+
44
+ // Demonstrate the functionality
45
+ console.log("🚀 Dependency system demo");
46
+ console.log(
47
+ `✅ Created ROLE_WITH_OWNERSHIP with ${ROLE_WITH_OWNERSHIP.dependencies.length} dependencies`,
48
+ );
49
+ console.log(
50
+ `📋 Dependencies: ${ROLE_WITH_OWNERSHIP.dependencies.map((d) => d.name).join(", ")}`,
51
+ );
52
+
53
+ // Test the autoreload mechanism
54
+ async function testAutoreload() {
55
+ console.log("\n🧪 Testing autoreload mechanism...");
56
+
57
+ try {
58
+ // Load the parameterized resource
59
+ const ownershipData = await ROLE_WITH_OWNERSHIP.GET_MANY.load();
60
+ console.log(
61
+ "✅ ROLE_WITH_OWNERSHIP.GET_MANY.load() executed:",
62
+ ownershipData.length,
63
+ "items",
64
+ );
65
+
66
+ // Trigger dependency changes that should cause autoreload
67
+ console.log("\n📝 Triggering dependency changes...");
68
+
69
+ await tables.POST.load({ name: "new_table", database_id: 1 });
70
+ console.log(
71
+ "✅ tables.POST.load() - should trigger ROLE_WITH_OWNERSHIP autoreload",
72
+ );
73
+
74
+ await database.POST.load({ name: "new_database" });
75
+ console.log(
76
+ "✅ database.POST.load() - should trigger ROLE_WITH_OWNERSHIP autoreload",
77
+ );
78
+
79
+ await role.POST.load({ name: "new_role" });
80
+ console.log(
81
+ "✅ role.POST.load() - should trigger ROLE_WITH_OWNERSHIP autoreload",
82
+ );
83
+
84
+ console.log("\n✅ Dependency system test completed successfully!");
85
+ console.log(
86
+ "💡 In a real application, these would automatically reload ROLE_WITH_OWNERSHIP.GET_MANY",
87
+ );
88
+ } catch (error) {
89
+ console.error("❌ Test failed:", error);
90
+ }
91
+ }
92
+
93
+ testAutoreload();
94
+
95
+ export { database, role, ROLE_WITH_OWNERSHIP, tables };
@@ -0,0 +1,187 @@
1
+ export const valueInLocalStorage = (key, options = {}) => {
2
+ const { type = "string" } = options;
3
+ const converter = typeConverters[type];
4
+ if (converter === undefined) {
5
+ console.warn(
6
+ `Invalid type "${type}" for "${key}" in local storage, expected one of ${Object.keys(
7
+ typeConverters,
8
+ ).join(", ")}`,
9
+ );
10
+ }
11
+ const getValidityMessage = (
12
+ valueToCheck,
13
+ valueInLocalStorage = valueToCheck,
14
+ ) => {
15
+ if (!converter) {
16
+ return "";
17
+ }
18
+ if (!converter.checkValidity) {
19
+ return "";
20
+ }
21
+ const checkValidityResult = converter.checkValidity(valueToCheck);
22
+ if (checkValidityResult === false) {
23
+ return `${valueInLocalStorage}`;
24
+ }
25
+ if (!checkValidityResult) {
26
+ return "";
27
+ }
28
+ return `${checkValidityResult}, got "${valueInLocalStorage}"`;
29
+ };
30
+
31
+ const get = () => {
32
+ let valueInLocalStorage = window.localStorage.getItem(key);
33
+ if (valueInLocalStorage === null) {
34
+ return Object.hasOwn(options, "default") ? options.default : undefined;
35
+ }
36
+ if (converter && converter.decode) {
37
+ const valueDecoded = converter.decode(valueInLocalStorage);
38
+ const validityMessage = getValidityMessage(
39
+ valueDecoded,
40
+ valueInLocalStorage,
41
+ );
42
+ if (validityMessage) {
43
+ console.warn(
44
+ `The value found in localStorage "${key}" is invalid: ${validityMessage}`,
45
+ );
46
+ return undefined;
47
+ }
48
+ return valueDecoded;
49
+ }
50
+ const validityMessage = getValidityMessage(valueInLocalStorage);
51
+ if (validityMessage) {
52
+ console.warn(
53
+ `The value found in localStorage "${key}" is invalid: ${validityMessage}`,
54
+ );
55
+ return undefined;
56
+ }
57
+ return valueInLocalStorage;
58
+ };
59
+ const set = (value) => {
60
+ if (value === undefined) {
61
+ window.localStorage.removeItem(key);
62
+ return;
63
+ }
64
+ const validityMessage = getValidityMessage(value);
65
+ if (validityMessage) {
66
+ console.warn(
67
+ `The value to set in localStorage "${key}" is invalid: ${validityMessage}`,
68
+ );
69
+ }
70
+ if (converter && converter.encode) {
71
+ const valueEncoded = converter.encode(value);
72
+ window.localStorage.setItem(key, valueEncoded);
73
+ return;
74
+ }
75
+ window.localStorage.setItem(key, value);
76
+ };
77
+ const remove = () => {
78
+ window.localStorage.removeItem(key);
79
+ };
80
+
81
+ return [get, set, remove];
82
+ };
83
+
84
+ const typeConverters = {
85
+ boolean: {
86
+ checkValidity: (value) => {
87
+ if (typeof value !== "boolean") {
88
+ return `must be a boolean`;
89
+ }
90
+ return "";
91
+ },
92
+ decode: (value) => {
93
+ return value === "true";
94
+ },
95
+ },
96
+ string: {
97
+ checkValidity: (value) => {
98
+ if (typeof value !== "string") {
99
+ return `must be a string`;
100
+ }
101
+ return "";
102
+ },
103
+ },
104
+ number: {
105
+ decode: (value) => {
106
+ const valueParsed = parseFloat(value);
107
+ return valueParsed;
108
+ },
109
+ checkValidity: (value) => {
110
+ if (typeof value !== "number") {
111
+ return `must be a number`;
112
+ }
113
+ if (!Number.isFinite(value)) {
114
+ return `must be finite`;
115
+ }
116
+ return "";
117
+ },
118
+ },
119
+ positive_number: {
120
+ decode: (value) => {
121
+ const valueParsed = parseFloat(value);
122
+ return valueParsed;
123
+ },
124
+ checkValidity: (value) => {
125
+ if (typeof value !== "number") {
126
+ return `must be a number`;
127
+ }
128
+ if (value < 0) {
129
+ return `must be positive`;
130
+ }
131
+ return "";
132
+ },
133
+ },
134
+ positive_integer: {
135
+ decode: (value) => {
136
+ const valueParsed = parseInt(value, 10);
137
+ return valueParsed;
138
+ },
139
+ checkValidity: (value) => {
140
+ if (typeof value !== "number") {
141
+ return `must be a number`;
142
+ }
143
+ if (!Number.isInteger(value)) {
144
+ return `must be an integer`;
145
+ }
146
+ if (value < 0) {
147
+ return `must be positive`;
148
+ }
149
+ return "";
150
+ },
151
+ },
152
+ percentage: {
153
+ checkValidity: (value) => {
154
+ if (typeof value !== "string") {
155
+ return `must be a percentage`;
156
+ }
157
+ if (!value.endsWith("%")) {
158
+ return `must end with %`;
159
+ }
160
+ const percentageString = value.slice(0, -1);
161
+ const percentageFloat = parseFloat(percentageString);
162
+ if (typeof percentageFloat !== "number") {
163
+ return `must be a percentage`;
164
+ }
165
+ if (percentageFloat < 0 || percentageFloat > 100) {
166
+ return `must be between 0 and 100`;
167
+ }
168
+ return "";
169
+ },
170
+ },
171
+ object: {
172
+ decode: (value) => {
173
+ const valueParsed = JSON.parse(value);
174
+ return valueParsed;
175
+ },
176
+ encode: (value) => {
177
+ const valueStringified = JSON.stringify(value);
178
+ return valueStringified;
179
+ },
180
+ checkValidity: (value) => {
181
+ if (value === null || typeof value !== "object") {
182
+ return `must be an object`;
183
+ }
184
+ return "";
185
+ },
186
+ },
187
+ };
@@ -0,0 +1 @@
1
+ export const SYMBOL_OBJECT_SIGNAL = Symbol.for("navi_object_signal");
@@ -0,0 +1,10 @@
1
+ import { getActionPrivateProperties } from "./action_private_properties.js";
2
+
3
+ export const useActionData = (action) => {
4
+ if (!action) {
5
+ return undefined;
6
+ }
7
+ const { computedDataSignal } = getActionPrivateProperties(action);
8
+ const data = computedDataSignal.value;
9
+ return data;
10
+ };
@@ -0,0 +1,47 @@
1
+ import { getActionPrivateProperties } from "./action_private_properties.js";
2
+ import { ABORTED, COMPLETED, IDLE, RUNNING } from "./action_run_states.js";
3
+
4
+ export const useActionStatus = (action) => {
5
+ if (!action) {
6
+ return {
7
+ params: undefined,
8
+ runningState: IDLE,
9
+ isPrerun: false,
10
+ idle: true,
11
+ loading: false,
12
+ aborted: false,
13
+ error: null,
14
+ completed: false,
15
+ data: undefined,
16
+ };
17
+ }
18
+ const {
19
+ paramsSignal,
20
+ runningStateSignal,
21
+ isPrerunSignal,
22
+ errorSignal,
23
+ computedDataSignal,
24
+ } = getActionPrivateProperties(action);
25
+
26
+ const params = paramsSignal.value;
27
+ const isPrerun = isPrerunSignal.value;
28
+ const runningState = runningStateSignal.value;
29
+ const idle = runningState === IDLE;
30
+ const aborted = runningState === ABORTED;
31
+ const error = errorSignal.value;
32
+ const loading = runningState === RUNNING;
33
+ const completed = runningState === COMPLETED;
34
+ const data = computedDataSignal.value;
35
+
36
+ return {
37
+ params,
38
+ runningState,
39
+ isPrerun,
40
+ idle,
41
+ loading,
42
+ aborted,
43
+ error,
44
+ completed,
45
+ data,
46
+ };
47
+ };
@@ -0,0 +1,15 @@
1
+ export const addManyEventListeners = (element, events) => {
2
+ const cleanupCallbackSet = new Set();
3
+ for (const event of Object.keys(events)) {
4
+ const callback = events[event];
5
+ element.addEventListener(event, callback);
6
+ cleanupCallbackSet.add(() => {
7
+ element.removeEventListener(event, callback);
8
+ });
9
+ }
10
+ return () => {
11
+ for (const cleanupCallback of cleanupCallbackSet) {
12
+ cleanupCallback();
13
+ }
14
+ };
15
+ };
@@ -0,0 +1,61 @@
1
+ export const addIntoArray = (array, ...valuesToAdd) => {
2
+ if (valuesToAdd.length === 1) {
3
+ const [valueToAdd] = valuesToAdd;
4
+ const arrayWithThisValue = [];
5
+ for (const value of array) {
6
+ if (value === valueToAdd) {
7
+ return array;
8
+ }
9
+ arrayWithThisValue.push(value);
10
+ }
11
+ arrayWithThisValue.push(valueToAdd);
12
+ return arrayWithThisValue;
13
+ }
14
+
15
+ const existingValueSet = new Set();
16
+ const arrayWithTheseValues = [];
17
+ for (const existingValue of array) {
18
+ arrayWithTheseValues.push(existingValue);
19
+ existingValueSet.add(existingValue);
20
+ }
21
+ let hasNewValues = false;
22
+ for (const valueToAdd of valuesToAdd) {
23
+ if (existingValueSet.has(valueToAdd)) {
24
+ continue;
25
+ }
26
+ arrayWithTheseValues.push(valueToAdd);
27
+ hasNewValues = true;
28
+ }
29
+ return hasNewValues ? arrayWithTheseValues : array;
30
+ };
31
+
32
+ export const removeFromArray = (array, ...valuesToRemove) => {
33
+ if (valuesToRemove.length === 1) {
34
+ const [valueToRemove] = valuesToRemove;
35
+ const arrayWithoutThisValue = [];
36
+ let found = false;
37
+ for (const value of array) {
38
+ if (value === valueToRemove) {
39
+ found = true;
40
+ continue;
41
+ }
42
+ arrayWithoutThisValue.push(value);
43
+ }
44
+ if (!found) {
45
+ return array;
46
+ }
47
+ return arrayWithoutThisValue;
48
+ }
49
+
50
+ const valuesToRemoveSet = new Set(valuesToRemove);
51
+ const arrayWithoutTheseValues = [];
52
+ let hasRemovedValues = false;
53
+ for (const value of array) {
54
+ if (valuesToRemoveSet.has(value)) {
55
+ hasRemovedValues = true;
56
+ continue;
57
+ }
58
+ arrayWithoutTheseValues.push(value);
59
+ }
60
+ return hasRemovedValues ? arrayWithoutTheseValues : array;
61
+ };
@@ -0,0 +1,15 @@
1
+ import { signal } from "@preact/signals";
2
+ import { addIntoArray, removeFromArray } from "./array_add_remove.js";
3
+
4
+ export const arraySignal = (initialValue = []) => {
5
+ const theSignal = signal(initialValue);
6
+
7
+ const add = (...args) => {
8
+ theSignal.value = addIntoArray(theSignal.peek(), ...args);
9
+ };
10
+ const remove = (...args) => {
11
+ theSignal.value = removeFromArray(theSignal.peek(), ...args);
12
+ };
13
+
14
+ return [theSignal, add, remove];
15
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Deep equality comparison for JavaScript values with cycle detection and identity optimization.
3
+ *
4
+ * This function performs a comprehensive deep comparison between two JavaScript values,
5
+ * handling all primitive types, objects, arrays, and edge cases that standard equality
6
+ * operators miss.
7
+ *
8
+ * Key features:
9
+ * - **Deep comparison**: Recursively compares nested objects and arrays
10
+ * - **Cycle detection**: Prevents infinite loops with circular references
11
+ * - **Identity optimization**: Uses SYMBOL_IDENTITY for fast comparison of objects with same identity
12
+ * - **Edge case handling**: Properly handles NaN, null, undefined, 0, false comparisons
13
+ * - **Type safety**: Ensures both values have same type before deep comparison
14
+ *
15
+ * Performance optimizations:
16
+ * - Early exit for reference equality (a === b)
17
+ * - Identity symbol check for objects (avoids deep comparison when possible)
18
+ * - Efficient array length check before element-by-element comparison
19
+ *
20
+ * **SYMBOL_IDENTITY explained**:
21
+ * This symbol allows recognizing objects as "conceptually the same" even when they are
22
+ * different object instances. When two objects share the same SYMBOL_IDENTITY value,
23
+ * they are considered equal without performing deep comparison.
24
+ *
25
+ * This is particularly useful for:
26
+ * - Copied objects that should be treated as the same entity
27
+ * - Objects reconstructed from serialization that represent the same data
28
+ * - Parameters passed through spread operator: `{ ...originalParams, newProp: value }`
29
+ * - Memoization scenarios where object content identity matters more than reference identity
30
+ *
31
+ * Use cases:
32
+ * - Memoization cache key comparison ({ id: 1 } should equal { id: 1 })
33
+ * - React/Preact dependency comparison for effects and memos
34
+ * - State change detection in signals and stores
35
+ * - Action parameter comparison for avoiding duplicate requests
36
+ * - Object recognition across serialization/deserialization boundaries
37
+ *
38
+ * Examples:
39
+ * ```js
40
+ * // Standard deep comparison
41
+ * compareTwoJsValues({ id: 1 }, { id: 1 }) // true (slow - deep comparison)
42
+ *
43
+ * // NaN edge case handling
44
+ * compareTwoJsValues(NaN, NaN) // true (unlike === which gives false)
45
+ *
46
+ * // Identity optimization - objects are different instances but same identity
47
+ * const originalParams = { userId: 123, filters: ['active'] };
48
+ * const copiedParams = { ...originalParams, newFlag: true };
49
+ *
50
+ * // Without SYMBOL_IDENTITY: slow deep comparison every time
51
+ * compareTwoJsValues(originalParams, copiedParams) // false (different content)
52
+ *
53
+ * // With SYMBOL_IDENTITY: fast path recognition
54
+ * const sharedIdentity = Symbol('params-identity');
55
+ * originalParams[SYMBOL_IDENTITY] = sharedIdentity;
56
+ * copiedParams[SYMBOL_IDENTITY] = sharedIdentity;
57
+ *
58
+ * compareTwoJsValues(originalParams, copiedParams) // true (fast - identity match)
59
+ * // ↑ This returns true immediately without comparing all properties
60
+ *
61
+ * // Real-world scenario: action memoization
62
+ * const params1 = { userId: 123 };
63
+ * const action1 = createAction(params1);
64
+ *
65
+ * const params2 = { ...params1, extra: 'data' }; // Different object reference
66
+ * params2[SYMBOL_IDENTITY] = params1[SYMBOL_IDENTITY]; // Same conceptual identity
67
+ *
68
+ * const action2 = createAction(params2);
69
+ * // action1 === action2 because params are recognized as conceptually identical
70
+ * ```
71
+ *
72
+ * @param {any} a - First value to compare
73
+ * @param {any} b - Second value to compare
74
+ * @param {Set} seenSet - Internal cycle detection set (automatically managed)
75
+ * @returns {boolean} true if values are deeply equal, false otherwise
76
+ */
77
+
78
+ /**
79
+ * Symbol used to mark objects with a conceptual identity that transcends reference equality.
80
+ *
81
+ * When two different object instances share the same SYMBOL_IDENTITY value, they are
82
+ * considered equal by compareTwoJsValues without performing expensive deep comparison.
83
+ *
84
+ * This enables recognition of "the same logical object" even when:
85
+ * - The object has been copied via spread operator: `{ ...obj, newProp }`
86
+ * - The object has been reconstructed from serialization
87
+ * - The object is a different instance but represents the same conceptual entity
88
+ *
89
+ * Use Symbol.for() to ensure the same symbol across different modules/contexts.
90
+ */
91
+ export const SYMBOL_IDENTITY = Symbol.for("navi_object_identity");
92
+
93
+ export const compareTwoJsValues = (a, b, seenSet = new Set()) => {
94
+ if (a === b) {
95
+ return true;
96
+ }
97
+ const aIsIsTruthy = Boolean(a);
98
+ const bIsTruthy = Boolean(b);
99
+ if (aIsIsTruthy && !bIsTruthy) {
100
+ return false;
101
+ }
102
+ if (!aIsIsTruthy && !bIsTruthy) {
103
+ // null, undefined, 0, false, NaN
104
+ if (isNaN(a) && isNaN(b)) {
105
+ return true;
106
+ }
107
+ return a === b;
108
+ }
109
+ const aType = typeof a;
110
+ const bType = typeof b;
111
+ if (aType !== bType) {
112
+ return false;
113
+ }
114
+ const aIsPrimitive =
115
+ a === null || (aType !== "object" && aType !== "function");
116
+ const bIsPrimitive =
117
+ b === null || (bType !== "object" && bType !== "function");
118
+ if (aIsPrimitive !== bIsPrimitive) {
119
+ return false;
120
+ }
121
+ if (aIsPrimitive && bIsPrimitive) {
122
+ return a === b;
123
+ }
124
+ if (seenSet.has(a)) {
125
+ return false;
126
+ }
127
+ if (seenSet.has(b)) {
128
+ return false;
129
+ }
130
+ seenSet.add(a);
131
+ seenSet.add(b);
132
+ const aIsArray = Array.isArray(a);
133
+ const bIsArray = Array.isArray(b);
134
+ if (aIsArray !== bIsArray) {
135
+ return false;
136
+ }
137
+ if (aIsArray) {
138
+ // compare arrays
139
+ if (a.length !== b.length) {
140
+ return false;
141
+ }
142
+ let i = 0;
143
+ while (i < a.length) {
144
+ const aValue = a[i];
145
+ const bValue = b[i];
146
+ if (!compareTwoJsValues(aValue, bValue, seenSet)) {
147
+ return false;
148
+ }
149
+ i++;
150
+ }
151
+ return true;
152
+ }
153
+ // compare objects
154
+ const aIdentity = a[SYMBOL_IDENTITY];
155
+ const bIdentity = b[SYMBOL_IDENTITY];
156
+ if (aIdentity === bIdentity && SYMBOL_IDENTITY in a && SYMBOL_IDENTITY in b) {
157
+ return true;
158
+ }
159
+ const aKeys = Object.keys(a);
160
+ const bKeys = Object.keys(b);
161
+ if (aKeys.length !== bKeys.length) {
162
+ return false;
163
+ }
164
+ for (const key of aKeys) {
165
+ const aValue = a[key];
166
+ const bValue = b[key];
167
+ if (!compareTwoJsValues(aValue, bValue, seenSet)) {
168
+ return false;
169
+ }
170
+ }
171
+ return true;
172
+ };
@@ -0,0 +1,21 @@
1
+ export const executeWithCleanup = (fn, cleanup) => {
2
+ let isThenable;
3
+ try {
4
+ const result = fn();
5
+ isThenable = result && typeof result.then === "function";
6
+ if (isThenable) {
7
+ return (async () => {
8
+ try {
9
+ return await result;
10
+ } finally {
11
+ cleanup();
12
+ }
13
+ })();
14
+ }
15
+ return result;
16
+ } finally {
17
+ if (!isThenable) {
18
+ cleanup();
19
+ }
20
+ }
21
+ };