webspresso 0.0.110 → 0.0.111
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/client/adapter/datastar.js +73 -23
- package/core/client/api.js +23 -21
- package/core/client/controller.js +288 -30
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +39 -19
- package/core/client/fragments.js +433 -95
- package/core/client/host.js +365 -0
- package/core/client/index.d.ts +18 -0
- package/core/client/index.js +154 -23
- package/core/client/lite.js +101 -4
- package/core/client/server-fragments.js +99 -21
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +322 -28
- package/index.js +3 -0
- package/package.json +1 -1
- package/src/bundle/index.js +175 -14
- package/src/dev/banner.js +4 -1
- package/src/dev-reload/index.js +29 -1
- package/src/server.js +28 -0
|
@@ -18,7 +18,15 @@
|
|
|
18
18
|
const { createFormHandle } = require('./forms');
|
|
19
19
|
const { createApiClient } = require('./api');
|
|
20
20
|
const { defaultStorage, createClientStorage } = require('./storage');
|
|
21
|
-
const {
|
|
21
|
+
const { createClientServiceRegistry } = require('./services');
|
|
22
|
+
const {
|
|
23
|
+
isArray,
|
|
24
|
+
isNumber,
|
|
25
|
+
decorateElement,
|
|
26
|
+
parseDurationMs,
|
|
27
|
+
parseRefetchMode,
|
|
28
|
+
setElementBusyState,
|
|
29
|
+
} = require('./utils');
|
|
22
30
|
|
|
23
31
|
let instanceSeq = 0;
|
|
24
32
|
let activeControllerContext = null;
|
|
@@ -43,7 +51,7 @@ function deepTouch(val, visited = new Set()) {
|
|
|
43
51
|
return;
|
|
44
52
|
}
|
|
45
53
|
visited.add(val);
|
|
46
|
-
if (
|
|
54
|
+
if (isArray(val)) {
|
|
47
55
|
for (let i = 0; i < val.length; i++) {
|
|
48
56
|
deepTouch(val[i], visited);
|
|
49
57
|
}
|
|
@@ -60,14 +68,14 @@ function deepCloneOrSnapshot(val) {
|
|
|
60
68
|
try {
|
|
61
69
|
return JSON.parse(JSON.stringify(val));
|
|
62
70
|
} catch {
|
|
63
|
-
return
|
|
71
|
+
return isArray(val) ? [...val] : { ...val };
|
|
64
72
|
}
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
function deepEqual(a, b) {
|
|
68
76
|
if (Object.is(a, b)) return true;
|
|
69
77
|
if (a === null || typeof a !== 'object' || b === null || typeof b !== 'object') return false;
|
|
70
|
-
if (
|
|
78
|
+
if (isArray(a) !== isArray(b)) return false;
|
|
71
79
|
const keysA = Object.keys(a);
|
|
72
80
|
const keysB = Object.keys(b);
|
|
73
81
|
if (keysA.length !== keysB.length) return false;
|
|
@@ -81,7 +89,7 @@ function deepEqual(a, b) {
|
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
function hasArrayChanged(newArr, oldArr) {
|
|
84
|
-
if (!
|
|
92
|
+
if (!isArray(newArr) || !isArray(oldArr)) return !Object.is(newArr, oldArr);
|
|
85
93
|
if (newArr.length !== oldArr.length) return true;
|
|
86
94
|
for (let i = 0; i < newArr.length; i++) {
|
|
87
95
|
if (!Object.is(newArr[i], oldArr[i])) return true;
|
|
@@ -108,7 +116,7 @@ function createStandaloneWatch(engine, source, callback, options = {}) {
|
|
|
108
116
|
let getter;
|
|
109
117
|
if (typeof source === 'function') {
|
|
110
118
|
getter = source;
|
|
111
|
-
} else if (
|
|
119
|
+
} else if (isArray(source)) {
|
|
112
120
|
getter = () => source.map((s) => (typeof s === 'function' ? s() : s));
|
|
113
121
|
} else {
|
|
114
122
|
getter = () => source;
|
|
@@ -147,14 +155,14 @@ function createStandaloneWatch(engine, source, callback, options = {}) {
|
|
|
147
155
|
let newValue;
|
|
148
156
|
try {
|
|
149
157
|
newValue = getter();
|
|
150
|
-
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !
|
|
158
|
+
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !isArray(source))) && newValue && typeof newValue === 'object') {
|
|
151
159
|
deepTouch(newValue);
|
|
152
160
|
}
|
|
153
161
|
} catch (_err) {
|
|
154
162
|
return;
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
const isArr =
|
|
165
|
+
const isArr = isArray(source);
|
|
158
166
|
|
|
159
167
|
if (isFirstRun) {
|
|
160
168
|
isFirstRun = false;
|
|
@@ -325,7 +333,7 @@ function defineController(definition = {}) {
|
|
|
325
333
|
}
|
|
326
334
|
if (definition.persist !== undefined && definition.persist !== null) {
|
|
327
335
|
const p = definition.persist;
|
|
328
|
-
const isArr =
|
|
336
|
+
const isArr = isArray(p);
|
|
329
337
|
const isStr = typeof p === 'string';
|
|
330
338
|
const isObj = typeof p === 'object' && p !== null && !isArr;
|
|
331
339
|
if (!isArr && !isStr && !isObj) {
|
|
@@ -381,13 +389,13 @@ function normalizePersistConfig(persistOpt, controllerName) {
|
|
|
381
389
|
let isGlobal = false;
|
|
382
390
|
let debounce = 50;
|
|
383
391
|
|
|
384
|
-
if (
|
|
392
|
+
if (isArray(persistOpt)) {
|
|
385
393
|
keys = persistOpt.map(String);
|
|
386
394
|
} else if (typeof persistOpt === 'string') {
|
|
387
395
|
const s = persistOpt.trim().toLowerCase();
|
|
388
396
|
storageType = s === 'session' ? 'session' : 'local';
|
|
389
397
|
} else if (typeof persistOpt === 'object' && persistOpt !== null) {
|
|
390
|
-
if (
|
|
398
|
+
if (isArray(persistOpt.keys)) {
|
|
391
399
|
keys = persistOpt.keys.map(String);
|
|
392
400
|
}
|
|
393
401
|
if (persistOpt.storage === 'session' || persistOpt.storage === 'memory') {
|
|
@@ -399,7 +407,7 @@ function normalizePersistConfig(persistOpt, controllerName) {
|
|
|
399
407
|
if (Boolean(persistOpt.global)) {
|
|
400
408
|
isGlobal = true;
|
|
401
409
|
}
|
|
402
|
-
if (
|
|
410
|
+
if (isNumber(persistOpt.debounce)) {
|
|
403
411
|
debounce = Math.max(0, persistOpt.debounce);
|
|
404
412
|
}
|
|
405
413
|
}
|
|
@@ -470,6 +478,14 @@ function createControllerRuntime(options = {}) {
|
|
|
470
478
|
const debug = options.debug || null;
|
|
471
479
|
const baseApiConfig = options.apiConfig || {};
|
|
472
480
|
const lazyLoader = typeof options.lazyLoader === 'function' ? options.lazyLoader : null;
|
|
481
|
+
const services =
|
|
482
|
+
options.services && typeof options.services.get === 'function'
|
|
483
|
+
? options.services
|
|
484
|
+
: createClientServiceRegistry({
|
|
485
|
+
root: options.root || null,
|
|
486
|
+
debug,
|
|
487
|
+
services: options.services || undefined,
|
|
488
|
+
});
|
|
473
489
|
|
|
474
490
|
// Registered controller definitions: name -> { name, definition, sourcePath }
|
|
475
491
|
const definitions = new Map();
|
|
@@ -534,7 +550,7 @@ function createControllerRuntime(options = {}) {
|
|
|
534
550
|
};
|
|
535
551
|
definitions.set(canonicalName, entry);
|
|
536
552
|
|
|
537
|
-
if (
|
|
553
|
+
if (isArray(meta.aliases)) {
|
|
538
554
|
for (const alias of meta.aliases) {
|
|
539
555
|
if (alias && !definitions.has(alias)) {
|
|
540
556
|
definitions.set(alias, entry);
|
|
@@ -587,6 +603,79 @@ function createControllerRuntime(options = {}) {
|
|
|
587
603
|
const scopedForms = new Map();
|
|
588
604
|
const scopedTasks = {};
|
|
589
605
|
const scopedTasksList = [];
|
|
606
|
+
const busyActionCounts = new Map();
|
|
607
|
+
let totalBusyActions = 0;
|
|
608
|
+
const busySignalPrefix = `__w_inst_${instanceId}.__busy`;
|
|
609
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.any`, false);
|
|
610
|
+
|
|
611
|
+
function syncAnyBusySignal() {
|
|
612
|
+
let busy = totalBusyActions > 0;
|
|
613
|
+
if (!busy) {
|
|
614
|
+
for (let i = 0; i < scopedTasksList.length; i++) {
|
|
615
|
+
if (scopedTasksList[i].pending) {
|
|
616
|
+
busy = true;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.any`, busy);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function setActionBusy(actionName, delta) {
|
|
625
|
+
const prevCount = busyActionCounts.get(actionName) || 0;
|
|
626
|
+
const nextCount = Math.max(0, prevCount + delta);
|
|
627
|
+
if (nextCount === 0) {
|
|
628
|
+
busyActionCounts.delete(actionName);
|
|
629
|
+
} else {
|
|
630
|
+
busyActionCounts.set(actionName, nextCount);
|
|
631
|
+
}
|
|
632
|
+
totalBusyActions = Math.max(0, totalBusyActions + delta);
|
|
633
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.action_${actionName}`, nextCount > 0);
|
|
634
|
+
syncAnyBusySignal();
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function isActionBusy(actionName) {
|
|
638
|
+
if (!actionName) return false;
|
|
639
|
+
datastar.engine.signals.value(`${busySignalPrefix}.action_${actionName}`);
|
|
640
|
+
return (busyActionCounts.get(actionName) || 0) > 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function checkIsBusy() {
|
|
644
|
+
datastar.engine.signals.value(`${busySignalPrefix}.any`);
|
|
645
|
+
if (totalBusyActions > 0) return true;
|
|
646
|
+
for (let i = 0; i < scopedTasksList.length; i++) {
|
|
647
|
+
if (scopedTasksList[i].pending) return true;
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function isLoadingTargetActive(rawSpec) {
|
|
653
|
+
const spec = typeof rawSpec === 'string' ? rawSpec.trim() : '';
|
|
654
|
+
if (!spec || spec === 'true') {
|
|
655
|
+
return checkIsBusy();
|
|
656
|
+
}
|
|
657
|
+
const parts = spec.split(',').map((s) => s.trim()).filter(Boolean);
|
|
658
|
+
for (let i = 0; i < parts.length; i++) {
|
|
659
|
+
const token = parts[i];
|
|
660
|
+
if (reg.definition.actions && Object.prototype.hasOwnProperty.call(reg.definition.actions, token)) {
|
|
661
|
+
if (isActionBusy(token)) return true;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (
|
|
665
|
+
Object.prototype.hasOwnProperty.call(scopedTasks, token) ||
|
|
666
|
+
(reg.definition.tasks && Object.prototype.hasOwnProperty.call(reg.definition.tasks, token))
|
|
667
|
+
) {
|
|
668
|
+
const t = getTask(token);
|
|
669
|
+
if (t && t.pending) return true;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (Boolean(datastar.evaluateStateExpression(state, token))) {
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
590
679
|
let currentActionEl = null;
|
|
591
680
|
let lastError = null;
|
|
592
681
|
const dynamicErrorListeners = new Set();
|
|
@@ -647,10 +736,29 @@ function createControllerRuntime(options = {}) {
|
|
|
647
736
|
const taskKey = taskOpts.key || `task_${++taskSeq}`;
|
|
648
737
|
const signalPrefix = `__w_inst_${instanceId}.task_${taskKey}`;
|
|
649
738
|
|
|
739
|
+
// React Query-style automatic revalidation options for tasks
|
|
740
|
+
const staleTimeMs = parseDurationMs(taskOpts.staleTime, 0);
|
|
741
|
+
const refetchOnMountMode = parseRefetchMode(
|
|
742
|
+
taskOpts.refetchOnMount !== undefined ? taskOpts.refetchOnMount : taskOpts.refetchOnVisit,
|
|
743
|
+
false
|
|
744
|
+
);
|
|
745
|
+
const refetchOnFocusMode = parseRefetchMode(
|
|
746
|
+
taskOpts.refetchOnWindowFocus !== undefined ? taskOpts.refetchOnWindowFocus : taskOpts.refetchOnFocus,
|
|
747
|
+
false
|
|
748
|
+
);
|
|
749
|
+
const refetchOnReconnectMode = parseRefetchMode(taskOpts.refetchOnReconnect, false);
|
|
750
|
+
const refetchIntervalMs = parseDurationMs(taskOpts.refetchInterval, 0);
|
|
751
|
+
const refetchIntervalInBg =
|
|
752
|
+
parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
753
|
+
|
|
650
754
|
let currentRunVersion = 0;
|
|
651
755
|
let activeRuns = 0;
|
|
652
756
|
let inFlightAbort = null;
|
|
653
757
|
let queuedPromise = Promise.resolve();
|
|
758
|
+
let _lastUpdatedAt = 0;
|
|
759
|
+
let taskIntervalTimer = null;
|
|
760
|
+
let focusDebounceTimer = null;
|
|
761
|
+
const taskCleanupListeners = [];
|
|
654
762
|
|
|
655
763
|
datastar.engine.signals.setValue(`${signalPrefix}.pending`, false);
|
|
656
764
|
datastar.engine.signals.setValue(`${signalPrefix}.data`, null);
|
|
@@ -662,8 +770,19 @@ function createControllerRuntime(options = {}) {
|
|
|
662
770
|
let _error = null;
|
|
663
771
|
let _stale = false;
|
|
664
772
|
|
|
773
|
+
function isTaskStale(mode = true) {
|
|
774
|
+
if (!mode) return false;
|
|
775
|
+
if (mode === 'always') return true;
|
|
776
|
+
if (_lastUpdatedAt === 0) return true;
|
|
777
|
+
if (staleTimeMs <= 0) return true;
|
|
778
|
+
return Date.now() - _lastUpdatedAt >= staleTimeMs;
|
|
779
|
+
}
|
|
780
|
+
|
|
665
781
|
function updateSignal(name, val) {
|
|
666
782
|
datastar.engine.signals.setValue(`${signalPrefix}.${name}`, val);
|
|
783
|
+
if (name === 'pending') {
|
|
784
|
+
syncAnyBusySignal();
|
|
785
|
+
}
|
|
667
786
|
}
|
|
668
787
|
|
|
669
788
|
async function run(...args) {
|
|
@@ -722,6 +841,7 @@ function createControllerRuntime(options = {}) {
|
|
|
722
841
|
_data = result;
|
|
723
842
|
_error = null;
|
|
724
843
|
_stale = false;
|
|
844
|
+
_lastUpdatedAt = Date.now();
|
|
725
845
|
updateSignal('data', result);
|
|
726
846
|
updateSignal('error', null);
|
|
727
847
|
updateSignal('stale', false);
|
|
@@ -787,16 +907,103 @@ function createControllerRuntime(options = {}) {
|
|
|
787
907
|
},
|
|
788
908
|
get stale() {
|
|
789
909
|
datastar.engine.signals.value(`${signalPrefix}.stale`);
|
|
790
|
-
return _stale;
|
|
910
|
+
return _stale || (_lastUpdatedAt > 0 && staleTimeMs > 0 && Date.now() - _lastUpdatedAt >= staleTimeMs);
|
|
791
911
|
},
|
|
912
|
+
get lastUpdatedAt() {
|
|
913
|
+
return _lastUpdatedAt;
|
|
914
|
+
},
|
|
915
|
+
isStale: () => isTaskStale(true),
|
|
792
916
|
run,
|
|
793
917
|
cancel,
|
|
794
918
|
dispose() {
|
|
919
|
+
if (taskIntervalTimer) {
|
|
920
|
+
clearInterval(taskIntervalTimer);
|
|
921
|
+
taskIntervalTimer = null;
|
|
922
|
+
}
|
|
923
|
+
if (focusDebounceTimer) {
|
|
924
|
+
clearTimeout(focusDebounceTimer);
|
|
925
|
+
focusDebounceTimer = null;
|
|
926
|
+
}
|
|
927
|
+
while (taskCleanupListeners.length > 0) {
|
|
928
|
+
const removeFn = taskCleanupListeners.pop();
|
|
929
|
+
try {
|
|
930
|
+
removeFn();
|
|
931
|
+
} catch (_e) {}
|
|
932
|
+
}
|
|
795
933
|
cancel();
|
|
796
934
|
datastar.engine.signals.remove(signalPrefix);
|
|
797
935
|
},
|
|
798
936
|
};
|
|
799
937
|
|
|
938
|
+
// Set up automatic background revalidation (mount / focus / reconnect / interval)
|
|
939
|
+
const doc = (rootEl && rootEl.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
940
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
941
|
+
|
|
942
|
+
function isDocHidden() {
|
|
943
|
+
return Boolean(doc && doc.visibilityState === 'hidden');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (refetchOnMountMode) {
|
|
947
|
+
Promise.resolve().then(() => {
|
|
948
|
+
if (isConnected() && isTaskStale(refetchOnMountMode)) {
|
|
949
|
+
run().catch(() => {});
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (refetchOnFocusMode) {
|
|
955
|
+
const onTaskFocus = () => {
|
|
956
|
+
if (!isConnected() || isDocHidden()) return;
|
|
957
|
+
if (focusDebounceTimer) clearTimeout(focusDebounceTimer);
|
|
958
|
+
focusDebounceTimer = setTimeout(() => {
|
|
959
|
+
focusDebounceTimer = null;
|
|
960
|
+
if (isConnected() && !isDocHidden() && activeRuns === 0 && isTaskStale(refetchOnFocusMode)) {
|
|
961
|
+
run().catch(() => {});
|
|
962
|
+
}
|
|
963
|
+
}, 20);
|
|
964
|
+
};
|
|
965
|
+
const onVisChange = () => {
|
|
966
|
+
if (!isDocHidden()) onTaskFocus();
|
|
967
|
+
};
|
|
968
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
969
|
+
doc.addEventListener('visibilitychange', onVisChange);
|
|
970
|
+
taskCleanupListeners.push(() => doc.removeEventListener('visibilitychange', onVisChange));
|
|
971
|
+
}
|
|
972
|
+
if (win && typeof win.addEventListener === 'function') {
|
|
973
|
+
win.addEventListener('focus', onTaskFocus);
|
|
974
|
+
taskCleanupListeners.push(() => win.removeEventListener('focus', onTaskFocus));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (refetchOnReconnectMode && win && typeof win.addEventListener === 'function') {
|
|
979
|
+
const onTaskOnline = () => {
|
|
980
|
+
if (isConnected() && activeRuns === 0 && isTaskStale(refetchOnReconnectMode)) {
|
|
981
|
+
run().catch(() => {});
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
win.addEventListener('online', onTaskOnline);
|
|
985
|
+
taskCleanupListeners.push(() => win.removeEventListener('online', onTaskOnline));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
if (refetchIntervalMs > 0) {
|
|
989
|
+
taskIntervalTimer = setInterval(() => {
|
|
990
|
+
if (!isConnected()) {
|
|
991
|
+
if (taskIntervalTimer) {
|
|
992
|
+
clearInterval(taskIntervalTimer);
|
|
993
|
+
taskIntervalTimer = null;
|
|
994
|
+
}
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
if (!refetchIntervalInBg && isDocHidden()) return;
|
|
998
|
+
if (activeRuns === 0) {
|
|
999
|
+
run().catch(() => {});
|
|
1000
|
+
}
|
|
1001
|
+
}, refetchIntervalMs);
|
|
1002
|
+
if (taskIntervalTimer && typeof taskIntervalTimer.unref === 'function') {
|
|
1003
|
+
taskIntervalTimer.unref();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
800
1007
|
scopedTasksList.push(taskObj);
|
|
801
1008
|
return taskObj;
|
|
802
1009
|
}
|
|
@@ -826,7 +1033,7 @@ function createControllerRuntime(options = {}) {
|
|
|
826
1033
|
getter = () => source(ctx);
|
|
827
1034
|
} else if (typeof source === 'string') {
|
|
828
1035
|
getter = () => resolveStatePath(state, source);
|
|
829
|
-
} else if (
|
|
1036
|
+
} else if (isArray(source)) {
|
|
830
1037
|
getter = () =>
|
|
831
1038
|
source.map((s) =>
|
|
832
1039
|
typeof s === 'function'
|
|
@@ -876,7 +1083,7 @@ function createControllerRuntime(options = {}) {
|
|
|
876
1083
|
let newValue;
|
|
877
1084
|
try {
|
|
878
1085
|
newValue = getter();
|
|
879
|
-
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !
|
|
1086
|
+
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !isArray(source))) && newValue && typeof newValue === 'object') {
|
|
880
1087
|
deepTouch(newValue);
|
|
881
1088
|
}
|
|
882
1089
|
} catch (err) {
|
|
@@ -884,7 +1091,7 @@ function createControllerRuntime(options = {}) {
|
|
|
884
1091
|
return;
|
|
885
1092
|
}
|
|
886
1093
|
|
|
887
|
-
const isArr =
|
|
1094
|
+
const isArr = isArray(source);
|
|
888
1095
|
|
|
889
1096
|
if (isFirstRun) {
|
|
890
1097
|
isFirstRun = false;
|
|
@@ -1069,6 +1276,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1069
1276
|
api: scopedApi,
|
|
1070
1277
|
datastar,
|
|
1071
1278
|
debug,
|
|
1279
|
+
services,
|
|
1072
1280
|
...formOptions,
|
|
1073
1281
|
});
|
|
1074
1282
|
scopedForms.set(handle.name, handle);
|
|
@@ -1168,6 +1376,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1168
1376
|
return state;
|
|
1169
1377
|
},
|
|
1170
1378
|
api: scopedApi,
|
|
1379
|
+
services,
|
|
1171
1380
|
forms: formsAccessor,
|
|
1172
1381
|
target: targetAccessor,
|
|
1173
1382
|
targets: targetsAccessor,
|
|
@@ -1201,6 +1410,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1201
1410
|
signal: lifecycleAbort.signal,
|
|
1202
1411
|
isConnected,
|
|
1203
1412
|
isDisposed: () => !isConnected(),
|
|
1413
|
+
get isBusy() {
|
|
1414
|
+
return checkIsBusy();
|
|
1415
|
+
},
|
|
1416
|
+
isActionBusy,
|
|
1204
1417
|
get lastError() {
|
|
1205
1418
|
return lastError;
|
|
1206
1419
|
},
|
|
@@ -1269,7 +1482,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1269
1482
|
if (persistConfig) {
|
|
1270
1483
|
const driver = resolvePersistDriver(persistConfig);
|
|
1271
1484
|
const savedData = driver.get(persistConfig.storageKey, null);
|
|
1272
|
-
if (savedData && typeof savedData === 'object' && !
|
|
1485
|
+
if (savedData && typeof savedData === 'object' && !isArray(savedData)) {
|
|
1273
1486
|
for (const [k, v] of Object.entries(savedData)) {
|
|
1274
1487
|
if (persistConfig.keys && !persistConfig.keys.includes(k)) continue;
|
|
1275
1488
|
if (Object.prototype.hasOwnProperty.call(initialRawState, k)) {
|
|
@@ -1308,6 +1521,36 @@ function createControllerRuntime(options = {}) {
|
|
|
1308
1521
|
if (typeof fn !== 'function') {
|
|
1309
1522
|
return undefined;
|
|
1310
1523
|
}
|
|
1524
|
+
|
|
1525
|
+
// Guard against duplicate clicks while an async action on this exact element is already in-flight
|
|
1526
|
+
if (
|
|
1527
|
+
actionEl &&
|
|
1528
|
+
actionEl.__wActionBusy === true &&
|
|
1529
|
+
(!actionEl.hasAttribute || !actionEl.hasAttribute('data-w-allow-concurrent'))
|
|
1530
|
+
) {
|
|
1531
|
+
if (event && typeof event.preventDefault === 'function') {
|
|
1532
|
+
event.preventDefault();
|
|
1533
|
+
}
|
|
1534
|
+
return undefined;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// Built-in `data-w-confirm` guard
|
|
1538
|
+
if (actionEl && typeof actionEl.getAttribute === 'function') {
|
|
1539
|
+
const confirmMsg = actionEl.getAttribute('data-w-confirm');
|
|
1540
|
+
if (confirmMsg !== null && confirmMsg !== '') {
|
|
1541
|
+
const confirmFn =
|
|
1542
|
+
typeof window !== 'undefined' && typeof window.confirm === 'function'
|
|
1543
|
+
? window.confirm.bind(window)
|
|
1544
|
+
: null;
|
|
1545
|
+
if (confirmFn && !confirmFn(confirmMsg)) {
|
|
1546
|
+
if (event && typeof event.preventDefault === 'function') {
|
|
1547
|
+
event.preventDefault();
|
|
1548
|
+
}
|
|
1549
|
+
return undefined;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1311
1554
|
const decoratedEl = decorateElement(actionEl);
|
|
1312
1555
|
currentActionEl = decoratedEl || null;
|
|
1313
1556
|
const prevContext = activeControllerContext;
|
|
@@ -1315,17 +1558,28 @@ function createControllerRuntime(options = {}) {
|
|
|
1315
1558
|
try {
|
|
1316
1559
|
const res = fn(ctx, event, decoratedEl);
|
|
1317
1560
|
if (res && typeof res.then === 'function') {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1561
|
+
setActionBusy(actionName, 1);
|
|
1562
|
+
setElementBusyState(actionEl, true);
|
|
1563
|
+
return res.then(
|
|
1564
|
+
(val) => {
|
|
1565
|
+
setActionBusy(actionName, -1);
|
|
1566
|
+
setElementBusyState(actionEl, false);
|
|
1567
|
+
return val;
|
|
1568
|
+
},
|
|
1569
|
+
(err) => {
|
|
1570
|
+
setActionBusy(actionName, -1);
|
|
1571
|
+
setElementBusyState(actionEl, false);
|
|
1572
|
+
// Ignore canceled/superseded request errors on disconnected or superseded instances
|
|
1573
|
+
if (err && err.isCanceled) return undefined;
|
|
1574
|
+
handleControllerError(err, {
|
|
1575
|
+
action: actionName,
|
|
1576
|
+
event,
|
|
1577
|
+
actionEl: decoratedEl,
|
|
1578
|
+
phase: 'action',
|
|
1579
|
+
});
|
|
1580
|
+
throw err;
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1329
1583
|
}
|
|
1330
1584
|
return res;
|
|
1331
1585
|
} catch (err) {
|
|
@@ -1353,6 +1607,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1353
1607
|
state,
|
|
1354
1608
|
invokeAction,
|
|
1355
1609
|
isConnected,
|
|
1610
|
+
isLoadingTargetActive,
|
|
1356
1611
|
});
|
|
1357
1612
|
domDisposers.push(unbind);
|
|
1358
1613
|
|
|
@@ -1364,7 +1619,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1364
1619
|
const formsInScope = rootEl.querySelectorAll('form[data-w-form]');
|
|
1365
1620
|
for (let i = 0; i < formsInScope.length; i++) {
|
|
1366
1621
|
if (isTargetInControllerScope(rootEl, formsInScope[i])) {
|
|
1367
|
-
createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug });
|
|
1622
|
+
createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1368
1623
|
}
|
|
1369
1624
|
}
|
|
1370
1625
|
}
|
|
@@ -1433,6 +1688,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1433
1688
|
name: controllerName,
|
|
1434
1689
|
el: rootEl,
|
|
1435
1690
|
state,
|
|
1691
|
+
tasks: tasksProxy,
|
|
1436
1692
|
ctx,
|
|
1437
1693
|
invokeAction,
|
|
1438
1694
|
actions: new Proxy({}, {
|
|
@@ -1642,6 +1898,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1642
1898
|
api: defaultApi,
|
|
1643
1899
|
datastar,
|
|
1644
1900
|
debug,
|
|
1901
|
+
services,
|
|
1645
1902
|
});
|
|
1646
1903
|
standaloneForms.set(formEl, handle);
|
|
1647
1904
|
}
|
|
@@ -1666,6 +1923,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1666
1923
|
|
|
1667
1924
|
return {
|
|
1668
1925
|
register,
|
|
1926
|
+
services,
|
|
1669
1927
|
hasController(name) {
|
|
1670
1928
|
return definitions.has(name);
|
|
1671
1929
|
},
|
package/core/client/debug.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and lifecycle cleanup errors without logging raw form values or sensitive payloads.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const { NOOP_DEBUGGER } = require('./utils');
|
|
11
|
+
const { isArray, isNumber, NOOP_DEBUGGER, uid } = require('./utils');
|
|
12
12
|
|
|
13
13
|
const SENSITIVE_KEY_PATTERN = /password|passwd|secret|token|authorization|cookie|credit|card|cvv|ssn|api[-_]?key/i;
|
|
14
14
|
|
|
@@ -32,7 +32,7 @@ function summarizePayloadSafely(payload, includeSafePrimitives = false) {
|
|
|
32
32
|
if (typeof Blob !== 'undefined' && payload instanceof Blob) {
|
|
33
33
|
return { type: 'Blob', size: payload.size, mimeType: payload.type || 'application/octet-stream' };
|
|
34
34
|
}
|
|
35
|
-
if (
|
|
35
|
+
if (isArray(payload)) {
|
|
36
36
|
return { type: 'Array', length: payload.length };
|
|
37
37
|
}
|
|
38
38
|
if (typeof payload === 'object') {
|
|
@@ -58,7 +58,7 @@ function summarizePayloadSafely(payload, includeSafePrimitives = false) {
|
|
|
58
58
|
*/
|
|
59
59
|
function createActiveDebugger(options = {}) {
|
|
60
60
|
let enabled = Boolean(options.enabled);
|
|
61
|
-
const maxEvents =
|
|
61
|
+
const maxEvents = isNumber(options.maxEvents) && options.maxEvents > 0 ? options.maxEvents : 200;
|
|
62
62
|
const customLogger = typeof options.logger === 'function' ? options.logger : null;
|
|
63
63
|
|
|
64
64
|
const events = [];
|
|
@@ -73,7 +73,7 @@ function createActiveDebugger(options = {}) {
|
|
|
73
73
|
|
|
74
74
|
function pushEvent(type, detail = {}) {
|
|
75
75
|
const entry = {
|
|
76
|
-
id:
|
|
76
|
+
id: uid('evt'),
|
|
77
77
|
type,
|
|
78
78
|
timestamp: Date.now(),
|
|
79
79
|
...detail,
|
|
@@ -192,7 +192,7 @@ function createActiveDebugger(options = {}) {
|
|
|
192
192
|
completedAt: Date.now(),
|
|
193
193
|
},
|
|
194
194
|
invalidation: {
|
|
195
|
-
tags:
|
|
195
|
+
tags: isArray(tags) ? [...tags] : [],
|
|
196
196
|
triggeredAt: Date.now(),
|
|
197
197
|
},
|
|
198
198
|
refresh: null,
|
|
@@ -215,11 +215,11 @@ function createActiveDebugger(options = {}) {
|
|
|
215
215
|
recordChainRefresh(chainId, { fragmentIds, status, loadMs, renderMs, totalServerMs, error }) {
|
|
216
216
|
const chain = chainId ? causalChains.get(chainId) : null;
|
|
217
217
|
const refreshData = {
|
|
218
|
-
fragmentIds:
|
|
218
|
+
fragmentIds: isArray(fragmentIds) ? [...fragmentIds] : [],
|
|
219
219
|
status: status || 'ok',
|
|
220
|
-
loadMs:
|
|
221
|
-
renderMs:
|
|
222
|
-
totalServerMs:
|
|
220
|
+
loadMs: isNumber(loadMs) ? loadMs : null,
|
|
221
|
+
renderMs: isNumber(renderMs) ? renderMs : null,
|
|
222
|
+
totalServerMs: isNumber(totalServerMs) ? totalServerMs : null,
|
|
223
223
|
error: error ? String(error.message || error) : null,
|
|
224
224
|
completedAt: Date.now(),
|
|
225
225
|
};
|
|
@@ -234,9 +234,9 @@ function createActiveDebugger(options = {}) {
|
|
|
234
234
|
const patchData = {
|
|
235
235
|
instanceId,
|
|
236
236
|
fragmentName: fragmentName || null,
|
|
237
|
-
patchMs:
|
|
238
|
-
loadMs:
|
|
239
|
-
renderMs:
|
|
237
|
+
patchMs: isNumber(patchMs) ? patchMs : 0,
|
|
238
|
+
loadMs: isNumber(loadMs) ? loadMs : null,
|
|
239
|
+
renderMs: isNumber(renderMs) ? renderMs : null,
|
|
240
240
|
completedAt: Date.now(),
|
|
241
241
|
};
|
|
242
242
|
if (chain) {
|