@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
package/src/actions.js ADDED
@@ -0,0 +1,1377 @@
1
+ import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
2
+ import { batch, computed, effect, signal } from "@preact/signals";
3
+ import {
4
+ getActionPrivateProperties,
5
+ setActionPrivateProperties,
6
+ } from "./action_private_properties.js";
7
+ import {
8
+ ABORTED,
9
+ COMPLETED,
10
+ FAILED,
11
+ IDLE,
12
+ RUNNING,
13
+ } from "./action_run_states.js";
14
+ import { SYMBOL_OBJECT_SIGNAL } from "./symbol_object_signal.js";
15
+ import { createIterableWeakSet } from "./utils/iterable_weak_set.js";
16
+ import { createJsValueWeakMap } from "./utils/js_value_weak_map.js";
17
+ import { mergeTwoJsValues } from "./utils/merge_two_js_values.js";
18
+ import {
19
+ isSignal,
20
+ stringifyForDisplay,
21
+ } from "./utils/stringify_for_display.js";
22
+ import { weakEffect } from "./utils/weak_effect.js";
23
+
24
+ const ACTION_AS_FUNCTION = true;
25
+ let DEBUG = false;
26
+ export const enableDebugActions = () => {
27
+ DEBUG = true;
28
+ };
29
+
30
+ let dispatchActions = (params) => {
31
+ const { requestedResult } = updateActions({
32
+ globalAbortSignal: new AbortController().signal,
33
+ abortSignal: new AbortController().signal,
34
+ ...params,
35
+ });
36
+ return requestedResult;
37
+ };
38
+
39
+ const dispatchSingleAction = (action, method, options) => {
40
+ const requestedResult = dispatchActions({
41
+ prerunSet: method === "prerun" ? new Set([action]) : undefined,
42
+ runSet: method === "run" ? new Set([action]) : undefined,
43
+ rerunSet: method === "rerun" ? new Set([action]) : undefined,
44
+ resetSet: method === "reset" ? new Set([action]) : undefined,
45
+ ...options,
46
+ });
47
+ if (requestedResult && typeof requestedResult.then === "function") {
48
+ return requestedResult.then((resolvedResult) =>
49
+ resolvedResult ? resolvedResult[0] : undefined,
50
+ );
51
+ }
52
+ return requestedResult ? requestedResult[0] : undefined;
53
+ };
54
+ export const setActionDispatcher = (value) => {
55
+ dispatchActions = value;
56
+ };
57
+
58
+ export const getActionDispatcher = () => dispatchActions;
59
+
60
+ export const rerunActions = async (
61
+ actionSet,
62
+ { reason = "rerunActions was called" } = {},
63
+ ) => {
64
+ return dispatchActions({
65
+ rerunSet: actionSet,
66
+ reason,
67
+ });
68
+ };
69
+
70
+ export const resetActions = async (
71
+ actionSet,
72
+ { reason = "resetActions was called" } = {},
73
+ ) => {
74
+ return dispatchActions({
75
+ resetSet: actionSet,
76
+ reason,
77
+ });
78
+ };
79
+ export const abortRunningActions = (
80
+ reason = "abortRunningActions was called",
81
+ ) => {
82
+ const { runningSet } = getActivationInfo();
83
+ for (const runningAction of runningSet) {
84
+ runningAction.abort(reason);
85
+ }
86
+ };
87
+
88
+ /**
89
+ * Registry that prevents prerun actions from being garbage collected.
90
+ *
91
+ * When an action is prerun, it might not have any active references yet
92
+ * (e.g., the component that will use it hasn't loaded yet due to dynamic imports).
93
+ * This registry keeps a reference to prerun actions for a configurable duration
94
+ * to ensure they remain available when needed.
95
+ *
96
+ * Actions are automatically unprotected when:
97
+ * - The protection duration expires (default: 5 minutes)
98
+ * - The action is explicitly stopped via .stop()
99
+ */
100
+ const prerunProtectionRegistry = (() => {
101
+ const protectedActionMap = new Map(); // action -> { timeoutId, timestamp }
102
+ const PROTECTION_DURATION = 5 * 60 * 1000; // 5 minutes en millisecondes
103
+
104
+ const unprotect = (action) => {
105
+ const protection = protectedActionMap.get(action);
106
+ if (protection) {
107
+ clearTimeout(protection.timeoutId);
108
+ protectedActionMap.delete(action);
109
+ if (DEBUG) {
110
+ const elapsed = Date.now() - protection.timestamp;
111
+ console.debug(`"${action}": GC protection removed after ${elapsed}ms`);
112
+ }
113
+ }
114
+ };
115
+
116
+ return {
117
+ protect(action) {
118
+ // Si déjà protégée, étendre la protection
119
+ if (protectedActionMap.has(action)) {
120
+ const existing = protectedActionMap.get(action);
121
+ clearTimeout(existing.timeoutId);
122
+ }
123
+
124
+ const timestamp = Date.now();
125
+ const timeoutId = setTimeout(() => {
126
+ unprotect(action);
127
+ if (DEBUG) {
128
+ console.debug(
129
+ `"${action}": prerun protection expired after ${PROTECTION_DURATION}ms`,
130
+ );
131
+ }
132
+ }, PROTECTION_DURATION);
133
+
134
+ protectedActionMap.set(action, { timeoutId, timestamp });
135
+
136
+ if (DEBUG) {
137
+ console.debug(
138
+ `"${action}": protected from GC for ${PROTECTION_DURATION}ms`,
139
+ );
140
+ }
141
+ },
142
+
143
+ unprotect,
144
+
145
+ isProtected(action) {
146
+ return protectedActionMap.has(action);
147
+ },
148
+
149
+ // Pour debugging
150
+ getProtectedActions() {
151
+ return Array.from(protectedActionMap.keys());
152
+ },
153
+
154
+ // Nettoyage manuel si nécessaire
155
+ clear() {
156
+ for (const [, protection] of protectedActionMap) {
157
+ clearTimeout(protection.timeoutId);
158
+ }
159
+ protectedActionMap.clear();
160
+ },
161
+ };
162
+ })();
163
+
164
+ export const formatActionSet = (actionSet, prefix = "") => {
165
+ let message = "";
166
+ message += `${prefix}`;
167
+ for (const action of actionSet) {
168
+ message += "\n";
169
+ message += prefixFirstAndIndentRemainingLines(String(action), {
170
+ prefix: " -",
171
+ });
172
+ }
173
+ return message;
174
+ };
175
+
176
+ const actionAbortMap = new Map();
177
+ const actionPromiseMap = new Map();
178
+ const activationWeakSet = createIterableWeakSet("activation");
179
+
180
+ const getActivationInfo = () => {
181
+ const runningSet = new Set();
182
+ const settledSet = new Set();
183
+
184
+ for (const action of activationWeakSet) {
185
+ const privateProps = getActionPrivateProperties(action);
186
+ const runningState = privateProps.runningStateSignal.peek();
187
+
188
+ if (runningState === RUNNING) {
189
+ runningSet.add(action);
190
+ } else if (
191
+ runningState === COMPLETED ||
192
+ runningState === FAILED ||
193
+ runningState === ABORTED
194
+ ) {
195
+ settledSet.add(action);
196
+ } else {
197
+ throw new Error(
198
+ `An action in the activation weak set must be RUNNING, ABORTED, FAILED or COMPLETED, found "${runningState.id}" for action "${action}"`,
199
+ );
200
+ }
201
+ }
202
+
203
+ return {
204
+ runningSet,
205
+ settledSet,
206
+ };
207
+ };
208
+
209
+ if (import.meta.dev) {
210
+ window.__actions__ = {
211
+ activationWeakSet,
212
+ getActivationInfo,
213
+ inspectActivations: () => {
214
+ const activations = [];
215
+ for (const action of activationWeakSet) {
216
+ activations.push({
217
+ name: action.name,
218
+ runningState: action.runningState.id,
219
+ error: action.error,
220
+ params: action.params,
221
+ isProxy: action.isProxy || false,
222
+ });
223
+ }
224
+ console.table(activations);
225
+ return activations;
226
+ },
227
+ cleanup: {
228
+ activation: {
229
+ forceCleanup: () => activationWeakSet.forceCleanup(),
230
+ schedule: () => activationWeakSet.schedule(),
231
+ getStats: () => activationWeakSet.getStats(),
232
+ },
233
+ },
234
+ };
235
+ }
236
+
237
+ export const updateActions = ({
238
+ globalAbortSignal,
239
+ abortSignal,
240
+ isReplace = false,
241
+ reason,
242
+ prerunSet = new Set(),
243
+ runSet = new Set(),
244
+ rerunSet = new Set(),
245
+ resetSet = new Set(),
246
+ abortSignalMap = new Map(),
247
+ onComplete,
248
+ onAbort,
249
+ onError,
250
+ } = {}) => {
251
+ /*
252
+ * Action update flow:
253
+ *
254
+ * Input: 4 sets of requested operations
255
+ * - prerunSet: actions to prerun (background, low priority)
256
+ * - runSet: actions to run (user-visible, medium priority)
257
+ * - rerunSet: actions to force rerun (highest priority)
258
+ * - resetSet: actions to reset/clear
259
+ *
260
+ * Priority resolution:
261
+ * - reset always wins (explicit cleanup)
262
+ * - rerun > run > prerun (rerun forces refresh even if already running)
263
+ * - An action in multiple sets triggers warnings in dev mode
264
+ *
265
+ * Output: Internal operation sets that track what will actually happen
266
+ * - willResetSet: actions that will be reset/cleared
267
+ * - willPrerunSet: actions that will be prerun
268
+ * - willRunSet: actions that will be run
269
+ * - willPromoteSet: prerun actions that become run-requested
270
+ * - stays*Set: actions that remain in their current state
271
+ */
272
+
273
+ const { runningSet, settledSet } = getActivationInfo();
274
+
275
+ // Warn about overlapping sets in development
276
+ if (import.meta.dev) {
277
+ const allSets = [
278
+ { name: "prerun", set: prerunSet },
279
+ { name: "run", set: runSet },
280
+ { name: "rerun", set: rerunSet },
281
+ { name: "reset", set: resetSet },
282
+ ];
283
+
284
+ for (let i = 0; i < allSets.length; i++) {
285
+ for (let j = i + 1; j < allSets.length; j++) {
286
+ const setA = allSets[i];
287
+ const setB = allSets[j];
288
+ for (const action of setA.set) {
289
+ if (setB.set.has(action)) {
290
+ console.warn(
291
+ `Action "${action}" is found in both ${setA.name}Set and ${setB.name}Set. This may lead to unexpected behavior.`,
292
+ );
293
+ }
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ if (DEBUG) {
300
+ console.group(`updateActions()`);
301
+ const lines = [
302
+ ...(prerunSet.size ? [formatActionSet(prerunSet, "- prerun:")] : []),
303
+ ...(runSet.size ? [formatActionSet(runSet, "- run:")] : []),
304
+ ...(rerunSet.size ? [formatActionSet(rerunSet, "- rerun:")] : []),
305
+ ...(resetSet.size ? [formatActionSet(resetSet, "- reset:")] : []),
306
+ ];
307
+ console.debug(
308
+ `requested operations:
309
+ ${lines.join("\n")}
310
+ - meta: { reason: ${reason}, isReplace: ${isReplace} }`,
311
+ );
312
+ }
313
+
314
+ // Internal sets that track what operations will actually be performed
315
+ const willResetSet = new Set();
316
+ const willPrerunSet = new Set();
317
+ const willRunSet = new Set();
318
+ const willPromoteSet = new Set(); // prerun -> run requested
319
+ const staysRunningSet = new Set();
320
+ const staysAbortedSet = new Set();
321
+ const staysFailedSet = new Set();
322
+ const staysCompletedSet = new Set();
323
+
324
+ // Step 1: Determine which actions will be reset
325
+ collect_actions_to_reset: {
326
+ for (const actionToReset of resetSet) {
327
+ if (actionToReset.runningState !== IDLE) {
328
+ willResetSet.add(actionToReset);
329
+ }
330
+ }
331
+ }
332
+
333
+ // Step 2: Process prerun, run, and rerun sets
334
+ collect_actions_to_prerun_and_run: {
335
+ const handleActionRequest = (
336
+ action,
337
+ requestType, // "prerun", "run", or "rerun"
338
+ ) => {
339
+ const isPrerun = requestType === "prerun";
340
+ const isRerun = requestType === "rerun";
341
+
342
+ if (
343
+ action.runningState === RUNNING ||
344
+ action.runningState === COMPLETED
345
+ ) {
346
+ // Action is already running/completed
347
+ // By default, we don't interfere with already active actions
348
+ // Unless it's a rerun or the action is also being reset
349
+ if (isRerun || willResetSet.has(action)) {
350
+ // Force reset first, then rerun/run
351
+ willResetSet.add(action);
352
+ if (isPrerun) {
353
+ willPrerunSet.add(action);
354
+ } else {
355
+ willRunSet.add(action);
356
+ }
357
+ }
358
+ // Otherwise, ignore the request (action stays as-is)
359
+ } else if (isPrerun) {
360
+ willPrerunSet.add(action);
361
+ } else {
362
+ willRunSet.add(action);
363
+ }
364
+ };
365
+
366
+ // Process prerunSet (lowest priority)
367
+ for (const actionToPrerun of prerunSet) {
368
+ if (runSet.has(actionToPrerun) || rerunSet.has(actionToPrerun)) {
369
+ // run/rerun wins over prerun - skip prerun
370
+ continue;
371
+ }
372
+ handleActionRequest(actionToPrerun, "prerun");
373
+ }
374
+
375
+ // Process runSet (medium priority)
376
+ for (const actionToRun of runSet) {
377
+ if (rerunSet.has(actionToRun)) {
378
+ // rerun wins over run - skip run
379
+ continue;
380
+ }
381
+ if (actionToRun.isPrerun && actionToRun.runningState !== IDLE) {
382
+ // Special case: action was prerun but not yet requested to run
383
+ // Just promote it to "run requested" without rerunning
384
+ willPromoteSet.add(actionToRun);
385
+ continue;
386
+ }
387
+ handleActionRequest(actionToRun, "run");
388
+ }
389
+
390
+ // Process rerunSet (highest priority)
391
+ for (const actionToRerun of rerunSet) {
392
+ handleActionRequest(actionToRerun, "rerun");
393
+ }
394
+ }
395
+ const allThenableArray = [];
396
+
397
+ // Step 3: Determine which actions will stay in their current state
398
+ collect_actions_that_stay: {
399
+ for (const actionRunning of runningSet) {
400
+ if (willResetSet.has(actionRunning)) {
401
+ // will be reset (aborted), we don't want to wait
402
+ } else if (
403
+ willRunSet.has(actionRunning) ||
404
+ willPrerunSet.has(actionRunning)
405
+ ) {
406
+ // will be run, we'll wait for the new run promise
407
+ } else {
408
+ // an action that was running and not affected by this update
409
+ const actionPromise = actionPromiseMap.get(actionRunning);
410
+ allThenableArray.push(actionPromise);
411
+ staysRunningSet.add(actionRunning);
412
+ }
413
+ }
414
+ for (const actionSettled of settledSet) {
415
+ if (willResetSet.has(actionSettled)) {
416
+ // will be reset
417
+ } else if (actionSettled.runningState === ABORTED) {
418
+ staysAbortedSet.add(actionSettled);
419
+ } else if (actionSettled.runningState === FAILED) {
420
+ staysFailedSet.add(actionSettled);
421
+ } else {
422
+ staysCompletedSet.add(actionSettled);
423
+ }
424
+ }
425
+ }
426
+ if (DEBUG) {
427
+ const lines = [
428
+ ...(willResetSet.size
429
+ ? [formatActionSet(willResetSet, "- will reset:")]
430
+ : []),
431
+ ...(willPrerunSet.size
432
+ ? [formatActionSet(willPrerunSet, "- will prerun:")]
433
+ : []),
434
+ ...(willPromoteSet.size
435
+ ? [formatActionSet(willPromoteSet, "- will promote:")]
436
+ : []),
437
+ ...(willRunSet.size ? [formatActionSet(willRunSet, "- will run:")] : []),
438
+ ...(staysRunningSet.size
439
+ ? [formatActionSet(staysRunningSet, "- stays running:")]
440
+ : []),
441
+ ...(staysAbortedSet.size
442
+ ? [formatActionSet(staysAbortedSet, "- stays aborted:")]
443
+ : []),
444
+ ...(staysFailedSet.size
445
+ ? [formatActionSet(staysFailedSet, "- stays failed:")]
446
+ : []),
447
+ ...(staysCompletedSet.size
448
+ ? [formatActionSet(staysCompletedSet, "- stays completed:")]
449
+ : []),
450
+ ];
451
+ console.debug(`operations that will be performed:
452
+ ${lines.join("\n")}`);
453
+ }
454
+
455
+ // Step 4: Execute resets
456
+ execute_resets: {
457
+ for (const actionToReset of willResetSet) {
458
+ const actionToResetPrivateProperties =
459
+ getActionPrivateProperties(actionToReset);
460
+ actionToResetPrivateProperties.performStop({ reason });
461
+ activationWeakSet.delete(actionToReset);
462
+ }
463
+ }
464
+
465
+ const resultArray = []; // Store results with their execution order
466
+ let hasAsync = false;
467
+
468
+ // Step 5: Execute preruns and runs
469
+ execute_preruns_and_runs: {
470
+ const onActionToRunOrPrerun = (actionToPrerunOrRun, isPrerun) => {
471
+ if (import.meta.dev && actionToPrerunOrRun.isProxy) {
472
+ // maybe remove this check one the API is stable because
473
+ // nothing in the API should allow this to happen
474
+ throw new Error(
475
+ `Proxy should not be reach this point, use the underlying action instead`,
476
+ );
477
+ }
478
+ const actionSpecificSignal = abortSignalMap.get(actionToPrerunOrRun);
479
+ const effectiveSignal = actionSpecificSignal || abortSignal;
480
+
481
+ const actionToRunPrivateProperties =
482
+ getActionPrivateProperties(actionToPrerunOrRun);
483
+ const performRunResult = actionToRunPrivateProperties.performRun({
484
+ globalAbortSignal,
485
+ abortSignal: effectiveSignal,
486
+ reason,
487
+ isPrerun,
488
+ onComplete,
489
+ onAbort,
490
+ onError,
491
+ });
492
+ activationWeakSet.add(actionToPrerunOrRun);
493
+
494
+ if (performRunResult && typeof performRunResult.then === "function") {
495
+ actionPromiseMap.set(actionToPrerunOrRun, performRunResult);
496
+ allThenableArray.push(performRunResult);
497
+ hasAsync = true;
498
+ // Store async result with order info
499
+ resultArray.push({
500
+ type: "async",
501
+ promise: performRunResult,
502
+ });
503
+ } else {
504
+ // Store sync result with order info
505
+ resultArray.push({
506
+ type: "sync",
507
+ result: performRunResult,
508
+ });
509
+ }
510
+ };
511
+
512
+ // Execute preruns
513
+ for (const actionToPrerun of willPrerunSet) {
514
+ onActionToRunOrPrerun(actionToPrerun, true);
515
+ }
516
+
517
+ // Execute runs
518
+ for (const actionToRun of willRunSet) {
519
+ onActionToRunOrPrerun(actionToRun, false);
520
+ }
521
+
522
+ // Execute promotions (prerun -> run requested)
523
+ for (const actionToPromote of willPromoteSet) {
524
+ const actionToPromotePrivateProperties =
525
+ getActionPrivateProperties(actionToPromote);
526
+ actionToPromotePrivateProperties.isPrerunSignal.value = false;
527
+ }
528
+ }
529
+ if (DEBUG) {
530
+ console.groupEnd();
531
+ }
532
+
533
+ // Calculate requestedResult based on the execution results
534
+ let requestedResult;
535
+ if (resultArray.length === 0) {
536
+ requestedResult = null;
537
+ } else if (hasAsync) {
538
+ requestedResult = Promise.all(
539
+ resultArray.map((item) =>
540
+ item.type === "sync" ? item.result : item.promise,
541
+ ),
542
+ );
543
+ } else {
544
+ requestedResult = resultArray.map((item) => item.result);
545
+ }
546
+
547
+ const allResult = allThenableArray.length
548
+ ? Promise.allSettled(allThenableArray)
549
+ : null;
550
+ const runningActionSet = new Set([...willPrerunSet, ...willRunSet]);
551
+ return {
552
+ requestedResult,
553
+ allResult,
554
+ runningActionSet,
555
+ };
556
+ };
557
+
558
+ const NO_PARAMS = {};
559
+ const initialParamsDefault = NO_PARAMS;
560
+ const metaDefault = {};
561
+
562
+ const actionWeakMap = new WeakMap();
563
+ export const createAction = (callback, rootOptions = {}) => {
564
+ const existing = actionWeakMap.get(callback);
565
+ if (existing) {
566
+ return existing;
567
+ }
568
+
569
+ let rootAction;
570
+
571
+ const createActionCore = (
572
+ {
573
+ name = callback.name || "anonymous",
574
+ params = initialParamsDefault,
575
+ isPrerun = true,
576
+ runningState = IDLE,
577
+ aborted = false,
578
+ error = null,
579
+ data,
580
+ computedData,
581
+ compute,
582
+ completed = false,
583
+ renderLoadedAsync,
584
+ sideEffect = () => {},
585
+ keepOldData = false,
586
+ meta = metaDefault,
587
+ dataEffect,
588
+ completeSideEffect,
589
+ },
590
+ { parentAction } = {},
591
+ ) => {
592
+ const initialData = data;
593
+ const paramsSignal = signal(params);
594
+ const isPrerunSignal = signal(isPrerun);
595
+ const runningStateSignal = signal(runningState);
596
+ const errorSignal = signal(error);
597
+ const dataSignal = signal(initialData);
598
+ const computedDataSignal = compute
599
+ ? computed(() => {
600
+ const data = dataSignal.value;
601
+ return compute(data);
602
+ })
603
+ : dataSignal;
604
+ computedData =
605
+ computedData === undefined
606
+ ? compute
607
+ ? compute(data)
608
+ : data
609
+ : computedData;
610
+
611
+ const prerun = (options) => {
612
+ return dispatchSingleAction(action, "prerun", options);
613
+ };
614
+ const run = (options) => {
615
+ return dispatchSingleAction(action, "run", options);
616
+ };
617
+ const rerun = (options) => {
618
+ return dispatchSingleAction(action, "rerun", options);
619
+ };
620
+ /**
621
+ * Stop the action completely - this will:
622
+ * 1. Abort the action if it's currently running
623
+ * 2. Reset the action to IDLE state
624
+ * 3. Clean up any resources and side effects
625
+ * 4. Reset data to initial value (unless keepOldData is true)
626
+ */
627
+ const stop = (options) => {
628
+ return dispatchSingleAction(action, "stop", options);
629
+ };
630
+ const abort = (reason) => {
631
+ if (runningState !== RUNNING) {
632
+ return false;
633
+ }
634
+ const actionAbort = actionAbortMap.get(action);
635
+ if (!actionAbort) {
636
+ return false;
637
+ }
638
+ if (DEBUG) {
639
+ console.log(`"${action}": aborting (reason: ${reason})`);
640
+ }
641
+ actionAbort(reason);
642
+ return true;
643
+ };
644
+
645
+ let action;
646
+
647
+ const childActionWeakSet = createIterableWeakSet("child_action");
648
+ /*
649
+ * Ephemeron behavior is critical here: actions must keep params alive.
650
+ * Without this, bindParams(params) could create a new action while code
651
+ * still references the old action with GC'd params. This would cause:
652
+ * - Duplicate actions in activationWeakSet (old + new)
653
+ * - Cache misses when looking up existing actions
654
+ * - Subtle bugs where different parts of code use different action instances
655
+ * The ephemeron pattern ensures params and actions have synchronized lifetimes.
656
+ */
657
+ const childActionWeakMap = createJsValueWeakMap();
658
+ const _bindParams = (newParamsOrSignal, options = {}) => {
659
+ // ✅ CAS 1: Signal direct -> proxy
660
+ if (isSignal(newParamsOrSignal)) {
661
+ const combinedParamsSignal = computed(() => {
662
+ const newParams = newParamsOrSignal.value;
663
+ return mergeTwoJsValues(params, newParams);
664
+ });
665
+ return createActionProxyFromSignal(
666
+ action,
667
+ combinedParamsSignal,
668
+ options,
669
+ );
670
+ }
671
+
672
+ // ✅ CAS 2: Objet -> vérifier s'il contient des signals
673
+ if (newParamsOrSignal && typeof newParamsOrSignal === "object") {
674
+ const staticParams = {};
675
+ const signalMap = new Map();
676
+
677
+ const keyArray = Object.keys(newParamsOrSignal);
678
+ for (const key of keyArray) {
679
+ const value = newParamsOrSignal[key];
680
+ if (isSignal(value)) {
681
+ signalMap.set(key, value);
682
+ } else {
683
+ const objectSignal = value ? value[SYMBOL_OBJECT_SIGNAL] : null;
684
+ if (objectSignal) {
685
+ signalMap.set(key, objectSignal);
686
+ } else {
687
+ staticParams[key] = value;
688
+ }
689
+ }
690
+ }
691
+
692
+ if (signalMap.size === 0) {
693
+ // Pas de signals, merge statique normal
694
+ if (params === null || typeof params !== "object") {
695
+ return createChildAction(newParamsOrSignal, options);
696
+ }
697
+ const combinedParams = mergeTwoJsValues(params, newParamsOrSignal);
698
+ return createChildAction({
699
+ params: combinedParams,
700
+ ...options,
701
+ });
702
+ }
703
+
704
+ // Combiner avec les params existants pour les valeurs statiques
705
+ const paramsSignal = computed(() => {
706
+ const params = {};
707
+ for (const key of keyArray) {
708
+ const signalForThisKey = signalMap.get(key);
709
+ if (signalForThisKey) {
710
+ params[key] = signalForThisKey.value;
711
+ } else {
712
+ params[key] = staticParams[key];
713
+ }
714
+ }
715
+ return params;
716
+ });
717
+ return createActionProxyFromSignal(action, paramsSignal, options);
718
+ }
719
+
720
+ // ✅ CAS 3: Primitive -> action enfant
721
+ return createChildAction({
722
+ params: newParamsOrSignal,
723
+ ...options,
724
+ });
725
+ };
726
+ const bindParams = (newParamsOrSignal, options = {}) => {
727
+ const existingChildAction = childActionWeakMap.get(newParamsOrSignal);
728
+ if (existingChildAction) {
729
+ return existingChildAction;
730
+ }
731
+ const childAction = _bindParams(newParamsOrSignal, options);
732
+ childActionWeakMap.set(newParamsOrSignal, childAction);
733
+ childActionWeakSet.add(childAction);
734
+
735
+ return childAction;
736
+ };
737
+
738
+ const createChildAction = (childOptions) => {
739
+ const childActionOptions = {
740
+ ...rootOptions,
741
+ ...childOptions,
742
+ meta: {
743
+ ...rootOptions.meta,
744
+ ...childOptions.meta,
745
+ },
746
+ };
747
+ const childAction = createActionCore(childActionOptions, {
748
+ parentAction: action,
749
+ });
750
+ return childAction;
751
+ };
752
+
753
+ // ✅ Implement matchAllSelfOrDescendant
754
+ const matchAllSelfOrDescendant = (predicate, { includeProxies } = {}) => {
755
+ const matches = [];
756
+
757
+ const traverse = (currentAction) => {
758
+ if (currentAction.isProxy && !includeProxies) {
759
+ // proxy action should be ignored because the underlying action will be found anyway
760
+ // and if we check the proxy action we'll end up with duplicates
761
+ // (loading the proxy would load the action it proxies)
762
+ // and as they are 2 different objects they would be added to the set
763
+ return;
764
+ }
765
+
766
+ if (predicate(currentAction)) {
767
+ matches.push(currentAction);
768
+ }
769
+
770
+ // Get child actions from the current action
771
+ const currentActionPrivateProps =
772
+ getActionPrivateProperties(currentAction);
773
+ const childActionWeakSet = currentActionPrivateProps.childActionWeakSet;
774
+ for (const childAction of childActionWeakSet) {
775
+ traverse(childAction);
776
+ }
777
+ };
778
+
779
+ traverse(action);
780
+ return matches;
781
+ };
782
+
783
+ name = generateActionName(name, params);
784
+ if (ACTION_AS_FUNCTION) {
785
+ // Create the action as a function that can be called directly
786
+ action = function actionFunction(params) {
787
+ const boundAction = bindParams(params);
788
+ return boundAction.rerun();
789
+ };
790
+ Object.defineProperty(action, "name", {
791
+ configurable: true,
792
+ writable: true,
793
+ value: name,
794
+ });
795
+ } else {
796
+ action = { name };
797
+ }
798
+
799
+ // Assign all the action properties and methods to the function
800
+ Object.assign(action, {
801
+ isAction: true,
802
+ callback,
803
+ rootAction,
804
+ parentAction,
805
+ params,
806
+ isPrerun,
807
+ runningState,
808
+ aborted,
809
+ error,
810
+ data,
811
+ computedData,
812
+ completed,
813
+ prerun,
814
+ run,
815
+ rerun,
816
+ stop,
817
+ abort,
818
+ bindParams,
819
+ matchAllSelfOrDescendant, // ✅ Add the new method
820
+ replaceParams: (newParams) => {
821
+ const currentParams = paramsSignal.value;
822
+ const nextParams = mergeTwoJsValues(currentParams, newParams);
823
+ if (nextParams === currentParams) {
824
+ return false;
825
+ }
826
+
827
+ // Update the weak map BEFORE updating the signal
828
+ // so that any code triggered by the signal update finds this action
829
+ if (parentAction) {
830
+ const parentActionPrivateProps =
831
+ getActionPrivateProperties(parentAction);
832
+ const parentChildActionWeakMap =
833
+ parentActionPrivateProps.childActionWeakMap;
834
+ parentChildActionWeakMap.delete(currentParams);
835
+ parentChildActionWeakMap.set(nextParams, action);
836
+ }
837
+
838
+ params = nextParams;
839
+ action.params = nextParams;
840
+ action.name = generateActionName(name, nextParams);
841
+ paramsSignal.value = nextParams;
842
+ return true;
843
+ },
844
+ toString: () => action.name,
845
+ meta,
846
+ });
847
+ Object.preventExtensions(action);
848
+
849
+ // Effects pour synchroniser les propriétés
850
+ effects: {
851
+ weakEffect([action], (actionRef) => {
852
+ isPrerun = isPrerunSignal.value;
853
+ actionRef.isPrerun = isPrerun;
854
+ });
855
+ weakEffect([action], (actionRef) => {
856
+ runningState = runningStateSignal.value;
857
+ actionRef.runningState = runningState;
858
+ aborted = runningState === ABORTED;
859
+ actionRef.aborted = aborted;
860
+ completed = runningState === COMPLETED;
861
+ actionRef.completed = completed;
862
+ });
863
+ weakEffect([action], (actionRef) => {
864
+ error = errorSignal.value;
865
+ actionRef.error = error;
866
+ });
867
+ weakEffect([action], (actionRef) => {
868
+ data = dataSignal.value;
869
+ computedData = computedDataSignal.value;
870
+ actionRef.data = data;
871
+ actionRef.computedData = computedData;
872
+ });
873
+ }
874
+
875
+ // Propriétés privées
876
+ private_properties: {
877
+ const ui = {
878
+ renderLoaded: null,
879
+ renderLoadedAsync,
880
+ hasRenderers: false, // Flag to track if action is bound to UI components
881
+ };
882
+ let sideEffectCleanup;
883
+
884
+ const performRun = (runParams) => {
885
+ const {
886
+ globalAbortSignal,
887
+ abortSignal,
888
+ reason,
889
+ isPrerun,
890
+ onComplete,
891
+ onAbort,
892
+ onError,
893
+ } = runParams;
894
+
895
+ if (isPrerun) {
896
+ prerunProtectionRegistry.protect(action);
897
+ }
898
+
899
+ const internalAbortController = new AbortController();
900
+ const internalAbortSignal = internalAbortController.signal;
901
+ const abort = (abortReason) => {
902
+ runningStateSignal.value = ABORTED;
903
+ internalAbortController.abort(abortReason);
904
+ actionAbortMap.delete(action);
905
+ if (isPrerun && (globalAbortSignal.aborted || abortSignal.aborted)) {
906
+ prerunProtectionRegistry.unprotect(action);
907
+ }
908
+ if (DEBUG) {
909
+ console.log(`"${action}": aborted (reason: ${abortReason})`);
910
+ }
911
+ };
912
+
913
+ const onAbortFromSpecific = () => {
914
+ abort(abortSignal.reason);
915
+ };
916
+ const onAbortFromGlobal = () => {
917
+ abort(globalAbortSignal.reason);
918
+ };
919
+
920
+ if (abortSignal) {
921
+ abortSignal.addEventListener("abort", onAbortFromSpecific);
922
+ }
923
+ if (globalAbortSignal) {
924
+ globalAbortSignal.addEventListener("abort", onAbortFromGlobal);
925
+ }
926
+
927
+ actionAbortMap.set(action, abort);
928
+
929
+ batch(() => {
930
+ errorSignal.value = null;
931
+ runningStateSignal.value = RUNNING;
932
+ if (!isPrerun) {
933
+ isPrerunSignal.value = false;
934
+ }
935
+ });
936
+
937
+ const args = [];
938
+ args.push(params);
939
+ args.push({ signal: internalAbortSignal, reason, isPrerun });
940
+ const returnValue = sideEffect(...args);
941
+ if (typeof returnValue === "function") {
942
+ sideEffectCleanup = returnValue;
943
+ }
944
+
945
+ let runResult;
946
+ let rejected = false;
947
+ let rejectedValue;
948
+ const onRunEnd = () => {
949
+ if (abortSignal) {
950
+ abortSignal.removeEventListener("abort", onAbortFromSpecific);
951
+ }
952
+ if (globalAbortSignal) {
953
+ globalAbortSignal.removeEventListener("abort", onAbortFromGlobal);
954
+ }
955
+ prerunProtectionRegistry.unprotect(action);
956
+ actionAbortMap.delete(action);
957
+ actionPromiseMap.delete(action);
958
+ /*
959
+ * Critical: dataEffect, onComplete and completeSideEffect must be batched together to prevent
960
+ * UI inconsistencies. The dataEffect might modify shared state (e.g.,
961
+ * deleting items from a store), and onLoad callbacks might trigger
962
+ * dependent action state changes.
963
+ *
964
+ * Without batching, the UI could render with partially updated state:
965
+ * - dataEffect deletes a resource from the store
966
+ * - UI renders immediately and tries to display the deleted resource
967
+ * - onLoad hasn't yet updated dependent actions to loading state
968
+ *
969
+ * Example: When deleting a resource, we need to both update the store
970
+ * AND put the action that loaded that resource back into loading state
971
+ * before the UI attempts to render the now-missing resource.
972
+ */
973
+ batch(() => {
974
+ dataSignal.value = dataEffect
975
+ ? dataEffect(runResult, action)
976
+ : runResult;
977
+ runningStateSignal.value = COMPLETED;
978
+ onComplete?.(computedDataSignal.peek(), action);
979
+ completeSideEffect?.(action);
980
+ });
981
+ if (DEBUG) {
982
+ console.log(`"${action}": completed (reason: ${reason})`);
983
+ }
984
+ return computedDataSignal.peek();
985
+ };
986
+ const onRunError = (e) => {
987
+ if (abortSignal) {
988
+ abortSignal.removeEventListener("abort", onAbortFromSpecific);
989
+ }
990
+ if (globalAbortSignal) {
991
+ globalAbortSignal.removeEventListener("abort", onAbortFromGlobal);
992
+ }
993
+ actionAbortMap.delete(action);
994
+ actionPromiseMap.delete(action);
995
+ if (internalAbortSignal.aborted && e === internalAbortSignal.reason) {
996
+ runningStateSignal.value = ABORTED;
997
+ if (isPrerun && abortSignal.aborted) {
998
+ prerunProtectionRegistry.unprotect(action);
999
+ }
1000
+ onAbort(e, action);
1001
+ return e;
1002
+ }
1003
+ if (e.name === "AbortError") {
1004
+ throw new Error(
1005
+ "never supposed to happen, abort error should be handled by the abort signal",
1006
+ );
1007
+ }
1008
+ if (DEBUG) {
1009
+ console.log(
1010
+ `"${action}": failed (error: ${e}, handled by ui: ${ui.hasRenderers})`,
1011
+ );
1012
+ }
1013
+ batch(() => {
1014
+ errorSignal.value = e;
1015
+ runningStateSignal.value = FAILED;
1016
+ onError?.(e, action);
1017
+ });
1018
+
1019
+ if (ui.hasRenderers || onError) {
1020
+ console.error(e);
1021
+ // For UI-bound actions: error is properly handled by logging + UI display
1022
+ // Return error instead of throwing to signal it's handled and prevent:
1023
+ // - jsenv error overlay from appearing
1024
+ // - error being treated as unhandled by runtime
1025
+ return e;
1026
+ }
1027
+ throw e;
1028
+ };
1029
+
1030
+ try {
1031
+ const thenableArray = [];
1032
+ const callbackResult = callback(...args);
1033
+ if (callbackResult && typeof callbackResult.then === "function") {
1034
+ thenableArray.push(
1035
+ callbackResult.then(
1036
+ (value) => {
1037
+ runResult = value;
1038
+ },
1039
+ (e) => {
1040
+ rejected = true;
1041
+ rejectedValue = e;
1042
+ },
1043
+ ),
1044
+ );
1045
+ } else {
1046
+ runResult = callbackResult;
1047
+ }
1048
+ if (ui.renderLoadedAsync && !ui.renderLoaded) {
1049
+ const renderLoadedPromise = ui.renderLoadedAsync(...args).then(
1050
+ (renderLoaded) => {
1051
+ ui.renderLoaded = renderLoaded;
1052
+ },
1053
+ (e) => {
1054
+ if (!rejected) {
1055
+ rejected = true;
1056
+ rejectedValue = e;
1057
+ }
1058
+ },
1059
+ );
1060
+ thenableArray.push(renderLoadedPromise);
1061
+ }
1062
+ if (thenableArray.length === 0) {
1063
+ return onRunEnd();
1064
+ }
1065
+ return Promise.all(thenableArray).then(() => {
1066
+ if (rejected) {
1067
+ return onRunError(rejectedValue);
1068
+ }
1069
+ return onRunEnd();
1070
+ });
1071
+ } catch (e) {
1072
+ return onRunError(e);
1073
+ }
1074
+ };
1075
+
1076
+ const performStop = ({ reason }) => {
1077
+ abort(reason);
1078
+ if (DEBUG) {
1079
+ console.log(`"${action}": stopping (reason: ${reason})`);
1080
+ }
1081
+
1082
+ prerunProtectionRegistry.unprotect(action);
1083
+
1084
+ if (sideEffectCleanup) {
1085
+ sideEffectCleanup(reason);
1086
+ sideEffectCleanup = undefined;
1087
+ }
1088
+
1089
+ actionPromiseMap.delete(action);
1090
+ batch(() => {
1091
+ errorSignal.value = null;
1092
+ if (!keepOldData) {
1093
+ dataSignal.value = initialData;
1094
+ }
1095
+ isPrerunSignal.value = true;
1096
+ runningStateSignal.value = IDLE;
1097
+ });
1098
+ };
1099
+
1100
+ const privateProperties = {
1101
+ initialData,
1102
+
1103
+ paramsSignal,
1104
+ runningStateSignal,
1105
+ isPrerunSignal,
1106
+ dataSignal,
1107
+ computedDataSignal,
1108
+ errorSignal,
1109
+
1110
+ performRun,
1111
+ performStop,
1112
+ ui,
1113
+
1114
+ childActionWeakSet,
1115
+ childActionWeakMap,
1116
+ };
1117
+ setActionPrivateProperties(action, privateProperties);
1118
+ }
1119
+
1120
+ return action;
1121
+ };
1122
+
1123
+ rootAction = createActionCore(rootOptions);
1124
+ actionWeakMap.set(callback, rootAction);
1125
+ return rootAction;
1126
+ };
1127
+
1128
+ const createActionProxyFromSignal = (
1129
+ action,
1130
+ paramsSignal,
1131
+ { rerunOnChange = false, onChange } = {},
1132
+ ) => {
1133
+ const actionTargetChangeCallbackSet = new Set();
1134
+ const onActionTargetChange = (callback) => {
1135
+ actionTargetChangeCallbackSet.add(callback);
1136
+ return () => {
1137
+ actionTargetChangeCallbackSet.delete(callback);
1138
+ };
1139
+ };
1140
+ const changeCleanupCallbackSet = new Set();
1141
+ const triggerTargetChange = (actionTarget, previousTarget) => {
1142
+ for (const changeCleanupCallback of changeCleanupCallbackSet) {
1143
+ changeCleanupCallback();
1144
+ }
1145
+ changeCleanupCallbackSet.clear();
1146
+ for (const callback of actionTargetChangeCallbackSet) {
1147
+ const returnValue = callback(actionTarget, previousTarget);
1148
+ if (typeof returnValue === "function") {
1149
+ changeCleanupCallbackSet.add(returnValue);
1150
+ }
1151
+ }
1152
+ };
1153
+
1154
+ let actionTarget = null;
1155
+ let currentAction = action;
1156
+ let currentActionPrivateProperties = getActionPrivateProperties(action);
1157
+ let actionTargetPreviousWeakRef = null;
1158
+ let isFirstEffect = true;
1159
+
1160
+ const _updateTarget = (params) => {
1161
+ const previousActionTarget = actionTargetPreviousWeakRef?.deref();
1162
+
1163
+ if (params === NO_PARAMS) {
1164
+ actionTarget = null;
1165
+ currentAction = action;
1166
+ currentActionPrivateProperties = getActionPrivateProperties(action);
1167
+ } else {
1168
+ actionTarget = action.bindParams(params);
1169
+ if (previousActionTarget === actionTarget) {
1170
+ return;
1171
+ }
1172
+ currentAction = actionTarget;
1173
+ currentActionPrivateProperties = getActionPrivateProperties(actionTarget);
1174
+ }
1175
+
1176
+ if (isFirstEffect) {
1177
+ isFirstEffect = false;
1178
+ }
1179
+ actionTargetPreviousWeakRef = actionTarget
1180
+ ? new WeakRef(actionTarget)
1181
+ : null;
1182
+ triggerTargetChange(actionTarget, previousActionTarget);
1183
+ };
1184
+
1185
+ const proxyMethod = (method) => {
1186
+ return (...args) => {
1187
+ /*
1188
+ * Ensure the proxy targets the correct action before method execution.
1189
+ * This prevents race conditions where external effects run before our
1190
+ * internal parameter synchronization effect. Using peek() avoids creating
1191
+ * reactive dependencies within this pass-through method.
1192
+ */
1193
+ _updateTarget(proxyParamsSignal.peek());
1194
+ return currentAction[method](...args);
1195
+ };
1196
+ };
1197
+
1198
+ const nameSignal = signal();
1199
+ const actionProxy = {
1200
+ isProxy: true,
1201
+ callback: undefined,
1202
+ get name() {
1203
+ return nameSignal.value;
1204
+ },
1205
+ params: undefined,
1206
+ isPrerun: undefined,
1207
+ runningState: undefined,
1208
+ aborted: undefined,
1209
+ error: undefined,
1210
+ data: undefined,
1211
+ computedData: undefined,
1212
+ completed: undefined,
1213
+ prerun: proxyMethod("prerun"),
1214
+ run: proxyMethod("run"),
1215
+ rerun: proxyMethod("rerun"),
1216
+ stop: proxyMethod("stop"),
1217
+ abort: proxyMethod("abort"),
1218
+ matchAllSelfOrDescendant: proxyMethod("matchAllSelfOrDescendant"),
1219
+ getCurrentAction: () => {
1220
+ _updateTarget(proxyParamsSignal.peek());
1221
+ return currentAction;
1222
+ },
1223
+ bindParams: () => {
1224
+ throw new Error(
1225
+ `bindParams() is not supported on action proxies, use the underlying action instead`,
1226
+ );
1227
+ },
1228
+ replaceParams: null, // Will be set below
1229
+ toString: () => actionProxy.name,
1230
+ meta: {},
1231
+ };
1232
+ Object.preventExtensions(actionProxy);
1233
+
1234
+ onActionTargetChange((actionTarget) => {
1235
+ const currentAction = actionTarget || action;
1236
+ nameSignal.value = `[Proxy] ${currentAction.name}`;
1237
+ actionProxy.callback = currentAction.callback;
1238
+ actionProxy.params = currentAction.params;
1239
+ actionProxy.isPrerun = currentAction.isPrerun;
1240
+ actionProxy.runningState = currentAction.runningState;
1241
+ actionProxy.aborted = currentAction.aborted;
1242
+ actionProxy.error = currentAction.error;
1243
+ actionProxy.data = currentAction.data;
1244
+ actionProxy.computedData = currentAction.computedData;
1245
+ actionProxy.completed = currentAction.completed;
1246
+ });
1247
+
1248
+ const proxyPrivateSignal = (signalPropertyName, propertyName) => {
1249
+ const signalProxy = signal();
1250
+ let dispose;
1251
+ onActionTargetChange(() => {
1252
+ if (dispose) {
1253
+ dispose();
1254
+ dispose = undefined;
1255
+ }
1256
+ dispose = effect(() => {
1257
+ const currentActionSignal =
1258
+ currentActionPrivateProperties[signalPropertyName];
1259
+ const currentActionSignalValue = currentActionSignal.value;
1260
+ signalProxy.value = currentActionSignalValue;
1261
+ if (propertyName) {
1262
+ actionProxy[propertyName] = currentActionSignalValue;
1263
+ }
1264
+ });
1265
+ return dispose;
1266
+ });
1267
+ return signalProxy;
1268
+ };
1269
+ const proxyPrivateMethod = (method) => {
1270
+ return (...args) => currentActionPrivateProperties[method](...args);
1271
+ };
1272
+
1273
+ // Create our own signal for params that we control completely
1274
+ const proxyParamsSignal = signal(paramsSignal.value);
1275
+
1276
+ // Watch for changes in the original paramsSignal and update ours
1277
+ // (original signal wins over any replaceParams calls)
1278
+ weakEffect(
1279
+ [paramsSignal, proxyParamsSignal],
1280
+ (paramsSignalRef, proxyParamsSignalRef) => {
1281
+ proxyParamsSignalRef.value = paramsSignalRef.value;
1282
+ },
1283
+ );
1284
+
1285
+ const proxyPrivateProperties = {
1286
+ get currentAction() {
1287
+ return currentAction;
1288
+ },
1289
+ paramsSignal: proxyParamsSignal,
1290
+ isPrerunSignal: proxyPrivateSignal("isPrerunSignal", "isPrerun"),
1291
+ runningStateSignal: proxyPrivateSignal(
1292
+ "runningStateSignal",
1293
+ "runningState",
1294
+ ),
1295
+ errorSignal: proxyPrivateSignal("errorSignal", "error"),
1296
+ dataSignal: proxyPrivateSignal("dataSignal", "data"),
1297
+ computedDataSignal: proxyPrivateSignal("computedDataSignal"),
1298
+ performRun: proxyPrivateMethod("performRun"),
1299
+ performStop: proxyPrivateMethod("performStop"),
1300
+ ui: currentActionPrivateProperties.ui,
1301
+ };
1302
+
1303
+ onActionTargetChange((actionTarget, previousTarget) => {
1304
+ proxyPrivateProperties.ui = currentActionPrivateProperties.ui;
1305
+ if (previousTarget && actionTarget) {
1306
+ const previousPrivateProps = getActionPrivateProperties(previousTarget);
1307
+ if (previousPrivateProps.ui.hasRenderers) {
1308
+ const newPrivateProps = getActionPrivateProperties(actionTarget);
1309
+ newPrivateProps.ui.hasRenderers = true;
1310
+ }
1311
+ }
1312
+ proxyPrivateProperties.childActionWeakSet =
1313
+ currentActionPrivateProperties.childActionWeakSet;
1314
+ });
1315
+ setActionPrivateProperties(actionProxy, proxyPrivateProperties);
1316
+
1317
+ {
1318
+ weakEffect([action], () => {
1319
+ const params = proxyParamsSignal.value;
1320
+ _updateTarget(params);
1321
+ });
1322
+ }
1323
+
1324
+ actionProxy.replaceParams = (newParams) => {
1325
+ if (currentAction === action) {
1326
+ const currentParams = proxyParamsSignal.value;
1327
+ const nextParams = mergeTwoJsValues(currentParams, newParams);
1328
+ if (nextParams === currentParams) {
1329
+ return false;
1330
+ }
1331
+ proxyParamsSignal.value = nextParams;
1332
+ return true;
1333
+ }
1334
+ if (!currentAction.replaceParams(newParams)) {
1335
+ return false;
1336
+ }
1337
+ proxyParamsSignal.value =
1338
+ currentActionPrivateProperties.paramsSignal.peek();
1339
+ return true;
1340
+ };
1341
+
1342
+ if (rerunOnChange) {
1343
+ onActionTargetChange((actionTarget, actionTargetPrevious) => {
1344
+ if (
1345
+ actionTarget &&
1346
+ actionTargetPrevious &&
1347
+ !actionTargetPrevious.isPrerun
1348
+ ) {
1349
+ actionTarget.rerun();
1350
+ }
1351
+ });
1352
+ }
1353
+ if (onChange) {
1354
+ onActionTargetChange((actionTarget, actionTargetPrevious) => {
1355
+ onChange(actionTarget, actionTargetPrevious);
1356
+ });
1357
+ }
1358
+
1359
+ return actionProxy;
1360
+ };
1361
+
1362
+ const generateActionName = (name, params) => {
1363
+ if (params === NO_PARAMS) {
1364
+ return `${name}({})`;
1365
+ }
1366
+ // Use stringifyForDisplay with asFunctionArgs option for the entire args array
1367
+ const argsString = stringifyForDisplay([params], 3, 0, {
1368
+ asFunctionArgs: true,
1369
+ });
1370
+ return `${name}${argsString}`;
1371
+ };
1372
+
1373
+ if (import.meta.hot) {
1374
+ import.meta.hot.dispose(() => {
1375
+ abortRunningActions();
1376
+ });
1377
+ }