webspresso 0.0.110 → 0.0.112
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 +190 -51
- package/core/client/api.js +23 -21
- package/core/client/controller.js +407 -40
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +163 -29
- package/core/client/fragments.js +616 -118
- package/core/client/host.js +369 -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 +142 -22
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +334 -29
- 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();
|
|
@@ -479,6 +495,7 @@ function createControllerRuntime(options = {}) {
|
|
|
479
495
|
const activeInstances = new Map();
|
|
480
496
|
// Standalone forms (forms without a controller wrapper)
|
|
481
497
|
const standaloneForms = new WeakMap();
|
|
498
|
+
const activeStandaloneForms = new Set();
|
|
482
499
|
|
|
483
500
|
/**
|
|
484
501
|
* Register a controller definition with collision checking.
|
|
@@ -534,7 +551,7 @@ function createControllerRuntime(options = {}) {
|
|
|
534
551
|
};
|
|
535
552
|
definitions.set(canonicalName, entry);
|
|
536
553
|
|
|
537
|
-
if (
|
|
554
|
+
if (isArray(meta.aliases)) {
|
|
538
555
|
for (const alias of meta.aliases) {
|
|
539
556
|
if (alias && !definitions.has(alias)) {
|
|
540
557
|
definitions.set(alias, entry);
|
|
@@ -587,6 +604,79 @@ function createControllerRuntime(options = {}) {
|
|
|
587
604
|
const scopedForms = new Map();
|
|
588
605
|
const scopedTasks = {};
|
|
589
606
|
const scopedTasksList = [];
|
|
607
|
+
const busyActionCounts = new Map();
|
|
608
|
+
let totalBusyActions = 0;
|
|
609
|
+
const busySignalPrefix = `__w_inst_${instanceId}.__busy`;
|
|
610
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.any`, false);
|
|
611
|
+
|
|
612
|
+
function syncAnyBusySignal() {
|
|
613
|
+
let busy = totalBusyActions > 0;
|
|
614
|
+
if (!busy) {
|
|
615
|
+
for (let i = 0; i < scopedTasksList.length; i++) {
|
|
616
|
+
if (scopedTasksList[i].pending) {
|
|
617
|
+
busy = true;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.any`, busy);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function setActionBusy(actionName, delta) {
|
|
626
|
+
const prevCount = busyActionCounts.get(actionName) || 0;
|
|
627
|
+
const nextCount = Math.max(0, prevCount + delta);
|
|
628
|
+
if (nextCount === 0) {
|
|
629
|
+
busyActionCounts.delete(actionName);
|
|
630
|
+
} else {
|
|
631
|
+
busyActionCounts.set(actionName, nextCount);
|
|
632
|
+
}
|
|
633
|
+
totalBusyActions = Math.max(0, totalBusyActions + delta);
|
|
634
|
+
datastar.engine.signals.setValue(`${busySignalPrefix}.action_${actionName}`, nextCount > 0);
|
|
635
|
+
syncAnyBusySignal();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function isActionBusy(actionName) {
|
|
639
|
+
if (!actionName) return false;
|
|
640
|
+
datastar.engine.signals.value(`${busySignalPrefix}.action_${actionName}`);
|
|
641
|
+
return (busyActionCounts.get(actionName) || 0) > 0;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function checkIsBusy() {
|
|
645
|
+
datastar.engine.signals.value(`${busySignalPrefix}.any`);
|
|
646
|
+
if (totalBusyActions > 0) return true;
|
|
647
|
+
for (let i = 0; i < scopedTasksList.length; i++) {
|
|
648
|
+
if (scopedTasksList[i].pending) return true;
|
|
649
|
+
}
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function isLoadingTargetActive(rawSpec) {
|
|
654
|
+
const spec = typeof rawSpec === 'string' ? rawSpec.trim() : '';
|
|
655
|
+
if (!spec || spec === 'true') {
|
|
656
|
+
return checkIsBusy();
|
|
657
|
+
}
|
|
658
|
+
const parts = spec.split(',').map((s) => s.trim()).filter(Boolean);
|
|
659
|
+
for (let i = 0; i < parts.length; i++) {
|
|
660
|
+
const token = parts[i];
|
|
661
|
+
if (reg.definition.actions && Object.prototype.hasOwnProperty.call(reg.definition.actions, token)) {
|
|
662
|
+
if (isActionBusy(token)) return true;
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (
|
|
666
|
+
Object.prototype.hasOwnProperty.call(scopedTasks, token) ||
|
|
667
|
+
(reg.definition.tasks && Object.prototype.hasOwnProperty.call(reg.definition.tasks, token))
|
|
668
|
+
) {
|
|
669
|
+
const t = getTask(token);
|
|
670
|
+
if (t && t.pending) return true;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (Boolean(datastar.evaluateStateExpression(state, token))) {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
|
|
590
680
|
let currentActionEl = null;
|
|
591
681
|
let lastError = null;
|
|
592
682
|
const dynamicErrorListeners = new Set();
|
|
@@ -647,10 +737,29 @@ function createControllerRuntime(options = {}) {
|
|
|
647
737
|
const taskKey = taskOpts.key || `task_${++taskSeq}`;
|
|
648
738
|
const signalPrefix = `__w_inst_${instanceId}.task_${taskKey}`;
|
|
649
739
|
|
|
740
|
+
// React Query-style automatic revalidation options for tasks
|
|
741
|
+
const staleTimeMs = parseDurationMs(taskOpts.staleTime, 0);
|
|
742
|
+
const refetchOnMountMode = parseRefetchMode(
|
|
743
|
+
taskOpts.refetchOnMount !== undefined ? taskOpts.refetchOnMount : taskOpts.refetchOnVisit,
|
|
744
|
+
false
|
|
745
|
+
);
|
|
746
|
+
const refetchOnFocusMode = parseRefetchMode(
|
|
747
|
+
taskOpts.refetchOnWindowFocus !== undefined ? taskOpts.refetchOnWindowFocus : taskOpts.refetchOnFocus,
|
|
748
|
+
false
|
|
749
|
+
);
|
|
750
|
+
const refetchOnReconnectMode = parseRefetchMode(taskOpts.refetchOnReconnect, false);
|
|
751
|
+
const refetchIntervalMs = parseDurationMs(taskOpts.refetchInterval, 0);
|
|
752
|
+
const refetchIntervalInBg =
|
|
753
|
+
parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
754
|
+
|
|
650
755
|
let currentRunVersion = 0;
|
|
651
756
|
let activeRuns = 0;
|
|
652
757
|
let inFlightAbort = null;
|
|
653
758
|
let queuedPromise = Promise.resolve();
|
|
759
|
+
let _lastUpdatedAt = 0;
|
|
760
|
+
let taskIntervalTimer = null;
|
|
761
|
+
let focusDebounceTimer = null;
|
|
762
|
+
const taskCleanupListeners = [];
|
|
654
763
|
|
|
655
764
|
datastar.engine.signals.setValue(`${signalPrefix}.pending`, false);
|
|
656
765
|
datastar.engine.signals.setValue(`${signalPrefix}.data`, null);
|
|
@@ -661,13 +770,29 @@ function createControllerRuntime(options = {}) {
|
|
|
661
770
|
let _data = null;
|
|
662
771
|
let _error = null;
|
|
663
772
|
let _stale = false;
|
|
773
|
+
let _lastArgs = [];
|
|
774
|
+
|
|
775
|
+
function isTaskStale(mode = true) {
|
|
776
|
+
if (!mode) return false;
|
|
777
|
+
if (mode === 'always') return true;
|
|
778
|
+
if (_lastUpdatedAt === 0) return true;
|
|
779
|
+
if (staleTimeMs <= 0) return true;
|
|
780
|
+
return Date.now() - _lastUpdatedAt >= staleTimeMs;
|
|
781
|
+
}
|
|
664
782
|
|
|
665
783
|
function updateSignal(name, val) {
|
|
666
784
|
datastar.engine.signals.setValue(`${signalPrefix}.${name}`, val);
|
|
785
|
+
if (name === 'pending') {
|
|
786
|
+
syncAnyBusySignal();
|
|
787
|
+
}
|
|
667
788
|
}
|
|
668
789
|
|
|
669
790
|
async function run(...args) {
|
|
670
791
|
if (!isConnected()) return undefined;
|
|
792
|
+
if (args.length > 0) {
|
|
793
|
+
_lastArgs = [...args];
|
|
794
|
+
}
|
|
795
|
+
const effectiveArgs = args.length > 0 ? args : _lastArgs;
|
|
671
796
|
|
|
672
797
|
if (policy === 'drop' && activeRuns > 0) {
|
|
673
798
|
if (debug && typeof debug.trackDiscardedResponse === 'function') {
|
|
@@ -683,12 +808,12 @@ function createControllerRuntime(options = {}) {
|
|
|
683
808
|
|
|
684
809
|
if (policy === 'queue') {
|
|
685
810
|
const wait = queuedPromise;
|
|
686
|
-
const nextRun = wait.then(() => executeRun(...
|
|
811
|
+
const nextRun = wait.then(() => executeRun(...effectiveArgs)).catch(() => {});
|
|
687
812
|
queuedPromise = nextRun;
|
|
688
813
|
return nextRun;
|
|
689
814
|
}
|
|
690
815
|
|
|
691
|
-
return executeRun(...
|
|
816
|
+
return executeRun(...effectiveArgs);
|
|
692
817
|
}
|
|
693
818
|
|
|
694
819
|
async function executeRun(...args) {
|
|
@@ -715,19 +840,20 @@ function createControllerRuntime(options = {}) {
|
|
|
715
840
|
try {
|
|
716
841
|
const result = await runFn(...args);
|
|
717
842
|
|
|
718
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
843
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
719
844
|
return undefined;
|
|
720
845
|
}
|
|
721
846
|
|
|
722
847
|
_data = result;
|
|
723
848
|
_error = null;
|
|
724
849
|
_stale = false;
|
|
850
|
+
_lastUpdatedAt = Date.now();
|
|
725
851
|
updateSignal('data', result);
|
|
726
852
|
updateSignal('error', null);
|
|
727
853
|
updateSignal('stale', false);
|
|
728
854
|
return result;
|
|
729
855
|
} catch (err) {
|
|
730
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
856
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
731
857
|
return undefined;
|
|
732
858
|
}
|
|
733
859
|
if (err && (err.isCanceled || err.name === 'AbortError')) {
|
|
@@ -745,7 +871,11 @@ function createControllerRuntime(options = {}) {
|
|
|
745
871
|
inFlightAbort = null;
|
|
746
872
|
}
|
|
747
873
|
activeRuns = Math.max(0, activeRuns - 1);
|
|
748
|
-
|
|
874
|
+
const shouldClearPending =
|
|
875
|
+
policy === 'parallel'
|
|
876
|
+
? activeRuns === 0
|
|
877
|
+
: runVersion === currentRunVersion || activeRuns === 0;
|
|
878
|
+
if (shouldClearPending) {
|
|
749
879
|
_pending = false;
|
|
750
880
|
_stale = false;
|
|
751
881
|
if (isConnected()) {
|
|
@@ -787,16 +917,106 @@ function createControllerRuntime(options = {}) {
|
|
|
787
917
|
},
|
|
788
918
|
get stale() {
|
|
789
919
|
datastar.engine.signals.value(`${signalPrefix}.stale`);
|
|
790
|
-
return _stale;
|
|
920
|
+
return _stale || (_lastUpdatedAt > 0 && staleTimeMs > 0 && Date.now() - _lastUpdatedAt >= staleTimeMs);
|
|
791
921
|
},
|
|
922
|
+
get lastUpdatedAt() {
|
|
923
|
+
return _lastUpdatedAt;
|
|
924
|
+
},
|
|
925
|
+
get lastArgs() {
|
|
926
|
+
return [..._lastArgs];
|
|
927
|
+
},
|
|
928
|
+
isStale: () => isTaskStale(true),
|
|
792
929
|
run,
|
|
793
930
|
cancel,
|
|
794
931
|
dispose() {
|
|
932
|
+
if (taskIntervalTimer) {
|
|
933
|
+
clearInterval(taskIntervalTimer);
|
|
934
|
+
taskIntervalTimer = null;
|
|
935
|
+
}
|
|
936
|
+
if (focusDebounceTimer) {
|
|
937
|
+
clearTimeout(focusDebounceTimer);
|
|
938
|
+
focusDebounceTimer = null;
|
|
939
|
+
}
|
|
940
|
+
while (taskCleanupListeners.length > 0) {
|
|
941
|
+
const removeFn = taskCleanupListeners.pop();
|
|
942
|
+
try {
|
|
943
|
+
removeFn();
|
|
944
|
+
} catch (_e) {}
|
|
945
|
+
}
|
|
795
946
|
cancel();
|
|
796
947
|
datastar.engine.signals.remove(signalPrefix);
|
|
797
948
|
},
|
|
798
949
|
};
|
|
799
950
|
|
|
951
|
+
// Set up automatic background revalidation (mount / focus / reconnect / interval)
|
|
952
|
+
const doc = (rootEl && rootEl.ownerDocument) || (typeof document !== 'undefined' ? document : null);
|
|
953
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
954
|
+
|
|
955
|
+
function isDocHidden() {
|
|
956
|
+
return Boolean(doc && doc.visibilityState === 'hidden');
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (refetchOnMountMode) {
|
|
960
|
+
Promise.resolve().then(() => {
|
|
961
|
+
if (isConnected() && isTaskStale(refetchOnMountMode)) {
|
|
962
|
+
run().catch(() => {});
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (refetchOnFocusMode) {
|
|
968
|
+
const onTaskFocus = () => {
|
|
969
|
+
if (!isConnected() || isDocHidden()) return;
|
|
970
|
+
if (focusDebounceTimer) clearTimeout(focusDebounceTimer);
|
|
971
|
+
focusDebounceTimer = setTimeout(() => {
|
|
972
|
+
focusDebounceTimer = null;
|
|
973
|
+
if (isConnected() && !isDocHidden() && activeRuns === 0 && isTaskStale(refetchOnFocusMode)) {
|
|
974
|
+
run().catch(() => {});
|
|
975
|
+
}
|
|
976
|
+
}, 20);
|
|
977
|
+
};
|
|
978
|
+
const onVisChange = () => {
|
|
979
|
+
if (!isDocHidden()) onTaskFocus();
|
|
980
|
+
};
|
|
981
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
982
|
+
doc.addEventListener('visibilitychange', onVisChange);
|
|
983
|
+
taskCleanupListeners.push(() => doc.removeEventListener('visibilitychange', onVisChange));
|
|
984
|
+
}
|
|
985
|
+
if (win && typeof win.addEventListener === 'function') {
|
|
986
|
+
win.addEventListener('focus', onTaskFocus);
|
|
987
|
+
taskCleanupListeners.push(() => win.removeEventListener('focus', onTaskFocus));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (refetchOnReconnectMode && win && typeof win.addEventListener === 'function') {
|
|
992
|
+
const onTaskOnline = () => {
|
|
993
|
+
if (isConnected() && activeRuns === 0 && isTaskStale(refetchOnReconnectMode)) {
|
|
994
|
+
run().catch(() => {});
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
win.addEventListener('online', onTaskOnline);
|
|
998
|
+
taskCleanupListeners.push(() => win.removeEventListener('online', onTaskOnline));
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
if (refetchIntervalMs > 0) {
|
|
1002
|
+
taskIntervalTimer = setInterval(() => {
|
|
1003
|
+
if (!isConnected()) {
|
|
1004
|
+
if (taskIntervalTimer) {
|
|
1005
|
+
clearInterval(taskIntervalTimer);
|
|
1006
|
+
taskIntervalTimer = null;
|
|
1007
|
+
}
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
if (!refetchIntervalInBg && isDocHidden()) return;
|
|
1011
|
+
if (activeRuns === 0) {
|
|
1012
|
+
run().catch(() => {});
|
|
1013
|
+
}
|
|
1014
|
+
}, refetchIntervalMs);
|
|
1015
|
+
if (taskIntervalTimer && typeof taskIntervalTimer.unref === 'function') {
|
|
1016
|
+
taskIntervalTimer.unref();
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
800
1020
|
scopedTasksList.push(taskObj);
|
|
801
1021
|
return taskObj;
|
|
802
1022
|
}
|
|
@@ -826,7 +1046,7 @@ function createControllerRuntime(options = {}) {
|
|
|
826
1046
|
getter = () => source(ctx);
|
|
827
1047
|
} else if (typeof source === 'string') {
|
|
828
1048
|
getter = () => resolveStatePath(state, source);
|
|
829
|
-
} else if (
|
|
1049
|
+
} else if (isArray(source)) {
|
|
830
1050
|
getter = () =>
|
|
831
1051
|
source.map((s) =>
|
|
832
1052
|
typeof s === 'function'
|
|
@@ -876,7 +1096,7 @@ function createControllerRuntime(options = {}) {
|
|
|
876
1096
|
let newValue;
|
|
877
1097
|
try {
|
|
878
1098
|
newValue = getter();
|
|
879
|
-
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !
|
|
1099
|
+
if ((deep || (source && typeof source === 'object' && typeof source !== 'function' && !isArray(source))) && newValue && typeof newValue === 'object') {
|
|
880
1100
|
deepTouch(newValue);
|
|
881
1101
|
}
|
|
882
1102
|
} catch (err) {
|
|
@@ -884,7 +1104,7 @@ function createControllerRuntime(options = {}) {
|
|
|
884
1104
|
return;
|
|
885
1105
|
}
|
|
886
1106
|
|
|
887
|
-
const isArr =
|
|
1107
|
+
const isArr = isArray(source);
|
|
888
1108
|
|
|
889
1109
|
if (isFirstRun) {
|
|
890
1110
|
isFirstRun = false;
|
|
@@ -1069,6 +1289,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1069
1289
|
api: scopedApi,
|
|
1070
1290
|
datastar,
|
|
1071
1291
|
debug,
|
|
1292
|
+
services,
|
|
1072
1293
|
...formOptions,
|
|
1073
1294
|
});
|
|
1074
1295
|
scopedForms.set(handle.name, handle);
|
|
@@ -1151,6 +1372,9 @@ function createControllerRuntime(options = {}) {
|
|
|
1151
1372
|
root: defaultStorage,
|
|
1152
1373
|
});
|
|
1153
1374
|
const persistConfig = normalizePersistConfig(reg.definition.persist, controllerName);
|
|
1375
|
+
let persistTimer = null;
|
|
1376
|
+
let lastSavedJson = '';
|
|
1377
|
+
const persistenceConflicts = [];
|
|
1154
1378
|
function resolvePersistDriver(pConfig) {
|
|
1155
1379
|
if (!pConfig) return storageCtx.local;
|
|
1156
1380
|
if (pConfig.isGlobal) {
|
|
@@ -1159,6 +1383,30 @@ function createControllerRuntime(options = {}) {
|
|
|
1159
1383
|
return storageCtx[pConfig.storageType] || storageCtx.local;
|
|
1160
1384
|
}
|
|
1161
1385
|
|
|
1386
|
+
function checkFragmentPersistenceConflict(idOrName, input) {
|
|
1387
|
+
if (!persistConfig || !fragments || typeof fragments.getInstance !== 'function' || !input || typeof input !== 'object') {
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
const inst =
|
|
1391
|
+
fragments.getInstance(idOrName) ||
|
|
1392
|
+
(typeof fragments.getInstances === 'function'
|
|
1393
|
+
? fragments.getInstances().find((i) => i.name === idOrName)
|
|
1394
|
+
: null);
|
|
1395
|
+
if (inst && inst.persistInput && inst.persistInput.key !== persistConfig.storageKey) {
|
|
1396
|
+
const conflict = {
|
|
1397
|
+
controller: controllerName,
|
|
1398
|
+
controllerStorageKey: persistConfig.storageKey,
|
|
1399
|
+
fragment: inst.instanceId,
|
|
1400
|
+
fragmentStorageKey: inst.persistInput.key,
|
|
1401
|
+
keys: Object.keys(input),
|
|
1402
|
+
};
|
|
1403
|
+
persistenceConflicts.push(conflict);
|
|
1404
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
1405
|
+
debug.recordEvent('persist:conflict', conflict);
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1162
1410
|
// 6. Context object passed to `connect(ctx)` and `actions[name](ctx, event, actionEl)`
|
|
1163
1411
|
ctx = Object.freeze({
|
|
1164
1412
|
instanceId,
|
|
@@ -1168,16 +1416,34 @@ function createControllerRuntime(options = {}) {
|
|
|
1168
1416
|
return state;
|
|
1169
1417
|
},
|
|
1170
1418
|
api: scopedApi,
|
|
1419
|
+
services,
|
|
1171
1420
|
forms: formsAccessor,
|
|
1172
1421
|
target: targetAccessor,
|
|
1173
1422
|
targets: targetsAccessor,
|
|
1174
1423
|
tasks: tasksProxy,
|
|
1175
1424
|
task: createTaskInstance,
|
|
1176
1425
|
storage: storageCtx,
|
|
1426
|
+
get persistenceConflicts() {
|
|
1427
|
+
return [...persistenceConflicts];
|
|
1428
|
+
},
|
|
1177
1429
|
clearPersistedState: () => {
|
|
1430
|
+
if (persistTimer) {
|
|
1431
|
+
clearTimeout(persistTimer);
|
|
1432
|
+
persistTimer = null;
|
|
1433
|
+
}
|
|
1178
1434
|
if (persistConfig) {
|
|
1179
1435
|
const driver = resolvePersistDriver(persistConfig);
|
|
1180
1436
|
driver.remove(persistConfig.storageKey);
|
|
1437
|
+
if (state && initialRawState) {
|
|
1438
|
+
const keysToTrack = persistConfig.keys || Object.keys(initialRawState);
|
|
1439
|
+
const currentPayload = {};
|
|
1440
|
+
for (const k of keysToTrack) {
|
|
1441
|
+
currentPayload[k] = state[k];
|
|
1442
|
+
}
|
|
1443
|
+
lastSavedJson = JSON.stringify(currentPayload);
|
|
1444
|
+
} else {
|
|
1445
|
+
lastSavedJson = '';
|
|
1446
|
+
}
|
|
1181
1447
|
}
|
|
1182
1448
|
},
|
|
1183
1449
|
param: (name, maybeEl) => {
|
|
@@ -1201,6 +1467,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1201
1467
|
signal: lifecycleAbort.signal,
|
|
1202
1468
|
isConnected,
|
|
1203
1469
|
isDisposed: () => !isConnected(),
|
|
1470
|
+
get isBusy() {
|
|
1471
|
+
return checkIsBusy();
|
|
1472
|
+
},
|
|
1473
|
+
isActionBusy,
|
|
1204
1474
|
get lastError() {
|
|
1205
1475
|
return lastError;
|
|
1206
1476
|
},
|
|
@@ -1217,7 +1487,12 @@ function createControllerRuntime(options = {}) {
|
|
|
1217
1487
|
return unregister;
|
|
1218
1488
|
},
|
|
1219
1489
|
invalidate: (tags) => (fragments ? fragments.invalidate(tags) : Promise.resolve({ ok: true, refreshed: [] })),
|
|
1490
|
+
retryFailedFragments: () =>
|
|
1491
|
+
fragments && typeof fragments.retryFailed === 'function'
|
|
1492
|
+
? fragments.retryFailed()
|
|
1493
|
+
: Promise.resolve({ refreshed: [], skipped: true }),
|
|
1220
1494
|
refresh: async (idOrName, input, opts) => {
|
|
1495
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1221
1496
|
if (!fragments) {
|
|
1222
1497
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1223
1498
|
if (opts && typeof opts.onSuccess === 'function') await opts.onSuccess(empty);
|
|
@@ -1233,6 +1508,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1233
1508
|
return inst && inst.input ? { ...inst.input } : {};
|
|
1234
1509
|
},
|
|
1235
1510
|
refresh: (input, opts) => {
|
|
1511
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1236
1512
|
if (!fragments) {
|
|
1237
1513
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1238
1514
|
if (opts && typeof opts.onSuccess === 'function') {
|
|
@@ -1265,14 +1541,34 @@ function createControllerRuntime(options = {}) {
|
|
|
1265
1541
|
);
|
|
1266
1542
|
state = stateManager.state;
|
|
1267
1543
|
|
|
1268
|
-
// Hydrate persisted state if defined
|
|
1544
|
+
// Hydrate persisted state if defined (with schema-compatible type validation against corrupted storage)
|
|
1269
1545
|
if (persistConfig) {
|
|
1270
1546
|
const driver = resolvePersistDriver(persistConfig);
|
|
1271
1547
|
const savedData = driver.get(persistConfig.storageKey, null);
|
|
1272
|
-
if (savedData && typeof savedData === 'object' && !
|
|
1548
|
+
if (savedData && typeof savedData === 'object' && !isArray(savedData)) {
|
|
1273
1549
|
for (const [k, v] of Object.entries(savedData)) {
|
|
1274
1550
|
if (persistConfig.keys && !persistConfig.keys.includes(k)) continue;
|
|
1275
|
-
if (Object.prototype.hasOwnProperty.call(initialRawState, k))
|
|
1551
|
+
if (!Object.prototype.hasOwnProperty.call(initialRawState, k)) continue;
|
|
1552
|
+
const initVal = initialRawState[k];
|
|
1553
|
+
if (initVal !== null && typeof initVal === 'object' && !isArray(initVal)) {
|
|
1554
|
+
if (v !== null && typeof v === 'object' && !isArray(v)) {
|
|
1555
|
+
const merged = { ...initVal };
|
|
1556
|
+
for (const [subKey, subVal] of Object.entries(v)) {
|
|
1557
|
+
if (Object.prototype.hasOwnProperty.call(initVal, subKey)) {
|
|
1558
|
+
if (initVal[subKey] === null || typeof subVal === typeof initVal[subKey]) {
|
|
1559
|
+
merged[subKey] = subVal;
|
|
1560
|
+
}
|
|
1561
|
+
} else {
|
|
1562
|
+
merged[subKey] = subVal;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
state[k] = merged;
|
|
1566
|
+
}
|
|
1567
|
+
} else if (isArray(initVal)) {
|
|
1568
|
+
if (isArray(v)) {
|
|
1569
|
+
state[k] = v;
|
|
1570
|
+
}
|
|
1571
|
+
} else if (initVal === null || initVal === undefined || typeof v === typeof initVal) {
|
|
1276
1572
|
state[k] = v;
|
|
1277
1573
|
}
|
|
1278
1574
|
}
|
|
@@ -1308,6 +1604,36 @@ function createControllerRuntime(options = {}) {
|
|
|
1308
1604
|
if (typeof fn !== 'function') {
|
|
1309
1605
|
return undefined;
|
|
1310
1606
|
}
|
|
1607
|
+
|
|
1608
|
+
// Guard against duplicate clicks while an async action on this exact element is already in-flight
|
|
1609
|
+
if (
|
|
1610
|
+
actionEl &&
|
|
1611
|
+
actionEl.__wActionBusy === true &&
|
|
1612
|
+
(!actionEl.hasAttribute || !actionEl.hasAttribute('data-w-allow-concurrent'))
|
|
1613
|
+
) {
|
|
1614
|
+
if (event && typeof event.preventDefault === 'function') {
|
|
1615
|
+
event.preventDefault();
|
|
1616
|
+
}
|
|
1617
|
+
return undefined;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// Built-in `data-w-confirm` guard
|
|
1621
|
+
if (actionEl && typeof actionEl.getAttribute === 'function') {
|
|
1622
|
+
const confirmMsg = actionEl.getAttribute('data-w-confirm');
|
|
1623
|
+
if (confirmMsg !== null && confirmMsg !== '') {
|
|
1624
|
+
const confirmFn =
|
|
1625
|
+
typeof window !== 'undefined' && typeof window.confirm === 'function'
|
|
1626
|
+
? window.confirm.bind(window)
|
|
1627
|
+
: null;
|
|
1628
|
+
if (confirmFn && !confirmFn(confirmMsg)) {
|
|
1629
|
+
if (event && typeof event.preventDefault === 'function') {
|
|
1630
|
+
event.preventDefault();
|
|
1631
|
+
}
|
|
1632
|
+
return undefined;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1311
1637
|
const decoratedEl = decorateElement(actionEl);
|
|
1312
1638
|
currentActionEl = decoratedEl || null;
|
|
1313
1639
|
const prevContext = activeControllerContext;
|
|
@@ -1315,17 +1641,28 @@ function createControllerRuntime(options = {}) {
|
|
|
1315
1641
|
try {
|
|
1316
1642
|
const res = fn(ctx, event, decoratedEl);
|
|
1317
1643
|
if (res && typeof res.then === 'function') {
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1644
|
+
setActionBusy(actionName, 1);
|
|
1645
|
+
setElementBusyState(actionEl, true);
|
|
1646
|
+
return res.then(
|
|
1647
|
+
(val) => {
|
|
1648
|
+
setActionBusy(actionName, -1);
|
|
1649
|
+
setElementBusyState(actionEl, false);
|
|
1650
|
+
return val;
|
|
1651
|
+
},
|
|
1652
|
+
(err) => {
|
|
1653
|
+
setActionBusy(actionName, -1);
|
|
1654
|
+
setElementBusyState(actionEl, false);
|
|
1655
|
+
// Ignore canceled/superseded request errors on disconnected or superseded instances
|
|
1656
|
+
if (err && err.isCanceled) return undefined;
|
|
1657
|
+
handleControllerError(err, {
|
|
1658
|
+
action: actionName,
|
|
1659
|
+
event,
|
|
1660
|
+
actionEl: decoratedEl,
|
|
1661
|
+
phase: 'action',
|
|
1662
|
+
});
|
|
1663
|
+
throw err;
|
|
1664
|
+
}
|
|
1665
|
+
);
|
|
1329
1666
|
}
|
|
1330
1667
|
return res;
|
|
1331
1668
|
} catch (err) {
|
|
@@ -1353,8 +1690,19 @@ function createControllerRuntime(options = {}) {
|
|
|
1353
1690
|
state,
|
|
1354
1691
|
invokeAction,
|
|
1355
1692
|
isConnected,
|
|
1693
|
+
isLoadingTargetActive,
|
|
1356
1694
|
});
|
|
1357
|
-
domDisposers.
|
|
1695
|
+
if (domDisposers.length === 0) {
|
|
1696
|
+
domDisposers.push(unbind);
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// Prune disconnected forms from scopedForms
|
|
1700
|
+
for (const [fName, fh] of Array.from(scopedForms.entries())) {
|
|
1701
|
+
if (!fh.el || fh.el.isConnected === false || !rootEl.contains(fh.el)) {
|
|
1702
|
+
fh.destroy();
|
|
1703
|
+
scopedForms.delete(fName);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1358
1706
|
|
|
1359
1707
|
// Also auto-initialize any `<form data-w-form>` inside this controller's scope
|
|
1360
1708
|
if (rootEl.tagName === 'FORM' && rootEl.hasAttribute('data-w-form')) {
|
|
@@ -1364,7 +1712,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1364
1712
|
const formsInScope = rootEl.querySelectorAll('form[data-w-form]');
|
|
1365
1713
|
for (let i = 0; i < formsInScope.length; i++) {
|
|
1366
1714
|
if (isTargetInControllerScope(rootEl, formsInScope[i])) {
|
|
1367
|
-
createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug });
|
|
1715
|
+
const fh = createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1716
|
+
if (fh && fh.name) {
|
|
1717
|
+
scopedForms.set(fh.name, fh);
|
|
1718
|
+
}
|
|
1368
1719
|
}
|
|
1369
1720
|
}
|
|
1370
1721
|
}
|
|
@@ -1375,8 +1726,6 @@ function createControllerRuntime(options = {}) {
|
|
|
1375
1726
|
// Auto-sync persisted state changes back to storage
|
|
1376
1727
|
if (persistConfig) {
|
|
1377
1728
|
const driver = resolvePersistDriver(persistConfig);
|
|
1378
|
-
let persistTimer = null;
|
|
1379
|
-
let lastSavedJson = '';
|
|
1380
1729
|
|
|
1381
1730
|
const stopPersistEffect = registerEffect(() => {
|
|
1382
1731
|
if (!isConnected()) return;
|
|
@@ -1433,6 +1782,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1433
1782
|
name: controllerName,
|
|
1434
1783
|
el: rootEl,
|
|
1435
1784
|
state,
|
|
1785
|
+
tasks: tasksProxy,
|
|
1436
1786
|
ctx,
|
|
1437
1787
|
invokeAction,
|
|
1438
1788
|
actions: new Proxy({}, {
|
|
@@ -1560,6 +1910,11 @@ function createControllerRuntime(options = {}) {
|
|
|
1560
1910
|
if (standaloneForm) {
|
|
1561
1911
|
standaloneForm.destroy();
|
|
1562
1912
|
standaloneForms.delete(node);
|
|
1913
|
+
activeStandaloneForms.delete(standaloneForm);
|
|
1914
|
+
} else if (node && node.__wFormHandle) {
|
|
1915
|
+
try {
|
|
1916
|
+
node.__wFormHandle.destroy();
|
|
1917
|
+
} catch (_e) {}
|
|
1563
1918
|
}
|
|
1564
1919
|
}
|
|
1565
1920
|
}
|
|
@@ -1642,8 +1997,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1642
1997
|
api: defaultApi,
|
|
1643
1998
|
datastar,
|
|
1644
1999
|
debug,
|
|
2000
|
+
services,
|
|
1645
2001
|
});
|
|
1646
2002
|
standaloneForms.set(formEl, handle);
|
|
2003
|
+
activeStandaloneForms.add(handle);
|
|
1647
2004
|
}
|
|
1648
2005
|
}
|
|
1649
2006
|
|
|
@@ -1666,6 +2023,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1666
2023
|
|
|
1667
2024
|
return {
|
|
1668
2025
|
register,
|
|
2026
|
+
services,
|
|
1669
2027
|
hasController(name) {
|
|
1670
2028
|
return definitions.has(name);
|
|
1671
2029
|
},
|
|
@@ -1686,10 +2044,19 @@ function createControllerRuntime(options = {}) {
|
|
|
1686
2044
|
getActiveInstances() {
|
|
1687
2045
|
return Array.from(activeInstances.values());
|
|
1688
2046
|
},
|
|
2047
|
+
getStandaloneFormCount() {
|
|
2048
|
+
return activeStandaloneForms.size;
|
|
2049
|
+
},
|
|
1689
2050
|
destroyAll() {
|
|
1690
2051
|
for (const inst of Array.from(activeInstances.values())) {
|
|
1691
2052
|
inst.destroy();
|
|
1692
2053
|
}
|
|
2054
|
+
for (const fh of Array.from(activeStandaloneForms.values())) {
|
|
2055
|
+
try {
|
|
2056
|
+
fh.destroy();
|
|
2057
|
+
} catch (_e) {}
|
|
2058
|
+
}
|
|
2059
|
+
activeStandaloneForms.clear();
|
|
1693
2060
|
},
|
|
1694
2061
|
};
|
|
1695
2062
|
}
|