webspresso 0.0.111 → 0.0.113
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 +700 -50
- package/core/client/api.js +147 -7
- package/core/client/controller.js +254 -22
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +598 -38
- package/core/client/fragments.js +644 -112
- package/core/client/host.js +5 -1
- package/core/client/index.js +13 -1
- package/core/client/lite.js +1 -1
- package/core/client/server-fragments.js +145 -37
- package/core/client/utils.js +72 -17
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +110 -2
- package/package.json +9 -8
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +67 -3
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
package/core/client/api.js
CHANGED
|
@@ -84,9 +84,28 @@ class ClientRequestError extends Error {
|
|
|
84
84
|
this.status === 408;
|
|
85
85
|
this.isDropped = Boolean(details.isDropped);
|
|
86
86
|
this.isSuperseded = Boolean(details.isSuperseded);
|
|
87
|
+
this.dispatched =
|
|
88
|
+
details.dispatched !== undefined
|
|
89
|
+
? Boolean(details.dispatched)
|
|
90
|
+
: !this.isDropped && (this.kind !== 'canceled' || Boolean(details.cause));
|
|
91
|
+
this.outcomeUnknown =
|
|
92
|
+
details.outcomeUnknown !== undefined
|
|
93
|
+
? Boolean(details.outcomeUnknown)
|
|
94
|
+
: MUTATION_METHODS.has(this.method) &&
|
|
95
|
+
this.dispatched &&
|
|
96
|
+
!this.isDropped &&
|
|
97
|
+
(this.kind === 'timeout' || this.kind === 'network' || this.kind === 'canceled');
|
|
98
|
+
this.outcome = this.outcomeUnknown
|
|
99
|
+
? 'unknown'
|
|
100
|
+
: !this.dispatched || this.isDropped
|
|
101
|
+
? 'not_sent'
|
|
102
|
+
: 'failed';
|
|
87
103
|
this.serverRollbackGuaranteed = false;
|
|
88
104
|
if (this.isCanceled) {
|
|
89
105
|
this.note = 'Client request cancellation/abort does not roll back server-side execution.';
|
|
106
|
+
} else if (this.outcomeUnknown) {
|
|
107
|
+
this.note =
|
|
108
|
+
'Mutation request was dispatched before response delivery failed; the server may have committed the operation.';
|
|
90
109
|
}
|
|
91
110
|
if (details.cause) {
|
|
92
111
|
this.cause = details.cause;
|
|
@@ -599,6 +618,110 @@ function abortableSleep(ms, signals = []) {
|
|
|
599
618
|
});
|
|
600
619
|
}
|
|
601
620
|
|
|
621
|
+
function parseXhrHeaders(rawHeaders) {
|
|
622
|
+
const map = new Map();
|
|
623
|
+
if (typeof rawHeaders === 'string' && rawHeaders) {
|
|
624
|
+
const lines = rawHeaders.trim().split(/[\r\n]+/);
|
|
625
|
+
for (let i = 0; i < lines.length; i++) {
|
|
626
|
+
const idx = lines[i].indexOf(':');
|
|
627
|
+
if (idx > 0) {
|
|
628
|
+
const k = lines[i].slice(0, idx).trim().toLowerCase();
|
|
629
|
+
const v = lines[i].slice(idx + 1).trim();
|
|
630
|
+
map.set(k, v);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
get(name) {
|
|
636
|
+
return map.get(String(name || '').toLowerCase()) ?? null;
|
|
637
|
+
},
|
|
638
|
+
has(name) {
|
|
639
|
+
return map.has(String(name || '').toLowerCase());
|
|
640
|
+
},
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function executeXhrUpload({ url, method, headers, body, credentials, signal, onUploadProgress }) {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
const xhr = new XMLHttpRequest();
|
|
647
|
+
xhr.open(method, url, true);
|
|
648
|
+
if (credentials === 'include' || credentials === 'same-origin') {
|
|
649
|
+
xhr.withCredentials = credentials === 'include';
|
|
650
|
+
}
|
|
651
|
+
if (headers && typeof headers === 'object') {
|
|
652
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
653
|
+
if (v !== undefined && v !== null) {
|
|
654
|
+
xhr.setRequestHeader(k, String(v));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (xhr.upload && typeof onUploadProgress === 'function') {
|
|
660
|
+
xhr.upload.onprogress = (evt) => {
|
|
661
|
+
const lengthComputable = Boolean(evt && evt.lengthComputable);
|
|
662
|
+
const loaded = evt && typeof evt.loaded === 'number' ? evt.loaded : 0;
|
|
663
|
+
const total = lengthComputable && evt && typeof evt.total === 'number' ? evt.total : 0;
|
|
664
|
+
const percent = lengthComputable && total > 0 ? Math.min(100, Math.round((loaded / total) * 100)) : null;
|
|
665
|
+
try {
|
|
666
|
+
onUploadProgress({ loaded, total, percent, lengthComputable });
|
|
667
|
+
} catch (_e) {}
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const onAbort = () => {
|
|
672
|
+
try {
|
|
673
|
+
xhr.abort();
|
|
674
|
+
} catch (_e) {}
|
|
675
|
+
};
|
|
676
|
+
if (signal) {
|
|
677
|
+
if (signal.aborted) {
|
|
678
|
+
const abortErr = new Error('The operation was aborted.');
|
|
679
|
+
abortErr.name = 'AbortError';
|
|
680
|
+
reject(abortErr);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const cleanup = () => {
|
|
687
|
+
if (signal) {
|
|
688
|
+
try {
|
|
689
|
+
signal.removeEventListener('abort', onAbort);
|
|
690
|
+
} catch (_e) {}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
xhr.onload = () => {
|
|
695
|
+
cleanup();
|
|
696
|
+
const status = xhr.status === 1223 ? 204 : xhr.status;
|
|
697
|
+
const rawText = typeof xhr.responseText === 'string' ? xhr.responseText : '';
|
|
698
|
+
const headerObj = parseXhrHeaders(xhr.getAllResponseHeaders ? xhr.getAllResponseHeaders() : '');
|
|
699
|
+
resolve({
|
|
700
|
+
ok: status >= 200 && status < 300,
|
|
701
|
+
status,
|
|
702
|
+
statusText: xhr.statusText || '',
|
|
703
|
+
headers: headerObj,
|
|
704
|
+
text: async () => rawText,
|
|
705
|
+
json: async () => (rawText ? JSON.parse(rawText) : null),
|
|
706
|
+
});
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
xhr.onerror = () => {
|
|
710
|
+
cleanup();
|
|
711
|
+
reject(new TypeError('Network request failed'));
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
xhr.onabort = () => {
|
|
715
|
+
cleanup();
|
|
716
|
+
const abortErr = new Error('The operation was aborted.');
|
|
717
|
+
abortErr.name = 'AbortError';
|
|
718
|
+
reject(abortErr);
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
xhr.send(body === undefined ? null : body);
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
|
|
602
725
|
/**
|
|
603
726
|
* Create a Typed API Client instance.
|
|
604
727
|
*
|
|
@@ -675,6 +798,8 @@ function createApiClient(config = {}) {
|
|
|
675
798
|
method: upperMethod,
|
|
676
799
|
url: requestUrl,
|
|
677
800
|
key: requestKey,
|
|
801
|
+
dispatched: false,
|
|
802
|
+
outcomeUnknown: false,
|
|
678
803
|
});
|
|
679
804
|
}
|
|
680
805
|
|
|
@@ -883,13 +1008,25 @@ function createApiClient(config = {}) {
|
|
|
883
1008
|
try {
|
|
884
1009
|
let response;
|
|
885
1010
|
try {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1011
|
+
if (typeof options.onUploadProgress === 'function' && typeof XMLHttpRequest !== 'undefined') {
|
|
1012
|
+
response = await executeXhrUpload({
|
|
1013
|
+
url: requestUrl,
|
|
1014
|
+
method: upperMethod,
|
|
1015
|
+
headers,
|
|
1016
|
+
body: serializedBody,
|
|
1017
|
+
credentials: options.credentials || 'same-origin',
|
|
1018
|
+
signal: abortController.signal,
|
|
1019
|
+
onUploadProgress: options.onUploadProgress,
|
|
1020
|
+
});
|
|
1021
|
+
} else {
|
|
1022
|
+
response = await fetchImpl(requestUrl, {
|
|
1023
|
+
method: upperMethod,
|
|
1024
|
+
headers,
|
|
1025
|
+
body: serializedBody,
|
|
1026
|
+
credentials: options.credentials || 'same-origin',
|
|
1027
|
+
signal: abortController.signal,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
893
1030
|
} catch (fetchErr) {
|
|
894
1031
|
if (timeoutTimer) {
|
|
895
1032
|
clearTimeout(timeoutTimer);
|
|
@@ -914,6 +1051,7 @@ function createApiClient(config = {}) {
|
|
|
914
1051
|
url: requestUrl,
|
|
915
1052
|
key: requestKey,
|
|
916
1053
|
isTimedOut: true,
|
|
1054
|
+
dispatched: true,
|
|
917
1055
|
cause: fetchErr,
|
|
918
1056
|
}
|
|
919
1057
|
);
|
|
@@ -947,6 +1085,7 @@ function createApiClient(config = {}) {
|
|
|
947
1085
|
url: requestUrl,
|
|
948
1086
|
key: requestKey,
|
|
949
1087
|
isSuperseded: superseded,
|
|
1088
|
+
dispatched: true,
|
|
950
1089
|
cause: fetchErr,
|
|
951
1090
|
}
|
|
952
1091
|
);
|
|
@@ -962,6 +1101,7 @@ function createApiClient(config = {}) {
|
|
|
962
1101
|
method: upperMethod,
|
|
963
1102
|
url: requestUrl,
|
|
964
1103
|
key: requestKey,
|
|
1104
|
+
dispatched: true,
|
|
965
1105
|
cause: fetchErr,
|
|
966
1106
|
}
|
|
967
1107
|
);
|
|
@@ -367,6 +367,8 @@ function defineController(definition = {}) {
|
|
|
367
367
|
tasks: definition.tasks && typeof definition.tasks === 'object' ? definition.tasks : {},
|
|
368
368
|
connect: typeof definition.connect === 'function' ? definition.connect : null,
|
|
369
369
|
disconnect: typeof definition.disconnect === 'function' ? definition.disconnect : null,
|
|
370
|
+
beforePatch: typeof definition.beforePatch === 'function' ? definition.beforePatch : null,
|
|
371
|
+
afterPatch: typeof definition.afterPatch === 'function' ? definition.afterPatch : null,
|
|
370
372
|
onError: typeof definition.onError === 'function' ? definition.onError : null,
|
|
371
373
|
persist: definition.persist || null,
|
|
372
374
|
watch: definition.watch && typeof definition.watch === 'object' ? definition.watch : null,
|
|
@@ -495,6 +497,7 @@ function createControllerRuntime(options = {}) {
|
|
|
495
497
|
const activeInstances = new Map();
|
|
496
498
|
// Standalone forms (forms without a controller wrapper)
|
|
497
499
|
const standaloneForms = new WeakMap();
|
|
500
|
+
const activeStandaloneForms = new Set();
|
|
498
501
|
|
|
499
502
|
/**
|
|
500
503
|
* Register a controller definition with collision checking.
|
|
@@ -737,19 +740,26 @@ function createControllerRuntime(options = {}) {
|
|
|
737
740
|
const signalPrefix = `__w_inst_${instanceId}.task_${taskKey}`;
|
|
738
741
|
|
|
739
742
|
// React Query-style automatic revalidation options for tasks
|
|
743
|
+
const isMutationTask = taskOpts.mutation === true || taskOpts.kind === 'mutation';
|
|
740
744
|
const staleTimeMs = parseDurationMs(taskOpts.staleTime, 0);
|
|
741
|
-
const refetchOnMountMode =
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
745
|
+
const refetchOnMountMode = isMutationTask
|
|
746
|
+
? false
|
|
747
|
+
: parseRefetchMode(
|
|
748
|
+
taskOpts.refetchOnMount !== undefined ? taskOpts.refetchOnMount : taskOpts.refetchOnVisit,
|
|
749
|
+
false
|
|
750
|
+
);
|
|
751
|
+
const refetchOnFocusMode = isMutationTask
|
|
752
|
+
? false
|
|
753
|
+
: parseRefetchMode(
|
|
754
|
+
taskOpts.refetchOnWindowFocus !== undefined ? taskOpts.refetchOnWindowFocus : taskOpts.refetchOnFocus,
|
|
755
|
+
false
|
|
756
|
+
);
|
|
757
|
+
const refetchOnReconnectMode = isMutationTask
|
|
758
|
+
? false
|
|
759
|
+
: parseRefetchMode(taskOpts.refetchOnReconnect, false);
|
|
760
|
+
const refetchIntervalMs = isMutationTask ? 0 : parseDurationMs(taskOpts.refetchInterval, 0);
|
|
751
761
|
const refetchIntervalInBg =
|
|
752
|
-
parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
762
|
+
!isMutationTask && parseRefetchMode(taskOpts.refetchIntervalInBackground, false) === true;
|
|
753
763
|
|
|
754
764
|
let currentRunVersion = 0;
|
|
755
765
|
let activeRuns = 0;
|
|
@@ -769,6 +779,7 @@ function createControllerRuntime(options = {}) {
|
|
|
769
779
|
let _data = null;
|
|
770
780
|
let _error = null;
|
|
771
781
|
let _stale = false;
|
|
782
|
+
let _lastArgs = [];
|
|
772
783
|
|
|
773
784
|
function isTaskStale(mode = true) {
|
|
774
785
|
if (!mode) return false;
|
|
@@ -787,6 +798,10 @@ function createControllerRuntime(options = {}) {
|
|
|
787
798
|
|
|
788
799
|
async function run(...args) {
|
|
789
800
|
if (!isConnected()) return undefined;
|
|
801
|
+
if (args.length > 0) {
|
|
802
|
+
_lastArgs = [...args];
|
|
803
|
+
}
|
|
804
|
+
const effectiveArgs = args.length > 0 ? args : _lastArgs;
|
|
790
805
|
|
|
791
806
|
if (policy === 'drop' && activeRuns > 0) {
|
|
792
807
|
if (debug && typeof debug.trackDiscardedResponse === 'function') {
|
|
@@ -802,12 +817,12 @@ function createControllerRuntime(options = {}) {
|
|
|
802
817
|
|
|
803
818
|
if (policy === 'queue') {
|
|
804
819
|
const wait = queuedPromise;
|
|
805
|
-
const nextRun = wait.then(() => executeRun(...
|
|
820
|
+
const nextRun = wait.then(() => executeRun(...effectiveArgs)).catch(() => {});
|
|
806
821
|
queuedPromise = nextRun;
|
|
807
822
|
return nextRun;
|
|
808
823
|
}
|
|
809
824
|
|
|
810
|
-
return executeRun(...
|
|
825
|
+
return executeRun(...effectiveArgs);
|
|
811
826
|
}
|
|
812
827
|
|
|
813
828
|
async function executeRun(...args) {
|
|
@@ -834,7 +849,7 @@ function createControllerRuntime(options = {}) {
|
|
|
834
849
|
try {
|
|
835
850
|
const result = await runFn(...args);
|
|
836
851
|
|
|
837
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
852
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
838
853
|
return undefined;
|
|
839
854
|
}
|
|
840
855
|
|
|
@@ -847,7 +862,7 @@ function createControllerRuntime(options = {}) {
|
|
|
847
862
|
updateSignal('stale', false);
|
|
848
863
|
return result;
|
|
849
864
|
} catch (err) {
|
|
850
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
865
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
851
866
|
return undefined;
|
|
852
867
|
}
|
|
853
868
|
if (err && (err.isCanceled || err.name === 'AbortError')) {
|
|
@@ -865,7 +880,11 @@ function createControllerRuntime(options = {}) {
|
|
|
865
880
|
inFlightAbort = null;
|
|
866
881
|
}
|
|
867
882
|
activeRuns = Math.max(0, activeRuns - 1);
|
|
868
|
-
|
|
883
|
+
const shouldClearPending =
|
|
884
|
+
policy === 'parallel'
|
|
885
|
+
? activeRuns === 0
|
|
886
|
+
: runVersion === currentRunVersion || activeRuns === 0;
|
|
887
|
+
if (shouldClearPending) {
|
|
869
888
|
_pending = false;
|
|
870
889
|
_stale = false;
|
|
871
890
|
if (isConnected()) {
|
|
@@ -912,6 +931,9 @@ function createControllerRuntime(options = {}) {
|
|
|
912
931
|
get lastUpdatedAt() {
|
|
913
932
|
return _lastUpdatedAt;
|
|
914
933
|
},
|
|
934
|
+
get lastArgs() {
|
|
935
|
+
return [..._lastArgs];
|
|
936
|
+
},
|
|
915
937
|
isStale: () => isTaskStale(true),
|
|
916
938
|
run,
|
|
917
939
|
cancel,
|
|
@@ -1359,6 +1381,9 @@ function createControllerRuntime(options = {}) {
|
|
|
1359
1381
|
root: defaultStorage,
|
|
1360
1382
|
});
|
|
1361
1383
|
const persistConfig = normalizePersistConfig(reg.definition.persist, controllerName);
|
|
1384
|
+
let persistTimer = null;
|
|
1385
|
+
let lastSavedJson = '';
|
|
1386
|
+
const persistenceConflicts = [];
|
|
1362
1387
|
function resolvePersistDriver(pConfig) {
|
|
1363
1388
|
if (!pConfig) return storageCtx.local;
|
|
1364
1389
|
if (pConfig.isGlobal) {
|
|
@@ -1367,6 +1392,30 @@ function createControllerRuntime(options = {}) {
|
|
|
1367
1392
|
return storageCtx[pConfig.storageType] || storageCtx.local;
|
|
1368
1393
|
}
|
|
1369
1394
|
|
|
1395
|
+
function checkFragmentPersistenceConflict(idOrName, input) {
|
|
1396
|
+
if (!persistConfig || !fragments || typeof fragments.getInstance !== 'function' || !input || typeof input !== 'object') {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const inst =
|
|
1400
|
+
fragments.getInstance(idOrName) ||
|
|
1401
|
+
(typeof fragments.getInstances === 'function'
|
|
1402
|
+
? fragments.getInstances().find((i) => i.name === idOrName)
|
|
1403
|
+
: null);
|
|
1404
|
+
if (inst && inst.persistInput && inst.persistInput.key !== persistConfig.storageKey) {
|
|
1405
|
+
const conflict = {
|
|
1406
|
+
controller: controllerName,
|
|
1407
|
+
controllerStorageKey: persistConfig.storageKey,
|
|
1408
|
+
fragment: inst.instanceId,
|
|
1409
|
+
fragmentStorageKey: inst.persistInput.key,
|
|
1410
|
+
keys: Object.keys(input),
|
|
1411
|
+
};
|
|
1412
|
+
persistenceConflicts.push(conflict);
|
|
1413
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
1414
|
+
debug.recordEvent('persist:conflict', conflict);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1370
1419
|
// 6. Context object passed to `connect(ctx)` and `actions[name](ctx, event, actionEl)`
|
|
1371
1420
|
ctx = Object.freeze({
|
|
1372
1421
|
instanceId,
|
|
@@ -1383,10 +1432,27 @@ function createControllerRuntime(options = {}) {
|
|
|
1383
1432
|
tasks: tasksProxy,
|
|
1384
1433
|
task: createTaskInstance,
|
|
1385
1434
|
storage: storageCtx,
|
|
1435
|
+
get persistenceConflicts() {
|
|
1436
|
+
return [...persistenceConflicts];
|
|
1437
|
+
},
|
|
1386
1438
|
clearPersistedState: () => {
|
|
1439
|
+
if (persistTimer) {
|
|
1440
|
+
clearTimeout(persistTimer);
|
|
1441
|
+
persistTimer = null;
|
|
1442
|
+
}
|
|
1387
1443
|
if (persistConfig) {
|
|
1388
1444
|
const driver = resolvePersistDriver(persistConfig);
|
|
1389
1445
|
driver.remove(persistConfig.storageKey);
|
|
1446
|
+
if (state && initialRawState) {
|
|
1447
|
+
const keysToTrack = persistConfig.keys || Object.keys(initialRawState);
|
|
1448
|
+
const currentPayload = {};
|
|
1449
|
+
for (const k of keysToTrack) {
|
|
1450
|
+
currentPayload[k] = state[k];
|
|
1451
|
+
}
|
|
1452
|
+
lastSavedJson = JSON.stringify(currentPayload);
|
|
1453
|
+
} else {
|
|
1454
|
+
lastSavedJson = '';
|
|
1455
|
+
}
|
|
1390
1456
|
}
|
|
1391
1457
|
},
|
|
1392
1458
|
param: (name, maybeEl) => {
|
|
@@ -1430,7 +1496,12 @@ function createControllerRuntime(options = {}) {
|
|
|
1430
1496
|
return unregister;
|
|
1431
1497
|
},
|
|
1432
1498
|
invalidate: (tags) => (fragments ? fragments.invalidate(tags) : Promise.resolve({ ok: true, refreshed: [] })),
|
|
1499
|
+
retryFailedFragments: () =>
|
|
1500
|
+
fragments && typeof fragments.retryFailed === 'function'
|
|
1501
|
+
? fragments.retryFailed()
|
|
1502
|
+
: Promise.resolve({ refreshed: [], skipped: true }),
|
|
1433
1503
|
refresh: async (idOrName, input, opts) => {
|
|
1504
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1434
1505
|
if (!fragments) {
|
|
1435
1506
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1436
1507
|
if (opts && typeof opts.onSuccess === 'function') await opts.onSuccess(empty);
|
|
@@ -1445,7 +1516,17 @@ function createControllerRuntime(options = {}) {
|
|
|
1445
1516
|
const inst = fragments && fragments.getInstance ? fragments.getInstance(idOrName) : null;
|
|
1446
1517
|
return inst && inst.input ? { ...inst.input } : {};
|
|
1447
1518
|
},
|
|
1519
|
+
get inputs() {
|
|
1520
|
+
if (!fragments || typeof fragments.getInstances !== 'function') return {};
|
|
1521
|
+
const list = fragments.getInstances(idOrName);
|
|
1522
|
+
const map = {};
|
|
1523
|
+
for (const inst of list) {
|
|
1524
|
+
map[inst.instanceId] = inst.input ? { ...inst.input } : {};
|
|
1525
|
+
}
|
|
1526
|
+
return map;
|
|
1527
|
+
},
|
|
1448
1528
|
refresh: (input, opts) => {
|
|
1529
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1449
1530
|
if (!fragments) {
|
|
1450
1531
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1451
1532
|
if (opts && typeof opts.onSuccess === 'function') {
|
|
@@ -1478,14 +1559,34 @@ function createControllerRuntime(options = {}) {
|
|
|
1478
1559
|
);
|
|
1479
1560
|
state = stateManager.state;
|
|
1480
1561
|
|
|
1481
|
-
// Hydrate persisted state if defined
|
|
1562
|
+
// Hydrate persisted state if defined (with schema-compatible type validation against corrupted storage)
|
|
1482
1563
|
if (persistConfig) {
|
|
1483
1564
|
const driver = resolvePersistDriver(persistConfig);
|
|
1484
1565
|
const savedData = driver.get(persistConfig.storageKey, null);
|
|
1485
1566
|
if (savedData && typeof savedData === 'object' && !isArray(savedData)) {
|
|
1486
1567
|
for (const [k, v] of Object.entries(savedData)) {
|
|
1487
1568
|
if (persistConfig.keys && !persistConfig.keys.includes(k)) continue;
|
|
1488
|
-
if (Object.prototype.hasOwnProperty.call(initialRawState, k))
|
|
1569
|
+
if (!Object.prototype.hasOwnProperty.call(initialRawState, k)) continue;
|
|
1570
|
+
const initVal = initialRawState[k];
|
|
1571
|
+
if (initVal !== null && typeof initVal === 'object' && !isArray(initVal)) {
|
|
1572
|
+
if (v !== null && typeof v === 'object' && !isArray(v)) {
|
|
1573
|
+
const merged = { ...initVal };
|
|
1574
|
+
for (const [subKey, subVal] of Object.entries(v)) {
|
|
1575
|
+
if (Object.prototype.hasOwnProperty.call(initVal, subKey)) {
|
|
1576
|
+
if (initVal[subKey] === null || typeof subVal === typeof initVal[subKey]) {
|
|
1577
|
+
merged[subKey] = subVal;
|
|
1578
|
+
}
|
|
1579
|
+
} else {
|
|
1580
|
+
merged[subKey] = subVal;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
state[k] = merged;
|
|
1584
|
+
}
|
|
1585
|
+
} else if (isArray(initVal)) {
|
|
1586
|
+
if (isArray(v)) {
|
|
1587
|
+
state[k] = v;
|
|
1588
|
+
}
|
|
1589
|
+
} else if (initVal === null || initVal === undefined || typeof v === typeof initVal) {
|
|
1489
1590
|
state[k] = v;
|
|
1490
1591
|
}
|
|
1491
1592
|
}
|
|
@@ -1609,7 +1710,17 @@ function createControllerRuntime(options = {}) {
|
|
|
1609
1710
|
isConnected,
|
|
1610
1711
|
isLoadingTargetActive,
|
|
1611
1712
|
});
|
|
1612
|
-
domDisposers.
|
|
1713
|
+
if (domDisposers.length === 0) {
|
|
1714
|
+
domDisposers.push(unbind);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// Prune disconnected forms from scopedForms
|
|
1718
|
+
for (const [fName, fh] of Array.from(scopedForms.entries())) {
|
|
1719
|
+
if (!fh.el || fh.el.isConnected === false || !rootEl.contains(fh.el)) {
|
|
1720
|
+
fh.destroy();
|
|
1721
|
+
scopedForms.delete(fName);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1613
1724
|
|
|
1614
1725
|
// Also auto-initialize any `<form data-w-form>` inside this controller's scope
|
|
1615
1726
|
if (rootEl.tagName === 'FORM' && rootEl.hasAttribute('data-w-form')) {
|
|
@@ -1619,7 +1730,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1619
1730
|
const formsInScope = rootEl.querySelectorAll('form[data-w-form]');
|
|
1620
1731
|
for (let i = 0; i < formsInScope.length; i++) {
|
|
1621
1732
|
if (isTargetInControllerScope(rootEl, formsInScope[i])) {
|
|
1622
|
-
createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1733
|
+
const fh = createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1734
|
+
if (fh && fh.name) {
|
|
1735
|
+
scopedForms.set(fh.name, fh);
|
|
1736
|
+
}
|
|
1623
1737
|
}
|
|
1624
1738
|
}
|
|
1625
1739
|
}
|
|
@@ -1630,8 +1744,6 @@ function createControllerRuntime(options = {}) {
|
|
|
1630
1744
|
// Auto-sync persisted state changes back to storage
|
|
1631
1745
|
if (persistConfig) {
|
|
1632
1746
|
const driver = resolvePersistDriver(persistConfig);
|
|
1633
|
-
let persistTimer = null;
|
|
1634
|
-
let lastSavedJson = '';
|
|
1635
1747
|
|
|
1636
1748
|
const stopPersistEffect = registerEffect(() => {
|
|
1637
1749
|
if (!isConnected()) return;
|
|
@@ -1703,6 +1815,33 @@ function createControllerRuntime(options = {}) {
|
|
|
1703
1815
|
get isConnected() {
|
|
1704
1816
|
return isConnected();
|
|
1705
1817
|
},
|
|
1818
|
+
invokeBeforePatch(meta = {}) {
|
|
1819
|
+
if (!isConnected() || !reg.definition.beforePatch) return;
|
|
1820
|
+
try {
|
|
1821
|
+
const result = reg.definition.beforePatch(ctx, meta);
|
|
1822
|
+
if (result && typeof result.then === 'function') {
|
|
1823
|
+
// beforePatch must be synchronous; warn in dev and do not block morph
|
|
1824
|
+
if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') {
|
|
1825
|
+
console.warn(`[webspresso] Controller "${controllerName}" beforePatch() returned a Promise; beforePatch hooks are synchronous and are not awaited.`);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
} catch (err) {
|
|
1829
|
+
handleControllerError(err, { phase: 'beforePatch' });
|
|
1830
|
+
}
|
|
1831
|
+
},
|
|
1832
|
+
invokeAfterPatch(meta = {}) {
|
|
1833
|
+
if (!isConnected() || !reg.definition.afterPatch) return;
|
|
1834
|
+
try {
|
|
1835
|
+
const res = reg.definition.afterPatch(ctx, meta);
|
|
1836
|
+
if (res && typeof res.then === 'function') {
|
|
1837
|
+
res.catch((err) => {
|
|
1838
|
+
handleControllerError(err, { phase: 'afterPatch' });
|
|
1839
|
+
});
|
|
1840
|
+
}
|
|
1841
|
+
} catch (err) {
|
|
1842
|
+
handleControllerError(err, { phase: 'afterPatch' });
|
|
1843
|
+
}
|
|
1844
|
+
},
|
|
1706
1845
|
destroy() {
|
|
1707
1846
|
if (!connected) return;
|
|
1708
1847
|
connected = false;
|
|
@@ -1816,6 +1955,11 @@ function createControllerRuntime(options = {}) {
|
|
|
1816
1955
|
if (standaloneForm) {
|
|
1817
1956
|
standaloneForm.destroy();
|
|
1818
1957
|
standaloneForms.delete(node);
|
|
1958
|
+
activeStandaloneForms.delete(standaloneForm);
|
|
1959
|
+
} else if (node && node.__wFormHandle) {
|
|
1960
|
+
try {
|
|
1961
|
+
node.__wFormHandle.destroy();
|
|
1962
|
+
} catch (_e) {}
|
|
1819
1963
|
}
|
|
1820
1964
|
}
|
|
1821
1965
|
}
|
|
@@ -1901,6 +2045,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1901
2045
|
services,
|
|
1902
2046
|
});
|
|
1903
2047
|
standaloneForms.set(formEl, handle);
|
|
2048
|
+
activeStandaloneForms.add(handle);
|
|
1904
2049
|
}
|
|
1905
2050
|
}
|
|
1906
2051
|
|
|
@@ -1921,6 +2066,82 @@ function createControllerRuntime(options = {}) {
|
|
|
1921
2066
|
return mounted;
|
|
1922
2067
|
}
|
|
1923
2068
|
|
|
2069
|
+
const patchCycleTracker = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
|
|
2070
|
+
|
|
2071
|
+
function collectAffectedInstances(fragmentEl) {
|
|
2072
|
+
if (!fragmentEl) return [];
|
|
2073
|
+
const affected = [];
|
|
2074
|
+
for (const inst of activeInstances.values()) {
|
|
2075
|
+
if (!inst || !inst.el || !inst.isConnected) continue;
|
|
2076
|
+
if (
|
|
2077
|
+
inst.el === fragmentEl ||
|
|
2078
|
+
(typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el)) ||
|
|
2079
|
+
(typeof inst.el.contains === 'function' && inst.el.contains(fragmentEl))
|
|
2080
|
+
) {
|
|
2081
|
+
affected.push(inst);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
// Sort deterministically: outer ancestors first, inner descendants second
|
|
2085
|
+
affected.sort((a, b) => {
|
|
2086
|
+
if (a.el === b.el) return 0;
|
|
2087
|
+
if (typeof a.el.contains === 'function' && a.el.contains(b.el)) return -1;
|
|
2088
|
+
if (typeof b.el.contains === 'function' && b.el.contains(a.el)) return 1;
|
|
2089
|
+
if (typeof a.el.compareDocumentPosition === 'function') {
|
|
2090
|
+
const pos = a.el.compareDocumentPosition(b.el);
|
|
2091
|
+
if (pos & 4) return -1; // DOCUMENT_POSITION_FOLLOWING
|
|
2092
|
+
if (pos & 2) return 1; // DOCUMENT_POSITION_PRECEDING
|
|
2093
|
+
}
|
|
2094
|
+
return 0;
|
|
2095
|
+
});
|
|
2096
|
+
return affected;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
function notifyBeforePatch(fragmentEl, meta = {}) {
|
|
2100
|
+
const affected = collectAffectedInstances(fragmentEl);
|
|
2101
|
+
for (let i = 0; i < affected.length; i++) {
|
|
2102
|
+
const inst = affected[i];
|
|
2103
|
+
inst.invokeBeforePatch({
|
|
2104
|
+
...meta,
|
|
2105
|
+
isFragmentRoot: inst.el === fragmentEl,
|
|
2106
|
+
isDescendant: inst.el !== fragmentEl && typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el),
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function notifyAfterPatch(fragmentEl, meta = {}) {
|
|
2112
|
+
if (patchCycleTracker && fragmentEl) {
|
|
2113
|
+
const now = Date.now();
|
|
2114
|
+
const prev = patchCycleTracker.get(fragmentEl) || { count: 0, lastAt: 0 };
|
|
2115
|
+
const count = now - prev.lastAt < 100 ? prev.count + 1 : 1;
|
|
2116
|
+
patchCycleTracker.set(fragmentEl, { count, lastAt: now });
|
|
2117
|
+
if (count > 5) {
|
|
2118
|
+
const msg = `Possible patch hook refresh loop detected on fragment "${meta.fragmentName || meta.fragmentId || 'unknown'}" (${count} rapid patch cycles).`;
|
|
2119
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
2120
|
+
debug.recordEvent('patch:loop-warning', {
|
|
2121
|
+
code: 'PATCH_HOOK_LOOP_DETECTED',
|
|
2122
|
+
fragmentId: meta.fragmentId || null,
|
|
2123
|
+
fragmentName: meta.fragmentName || null,
|
|
2124
|
+
count,
|
|
2125
|
+
message: msg,
|
|
2126
|
+
});
|
|
2127
|
+
} else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV !== 'production' && typeof console !== 'undefined') {
|
|
2128
|
+
console.warn(`[webspresso] ${msg}`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
const affected = collectAffectedInstances(fragmentEl);
|
|
2134
|
+
// Reverse order for afterPatch: inner descendants first, outer ancestors second
|
|
2135
|
+
for (let i = affected.length - 1; i >= 0; i--) {
|
|
2136
|
+
const inst = affected[i];
|
|
2137
|
+
inst.invokeAfterPatch({
|
|
2138
|
+
...meta,
|
|
2139
|
+
isFragmentRoot: inst.el === fragmentEl,
|
|
2140
|
+
isDescendant: inst.el !== fragmentEl && typeof fragmentEl.contains === 'function' && fragmentEl.contains(inst.el),
|
|
2141
|
+
});
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1924
2145
|
return {
|
|
1925
2146
|
register,
|
|
1926
2147
|
services,
|
|
@@ -1934,6 +2155,8 @@ function createControllerRuntime(options = {}) {
|
|
|
1934
2155
|
mountSubtree,
|
|
1935
2156
|
syncTree: mountSubtree,
|
|
1936
2157
|
unmountSubtree,
|
|
2158
|
+
notifyBeforePatch,
|
|
2159
|
+
notifyAfterPatch,
|
|
1937
2160
|
mountControllerOnElement,
|
|
1938
2161
|
getInstanceForElement(el, name) {
|
|
1939
2162
|
const map = elementInstances.get(el);
|
|
@@ -1944,10 +2167,19 @@ function createControllerRuntime(options = {}) {
|
|
|
1944
2167
|
getActiveInstances() {
|
|
1945
2168
|
return Array.from(activeInstances.values());
|
|
1946
2169
|
},
|
|
2170
|
+
getStandaloneFormCount() {
|
|
2171
|
+
return activeStandaloneForms.size;
|
|
2172
|
+
},
|
|
1947
2173
|
destroyAll() {
|
|
1948
2174
|
for (const inst of Array.from(activeInstances.values())) {
|
|
1949
2175
|
inst.destroy();
|
|
1950
2176
|
}
|
|
2177
|
+
for (const fh of Array.from(activeStandaloneForms.values())) {
|
|
2178
|
+
try {
|
|
2179
|
+
fh.destroy();
|
|
2180
|
+
} catch (_e) {}
|
|
2181
|
+
}
|
|
2182
|
+
activeStandaloneForms.clear();
|
|
1951
2183
|
},
|
|
1952
2184
|
};
|
|
1953
2185
|
}
|