webspresso 0.0.111 → 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 +119 -30
- package/core/client/controller.js +120 -11
- package/core/client/forms.js +124 -10
- package/core/client/fragments.js +255 -95
- package/core/client/host.js +5 -1
- package/core/client/lite.js +1 -1
- package/core/client/server-fragments.js +43 -1
- package/index.d.ts +12 -1
- package/package.json +1 -1
|
@@ -218,6 +218,22 @@ function createSignalsEngine() {
|
|
|
218
218
|
}
|
|
219
219
|
return out;
|
|
220
220
|
}
|
|
221
|
+
|
|
222
|
+
getSignalCount() {
|
|
223
|
+
return this._nodes.size;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getSubscriberCount() {
|
|
227
|
+
const runners = new Set();
|
|
228
|
+
for (const node of this._nodes.values()) {
|
|
229
|
+
for (const sub of node._subs) {
|
|
230
|
+
if (!sub.disposed) {
|
|
231
|
+
runners.add(sub);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return runners.size;
|
|
236
|
+
}
|
|
221
237
|
}
|
|
222
238
|
|
|
223
239
|
return {
|
|
@@ -760,7 +776,7 @@ function getMorphNodeKey(node) {
|
|
|
760
776
|
* Sync attributes from `fromEl` onto `toEl` while respecting client-owned properties
|
|
761
777
|
* and preserving active focus / form draft states.
|
|
762
778
|
*/
|
|
763
|
-
function syncElementAttributes(toEl, fromEl, ownership) {
|
|
779
|
+
function syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm = false) {
|
|
764
780
|
const fromAttrs = fromEl.attributes;
|
|
765
781
|
const toAttrs = Array.from(toEl.attributes);
|
|
766
782
|
|
|
@@ -787,6 +803,9 @@ function syncElementAttributes(toEl, fromEl, ownership) {
|
|
|
787
803
|
const attr = fromAttrs[i];
|
|
788
804
|
if (ownership.disabled && attr.name === 'disabled') continue;
|
|
789
805
|
if (ownership.show && attr.name === 'hidden') continue;
|
|
806
|
+
if (attr.name === 'action' && toEl.tagName === 'FORM' && (toEl.__wCustomAction || insideDirtyForm)) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
790
809
|
if (toEl.getAttribute(attr.name) !== attr.value) {
|
|
791
810
|
toEl.setAttribute(attr.name, attr.value);
|
|
792
811
|
}
|
|
@@ -847,7 +866,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
847
866
|
const isForm = toEl.tagName === 'FORM';
|
|
848
867
|
const insideDirtyForm = Boolean(ctx.insideDirtyForm || (isForm && isFormDirtyOrPreserved(toEl)));
|
|
849
868
|
|
|
850
|
-
syncElementAttributes(toEl, fromEl, ownership);
|
|
869
|
+
syncElementAttributes(toEl, fromEl, ownership, insideDirtyForm);
|
|
851
870
|
|
|
852
871
|
if (toEl.tagName === 'INPUT' || toEl.tagName === 'TEXTAREA' || toEl.tagName === 'SELECT') {
|
|
853
872
|
syncFormControlValue(toEl, fromEl, ownership, insideDirtyForm);
|
|
@@ -896,7 +915,7 @@ function morphDomTrees(toEl, fromEl, ctx = {}) {
|
|
|
896
915
|
}
|
|
897
916
|
}
|
|
898
917
|
|
|
899
|
-
if (!match) {
|
|
918
|
+
if (!match && !key) {
|
|
900
919
|
// Look ahead for a compatible unkeyed element with the same tagName and controller attribute
|
|
901
920
|
for (let j = unkeyedCursor; j < existingChildren.length; j++) {
|
|
902
921
|
const cand = existingChildren[j];
|
|
@@ -1063,7 +1082,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1063
1082
|
* - Single binding per element/attribute per instance (`WeakMap` guard)
|
|
1064
1083
|
* - All effects & listeners returned in a single cleanup function
|
|
1065
1084
|
*/
|
|
1066
|
-
const boundElementsByInstance = new Map(); // instanceId ->
|
|
1085
|
+
const boundElementsByInstance = new Map(); // instanceId -> Map<Element, Array<() => void>>
|
|
1067
1086
|
|
|
1068
1087
|
function isWithinControllerScope(rootEl, candidateEl) {
|
|
1069
1088
|
if (candidateEl === rootEl) return true;
|
|
@@ -1164,30 +1183,46 @@ function createDatastarAdapter(options = {}) {
|
|
|
1164
1183
|
isLoadingTargetActive,
|
|
1165
1184
|
}) {
|
|
1166
1185
|
if (!boundElementsByInstance.has(instanceId)) {
|
|
1167
|
-
boundElementsByInstance.set(instanceId, new
|
|
1186
|
+
boundElementsByInstance.set(instanceId, new Map());
|
|
1187
|
+
}
|
|
1188
|
+
const elementDisposersMap = boundElementsByInstance.get(instanceId);
|
|
1189
|
+
|
|
1190
|
+
// Prune any previously bound elements that were detached/replaced by fragment patches
|
|
1191
|
+
for (const [prevEl, prevDisposers] of Array.from(elementDisposersMap.entries())) {
|
|
1192
|
+
const detached =
|
|
1193
|
+
!prevEl ||
|
|
1194
|
+
prevEl.isConnected === false ||
|
|
1195
|
+
(rootEl && typeof rootEl.contains === 'function' && !rootEl.contains(prevEl));
|
|
1196
|
+
if (detached) {
|
|
1197
|
+
for (let d = 0; d < prevDisposers.length; d++) {
|
|
1198
|
+
try {
|
|
1199
|
+
prevDisposers[d]();
|
|
1200
|
+
} catch (_e) {}
|
|
1201
|
+
}
|
|
1202
|
+
elementDisposersMap.delete(prevEl);
|
|
1203
|
+
}
|
|
1168
1204
|
}
|
|
1169
|
-
const boundSet = boundElementsByInstance.get(instanceId);
|
|
1170
|
-
const disposers = [];
|
|
1171
1205
|
|
|
1172
1206
|
const elements = collectScopedElements(rootEl);
|
|
1173
1207
|
|
|
1174
1208
|
for (let i = 0; i < elements.length; i++) {
|
|
1175
1209
|
const el = elements[i];
|
|
1176
|
-
if (
|
|
1177
|
-
|
|
1210
|
+
if (elementDisposersMap.has(el)) continue;
|
|
1211
|
+
const elDisposers = [];
|
|
1212
|
+
elementDisposersMap.set(el, elDisposers);
|
|
1178
1213
|
|
|
1179
1214
|
// 1. data-w-text
|
|
1180
1215
|
if (el.hasAttribute('data-w-text')) {
|
|
1181
1216
|
const expr = el.getAttribute('data-w-text');
|
|
1182
1217
|
const stop = engine.effect(() => {
|
|
1183
|
-
if (!isConnected()) return;
|
|
1218
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1184
1219
|
const val = evaluateStateExpression(state, expr);
|
|
1185
1220
|
const text = val === null || val === undefined ? '' : String(val);
|
|
1186
1221
|
if (el.textContent !== text) {
|
|
1187
1222
|
el.textContent = text;
|
|
1188
1223
|
}
|
|
1189
1224
|
});
|
|
1190
|
-
|
|
1225
|
+
elDisposers.push(stop);
|
|
1191
1226
|
}
|
|
1192
1227
|
|
|
1193
1228
|
// 2. data-w-show
|
|
@@ -1195,10 +1230,10 @@ function createDatastarAdapter(options = {}) {
|
|
|
1195
1230
|
const expr = el.getAttribute('data-w-show');
|
|
1196
1231
|
const initialDisplay = el.style && el.style.display && el.style.display !== 'none' ? el.style.display : '';
|
|
1197
1232
|
const stop = engine.effect(() => {
|
|
1198
|
-
if (!isConnected()) return;
|
|
1233
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1199
1234
|
toggleElementVisibility(el, Boolean(evaluateStateExpression(state, expr)), initialDisplay);
|
|
1200
1235
|
});
|
|
1201
|
-
|
|
1236
|
+
elDisposers.push(stop);
|
|
1202
1237
|
}
|
|
1203
1238
|
|
|
1204
1239
|
// 2b. data-w-loading & data-w-loading-hide (declarative action/task busy indicators)
|
|
@@ -1211,7 +1246,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1211
1246
|
.split(/\s+/)
|
|
1212
1247
|
.filter(Boolean);
|
|
1213
1248
|
const stop = engine.effect(() => {
|
|
1214
|
-
if (!isConnected()) return;
|
|
1249
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1215
1250
|
const active =
|
|
1216
1251
|
typeof isLoadingTargetActive === 'function'
|
|
1217
1252
|
? Boolean(isLoadingTargetActive(rawSpec))
|
|
@@ -1224,14 +1259,14 @@ function createDatastarAdapter(options = {}) {
|
|
|
1224
1259
|
}
|
|
1225
1260
|
}
|
|
1226
1261
|
});
|
|
1227
|
-
|
|
1262
|
+
elDisposers.push(stop);
|
|
1228
1263
|
}
|
|
1229
1264
|
|
|
1230
1265
|
// 3. data-w-disabled
|
|
1231
1266
|
if (el.hasAttribute('data-w-disabled')) {
|
|
1232
1267
|
const expr = el.getAttribute('data-w-disabled');
|
|
1233
1268
|
const stop = engine.effect(() => {
|
|
1234
|
-
if (!isConnected()) return;
|
|
1269
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1235
1270
|
const disabled = Boolean(evaluateStateExpression(state, expr));
|
|
1236
1271
|
if ('disabled' in el) {
|
|
1237
1272
|
el.disabled = disabled;
|
|
@@ -1244,7 +1279,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1244
1279
|
el.removeAttribute('aria-disabled');
|
|
1245
1280
|
}
|
|
1246
1281
|
});
|
|
1247
|
-
|
|
1282
|
+
elDisposers.push(stop);
|
|
1248
1283
|
}
|
|
1249
1284
|
|
|
1250
1285
|
// 4. data-w-model (two-way binding between input/select/textarea and controller state)
|
|
@@ -1286,7 +1321,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1286
1321
|
};
|
|
1287
1322
|
el.addEventListener('compositionstart', onCompositionStart);
|
|
1288
1323
|
el.addEventListener('compositionend', onCompositionEnd);
|
|
1289
|
-
|
|
1324
|
+
elDisposers.push(() => {
|
|
1290
1325
|
try {
|
|
1291
1326
|
el.removeEventListener('compositionstart', onCompositionStart);
|
|
1292
1327
|
el.removeEventListener('compositionend', onCompositionEnd);
|
|
@@ -1295,7 +1330,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1295
1330
|
|
|
1296
1331
|
// State -> DOM effect (synchronizes changes in state to the DOM control)
|
|
1297
1332
|
const stopModelEffect = engine.effect(() => {
|
|
1298
|
-
if (!isConnected() || isComposing) return;
|
|
1333
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1299
1334
|
const currentVal = getByPath(state, modelPath);
|
|
1300
1335
|
|
|
1301
1336
|
if (type === 'checkbox') {
|
|
@@ -1319,7 +1354,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1319
1354
|
}
|
|
1320
1355
|
}
|
|
1321
1356
|
});
|
|
1322
|
-
|
|
1357
|
+
elDisposers.push(stopModelEffect);
|
|
1323
1358
|
|
|
1324
1359
|
// DOM -> State listener (synchronizes user interactions to state)
|
|
1325
1360
|
const evtType =
|
|
@@ -1328,7 +1363,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1328
1363
|
: 'input';
|
|
1329
1364
|
|
|
1330
1365
|
const onModelInput = () => {
|
|
1331
|
-
if (!isConnected() || isComposing) return;
|
|
1366
|
+
if (!isConnected() || el.isConnected === false || isComposing) return;
|
|
1332
1367
|
|
|
1333
1368
|
if (type === 'checkbox') {
|
|
1334
1369
|
const cur = getByPath(state, modelPath);
|
|
@@ -1360,7 +1395,7 @@ function createDatastarAdapter(options = {}) {
|
|
|
1360
1395
|
};
|
|
1361
1396
|
|
|
1362
1397
|
el.addEventListener(evtType, onModelInput);
|
|
1363
|
-
|
|
1398
|
+
elDisposers.push(() => el.removeEventListener(evtType, onModelInput));
|
|
1364
1399
|
}
|
|
1365
1400
|
}
|
|
1366
1401
|
|
|
@@ -1368,23 +1403,30 @@ function createDatastarAdapter(options = {}) {
|
|
|
1368
1403
|
const eventBindings = extractEventBindings(el);
|
|
1369
1404
|
for (const binding of eventBindings) {
|
|
1370
1405
|
const listener = (event) => {
|
|
1371
|
-
if (!isConnected()) return;
|
|
1406
|
+
if (!isConnected() || el.isConnected === false) return;
|
|
1372
1407
|
if (binding.modifiers.has('prevent')) event.preventDefault();
|
|
1373
1408
|
if (binding.modifiers.has('stop')) event.stopPropagation();
|
|
1374
1409
|
invokeAction(binding.actionName, event, el);
|
|
1375
1410
|
};
|
|
1376
1411
|
el.addEventListener(binding.eventName, listener);
|
|
1377
|
-
|
|
1412
|
+
elDisposers.push(() => el.removeEventListener(binding.eventName, listener));
|
|
1378
1413
|
}
|
|
1379
1414
|
}
|
|
1380
1415
|
|
|
1381
1416
|
return function unbindInstanceDom() {
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1417
|
+
const map = boundElementsByInstance.get(instanceId);
|
|
1418
|
+
if (map) {
|
|
1419
|
+
for (const elDisposers of map.values()) {
|
|
1420
|
+
for (let i = 0; i < elDisposers.length; i++) {
|
|
1421
|
+
try {
|
|
1422
|
+
elDisposers[i]();
|
|
1423
|
+
} catch (_err) {
|
|
1424
|
+
// Ignore individual unbind errors
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1387
1427
|
}
|
|
1428
|
+
map.clear();
|
|
1429
|
+
boundElementsByInstance.delete(instanceId);
|
|
1388
1430
|
}
|
|
1389
1431
|
};
|
|
1390
1432
|
}
|
|
@@ -1394,14 +1436,53 @@ function createDatastarAdapter(options = {}) {
|
|
|
1394
1436
|
*/
|
|
1395
1437
|
function patchFragmentElement(targetEl, htmlString, lifecycleCallbacks = {}) {
|
|
1396
1438
|
if (!targetEl || typeof htmlString !== 'string') return targetEl;
|
|
1397
|
-
const
|
|
1439
|
+
const doc = targetEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
1440
|
+
const parsed = parseHtmlFragment(htmlString, doc);
|
|
1398
1441
|
if (!parsed) return targetEl;
|
|
1399
1442
|
|
|
1443
|
+
// Capture active focus & text selection if inside targetEl before morphing
|
|
1444
|
+
let focusedEl = null;
|
|
1445
|
+
let selStart = null;
|
|
1446
|
+
let selEnd = null;
|
|
1447
|
+
if (
|
|
1448
|
+
doc &&
|
|
1449
|
+
doc.activeElement &&
|
|
1450
|
+
typeof targetEl.contains === 'function' &&
|
|
1451
|
+
targetEl.contains(doc.activeElement)
|
|
1452
|
+
) {
|
|
1453
|
+
focusedEl = doc.activeElement;
|
|
1454
|
+
try {
|
|
1455
|
+
if (typeof focusedEl.selectionStart === 'number') selStart = focusedEl.selectionStart;
|
|
1456
|
+
if (typeof focusedEl.selectionEnd === 'number') selEnd = focusedEl.selectionEnd;
|
|
1457
|
+
} catch (_e) {}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1400
1460
|
const morphed = morphDomTrees(targetEl, parsed, {
|
|
1401
1461
|
onBeforeNodeRemove: lifecycleCallbacks.onBeforeNodeRemove,
|
|
1402
1462
|
onNodeAdded: lifecycleCallbacks.onNodeAdded,
|
|
1403
1463
|
});
|
|
1404
1464
|
|
|
1465
|
+
// Restore focus & selection range if the focused element was preserved inside targetEl
|
|
1466
|
+
if (
|
|
1467
|
+
focusedEl &&
|
|
1468
|
+
focusedEl.isConnected !== false &&
|
|
1469
|
+
typeof targetEl.contains === 'function' &&
|
|
1470
|
+
targetEl.contains(focusedEl)
|
|
1471
|
+
) {
|
|
1472
|
+
try {
|
|
1473
|
+
if (doc && doc.activeElement !== focusedEl && typeof focusedEl.focus === 'function') {
|
|
1474
|
+
focusedEl.focus();
|
|
1475
|
+
}
|
|
1476
|
+
if (
|
|
1477
|
+
typeof selStart === 'number' &&
|
|
1478
|
+
typeof selEnd === 'number' &&
|
|
1479
|
+
typeof focusedEl.setSelectionRange === 'function'
|
|
1480
|
+
) {
|
|
1481
|
+
focusedEl.setSelectionRange(selStart, selEnd);
|
|
1482
|
+
}
|
|
1483
|
+
} catch (_e) {}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1405
1486
|
// If external @starfederation/datastar is active, notify apply() on updated subtree
|
|
1406
1487
|
if (externalDatastar && typeof externalDatastar.apply === 'function') {
|
|
1407
1488
|
try {
|
|
@@ -1432,6 +1513,14 @@ function createDatastarAdapter(options = {}) {
|
|
|
1432
1513
|
setByPath,
|
|
1433
1514
|
isWithinControllerScope,
|
|
1434
1515
|
collectScopedElements,
|
|
1516
|
+
getBoundInstanceCount: () => boundElementsByInstance.size,
|
|
1517
|
+
getBoundElementCount: () => {
|
|
1518
|
+
let total = 0;
|
|
1519
|
+
for (const m of boundElementsByInstance.values()) {
|
|
1520
|
+
total += m.size;
|
|
1521
|
+
}
|
|
1522
|
+
return total;
|
|
1523
|
+
},
|
|
1435
1524
|
};
|
|
1436
1525
|
}
|
|
1437
1526
|
|
|
@@ -495,6 +495,7 @@ function createControllerRuntime(options = {}) {
|
|
|
495
495
|
const activeInstances = new Map();
|
|
496
496
|
// Standalone forms (forms without a controller wrapper)
|
|
497
497
|
const standaloneForms = new WeakMap();
|
|
498
|
+
const activeStandaloneForms = new Set();
|
|
498
499
|
|
|
499
500
|
/**
|
|
500
501
|
* Register a controller definition with collision checking.
|
|
@@ -769,6 +770,7 @@ function createControllerRuntime(options = {}) {
|
|
|
769
770
|
let _data = null;
|
|
770
771
|
let _error = null;
|
|
771
772
|
let _stale = false;
|
|
773
|
+
let _lastArgs = [];
|
|
772
774
|
|
|
773
775
|
function isTaskStale(mode = true) {
|
|
774
776
|
if (!mode) return false;
|
|
@@ -787,6 +789,10 @@ function createControllerRuntime(options = {}) {
|
|
|
787
789
|
|
|
788
790
|
async function run(...args) {
|
|
789
791
|
if (!isConnected()) return undefined;
|
|
792
|
+
if (args.length > 0) {
|
|
793
|
+
_lastArgs = [...args];
|
|
794
|
+
}
|
|
795
|
+
const effectiveArgs = args.length > 0 ? args : _lastArgs;
|
|
790
796
|
|
|
791
797
|
if (policy === 'drop' && activeRuns > 0) {
|
|
792
798
|
if (debug && typeof debug.trackDiscardedResponse === 'function') {
|
|
@@ -802,12 +808,12 @@ function createControllerRuntime(options = {}) {
|
|
|
802
808
|
|
|
803
809
|
if (policy === 'queue') {
|
|
804
810
|
const wait = queuedPromise;
|
|
805
|
-
const nextRun = wait.then(() => executeRun(...
|
|
811
|
+
const nextRun = wait.then(() => executeRun(...effectiveArgs)).catch(() => {});
|
|
806
812
|
queuedPromise = nextRun;
|
|
807
813
|
return nextRun;
|
|
808
814
|
}
|
|
809
815
|
|
|
810
|
-
return executeRun(...
|
|
816
|
+
return executeRun(...effectiveArgs);
|
|
811
817
|
}
|
|
812
818
|
|
|
813
819
|
async function executeRun(...args) {
|
|
@@ -834,7 +840,7 @@ function createControllerRuntime(options = {}) {
|
|
|
834
840
|
try {
|
|
835
841
|
const result = await runFn(...args);
|
|
836
842
|
|
|
837
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
843
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
838
844
|
return undefined;
|
|
839
845
|
}
|
|
840
846
|
|
|
@@ -847,7 +853,7 @@ function createControllerRuntime(options = {}) {
|
|
|
847
853
|
updateSignal('stale', false);
|
|
848
854
|
return result;
|
|
849
855
|
} catch (err) {
|
|
850
|
-
if (!isConnected() || runVersion !== currentRunVersion) {
|
|
856
|
+
if (!isConnected() || (policy !== 'parallel' && runVersion !== currentRunVersion)) {
|
|
851
857
|
return undefined;
|
|
852
858
|
}
|
|
853
859
|
if (err && (err.isCanceled || err.name === 'AbortError')) {
|
|
@@ -865,7 +871,11 @@ function createControllerRuntime(options = {}) {
|
|
|
865
871
|
inFlightAbort = null;
|
|
866
872
|
}
|
|
867
873
|
activeRuns = Math.max(0, activeRuns - 1);
|
|
868
|
-
|
|
874
|
+
const shouldClearPending =
|
|
875
|
+
policy === 'parallel'
|
|
876
|
+
? activeRuns === 0
|
|
877
|
+
: runVersion === currentRunVersion || activeRuns === 0;
|
|
878
|
+
if (shouldClearPending) {
|
|
869
879
|
_pending = false;
|
|
870
880
|
_stale = false;
|
|
871
881
|
if (isConnected()) {
|
|
@@ -912,6 +922,9 @@ function createControllerRuntime(options = {}) {
|
|
|
912
922
|
get lastUpdatedAt() {
|
|
913
923
|
return _lastUpdatedAt;
|
|
914
924
|
},
|
|
925
|
+
get lastArgs() {
|
|
926
|
+
return [..._lastArgs];
|
|
927
|
+
},
|
|
915
928
|
isStale: () => isTaskStale(true),
|
|
916
929
|
run,
|
|
917
930
|
cancel,
|
|
@@ -1359,6 +1372,9 @@ function createControllerRuntime(options = {}) {
|
|
|
1359
1372
|
root: defaultStorage,
|
|
1360
1373
|
});
|
|
1361
1374
|
const persistConfig = normalizePersistConfig(reg.definition.persist, controllerName);
|
|
1375
|
+
let persistTimer = null;
|
|
1376
|
+
let lastSavedJson = '';
|
|
1377
|
+
const persistenceConflicts = [];
|
|
1362
1378
|
function resolvePersistDriver(pConfig) {
|
|
1363
1379
|
if (!pConfig) return storageCtx.local;
|
|
1364
1380
|
if (pConfig.isGlobal) {
|
|
@@ -1367,6 +1383,30 @@ function createControllerRuntime(options = {}) {
|
|
|
1367
1383
|
return storageCtx[pConfig.storageType] || storageCtx.local;
|
|
1368
1384
|
}
|
|
1369
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
|
+
|
|
1370
1410
|
// 6. Context object passed to `connect(ctx)` and `actions[name](ctx, event, actionEl)`
|
|
1371
1411
|
ctx = Object.freeze({
|
|
1372
1412
|
instanceId,
|
|
@@ -1383,10 +1423,27 @@ function createControllerRuntime(options = {}) {
|
|
|
1383
1423
|
tasks: tasksProxy,
|
|
1384
1424
|
task: createTaskInstance,
|
|
1385
1425
|
storage: storageCtx,
|
|
1426
|
+
get persistenceConflicts() {
|
|
1427
|
+
return [...persistenceConflicts];
|
|
1428
|
+
},
|
|
1386
1429
|
clearPersistedState: () => {
|
|
1430
|
+
if (persistTimer) {
|
|
1431
|
+
clearTimeout(persistTimer);
|
|
1432
|
+
persistTimer = null;
|
|
1433
|
+
}
|
|
1387
1434
|
if (persistConfig) {
|
|
1388
1435
|
const driver = resolvePersistDriver(persistConfig);
|
|
1389
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
|
+
}
|
|
1390
1447
|
}
|
|
1391
1448
|
},
|
|
1392
1449
|
param: (name, maybeEl) => {
|
|
@@ -1430,7 +1487,12 @@ function createControllerRuntime(options = {}) {
|
|
|
1430
1487
|
return unregister;
|
|
1431
1488
|
},
|
|
1432
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 }),
|
|
1433
1494
|
refresh: async (idOrName, input, opts) => {
|
|
1495
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1434
1496
|
if (!fragments) {
|
|
1435
1497
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1436
1498
|
if (opts && typeof opts.onSuccess === 'function') await opts.onSuccess(empty);
|
|
@@ -1446,6 +1508,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1446
1508
|
return inst && inst.input ? { ...inst.input } : {};
|
|
1447
1509
|
},
|
|
1448
1510
|
refresh: (input, opts) => {
|
|
1511
|
+
checkFragmentPersistenceConflict(idOrName, input);
|
|
1449
1512
|
if (!fragments) {
|
|
1450
1513
|
const empty = { ok: true, refreshed: [], skipped: true };
|
|
1451
1514
|
if (opts && typeof opts.onSuccess === 'function') {
|
|
@@ -1478,14 +1541,34 @@ function createControllerRuntime(options = {}) {
|
|
|
1478
1541
|
);
|
|
1479
1542
|
state = stateManager.state;
|
|
1480
1543
|
|
|
1481
|
-
// Hydrate persisted state if defined
|
|
1544
|
+
// Hydrate persisted state if defined (with schema-compatible type validation against corrupted storage)
|
|
1482
1545
|
if (persistConfig) {
|
|
1483
1546
|
const driver = resolvePersistDriver(persistConfig);
|
|
1484
1547
|
const savedData = driver.get(persistConfig.storageKey, null);
|
|
1485
1548
|
if (savedData && typeof savedData === 'object' && !isArray(savedData)) {
|
|
1486
1549
|
for (const [k, v] of Object.entries(savedData)) {
|
|
1487
1550
|
if (persistConfig.keys && !persistConfig.keys.includes(k)) continue;
|
|
1488
|
-
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) {
|
|
1489
1572
|
state[k] = v;
|
|
1490
1573
|
}
|
|
1491
1574
|
}
|
|
@@ -1609,7 +1692,17 @@ function createControllerRuntime(options = {}) {
|
|
|
1609
1692
|
isConnected,
|
|
1610
1693
|
isLoadingTargetActive,
|
|
1611
1694
|
});
|
|
1612
|
-
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
|
+
}
|
|
1613
1706
|
|
|
1614
1707
|
// Also auto-initialize any `<form data-w-form>` inside this controller's scope
|
|
1615
1708
|
if (rootEl.tagName === 'FORM' && rootEl.hasAttribute('data-w-form')) {
|
|
@@ -1619,7 +1712,10 @@ function createControllerRuntime(options = {}) {
|
|
|
1619
1712
|
const formsInScope = rootEl.querySelectorAll('form[data-w-form]');
|
|
1620
1713
|
for (let i = 0; i < formsInScope.length; i++) {
|
|
1621
1714
|
if (isTargetInControllerScope(rootEl, formsInScope[i])) {
|
|
1622
|
-
createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1715
|
+
const fh = createFormHandle(formsInScope[i], { api: scopedApi, datastar, debug, services });
|
|
1716
|
+
if (fh && fh.name) {
|
|
1717
|
+
scopedForms.set(fh.name, fh);
|
|
1718
|
+
}
|
|
1623
1719
|
}
|
|
1624
1720
|
}
|
|
1625
1721
|
}
|
|
@@ -1630,8 +1726,6 @@ function createControllerRuntime(options = {}) {
|
|
|
1630
1726
|
// Auto-sync persisted state changes back to storage
|
|
1631
1727
|
if (persistConfig) {
|
|
1632
1728
|
const driver = resolvePersistDriver(persistConfig);
|
|
1633
|
-
let persistTimer = null;
|
|
1634
|
-
let lastSavedJson = '';
|
|
1635
1729
|
|
|
1636
1730
|
const stopPersistEffect = registerEffect(() => {
|
|
1637
1731
|
if (!isConnected()) return;
|
|
@@ -1816,6 +1910,11 @@ function createControllerRuntime(options = {}) {
|
|
|
1816
1910
|
if (standaloneForm) {
|
|
1817
1911
|
standaloneForm.destroy();
|
|
1818
1912
|
standaloneForms.delete(node);
|
|
1913
|
+
activeStandaloneForms.delete(standaloneForm);
|
|
1914
|
+
} else if (node && node.__wFormHandle) {
|
|
1915
|
+
try {
|
|
1916
|
+
node.__wFormHandle.destroy();
|
|
1917
|
+
} catch (_e) {}
|
|
1819
1918
|
}
|
|
1820
1919
|
}
|
|
1821
1920
|
}
|
|
@@ -1901,6 +2000,7 @@ function createControllerRuntime(options = {}) {
|
|
|
1901
2000
|
services,
|
|
1902
2001
|
});
|
|
1903
2002
|
standaloneForms.set(formEl, handle);
|
|
2003
|
+
activeStandaloneForms.add(handle);
|
|
1904
2004
|
}
|
|
1905
2005
|
}
|
|
1906
2006
|
|
|
@@ -1944,10 +2044,19 @@ function createControllerRuntime(options = {}) {
|
|
|
1944
2044
|
getActiveInstances() {
|
|
1945
2045
|
return Array.from(activeInstances.values());
|
|
1946
2046
|
},
|
|
2047
|
+
getStandaloneFormCount() {
|
|
2048
|
+
return activeStandaloneForms.size;
|
|
2049
|
+
},
|
|
1947
2050
|
destroyAll() {
|
|
1948
2051
|
for (const inst of Array.from(activeInstances.values())) {
|
|
1949
2052
|
inst.destroy();
|
|
1950
2053
|
}
|
|
2054
|
+
for (const fh of Array.from(activeStandaloneForms.values())) {
|
|
2055
|
+
try {
|
|
2056
|
+
fh.destroy();
|
|
2057
|
+
} catch (_e) {}
|
|
2058
|
+
}
|
|
2059
|
+
activeStandaloneForms.clear();
|
|
1951
2060
|
},
|
|
1952
2061
|
};
|
|
1953
2062
|
}
|