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
package/core/client/fragments.js
CHANGED
|
@@ -65,6 +65,16 @@ function parseFragmentInputAttr(raw) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function isStrictAncestor(parentEl, childEl) {
|
|
69
|
+
return Boolean(
|
|
70
|
+
parentEl &&
|
|
71
|
+
childEl &&
|
|
72
|
+
parentEl !== childEl &&
|
|
73
|
+
typeof parentEl.contains === 'function' &&
|
|
74
|
+
parentEl.contains(childEl)
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
68
78
|
/**
|
|
69
79
|
* Filter a list of fragment records so that if fragment B's DOM element is inside
|
|
70
80
|
* fragment A's DOM subtree, B is omitted from the batch because refreshing A will
|
|
@@ -79,7 +89,7 @@ function deduplicateParentChildFragments(candidates) {
|
|
|
79
89
|
if (!candidate.el) return true;
|
|
80
90
|
for (const other of candidates) {
|
|
81
91
|
if (other === candidate || !other.el) continue;
|
|
82
|
-
if (other.el
|
|
92
|
+
if (isStrictAncestor(other.el, candidate.el)) {
|
|
83
93
|
return false;
|
|
84
94
|
}
|
|
85
95
|
}
|
|
@@ -140,7 +150,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
140
150
|
|
|
141
151
|
// Automatic refetch (focus / reconnect / mount / interval) state
|
|
142
152
|
const pendingAutoInstanceIds = new Set();
|
|
153
|
+
const pendingAutoReasons = new Set();
|
|
143
154
|
let autoBatchScheduled = false;
|
|
155
|
+
let autoBatchTimer = null;
|
|
144
156
|
let focusDebounceTimer = null;
|
|
145
157
|
let isDisposed = false;
|
|
146
158
|
let lifecycleListenersAttached = false;
|
|
@@ -195,76 +207,95 @@ function createFragmentCoordinator(options = {}) {
|
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
210
|
+
async function flushAutoRefresh() {
|
|
211
|
+
autoBatchScheduled = false;
|
|
212
|
+
if (autoBatchTimer) {
|
|
213
|
+
clearTimeout(autoBatchTimer);
|
|
214
|
+
autoBatchTimer = null;
|
|
202
215
|
}
|
|
203
|
-
if (
|
|
204
|
-
|
|
216
|
+
if (isDisposed || pendingAutoInstanceIds.size === 0) return;
|
|
217
|
+
const idsToRefresh = Array.from(pendingAutoInstanceIds);
|
|
218
|
+
const reasons = Array.from(pendingAutoReasons);
|
|
219
|
+
pendingAutoInstanceIds.clear();
|
|
220
|
+
pendingAutoReasons.clear();
|
|
221
|
+
|
|
222
|
+
// Skip background auto-refresh if a mutation invalidation batch is currently in flight
|
|
223
|
+
if (inFlightBatchCount > 0) return;
|
|
224
|
+
|
|
225
|
+
const candidates = [];
|
|
226
|
+
for (const id of idsToRefresh) {
|
|
227
|
+
const inst = instances.get(id);
|
|
228
|
+
if (
|
|
229
|
+
inst &&
|
|
230
|
+
inst.el &&
|
|
231
|
+
inst.el.isConnected !== false &&
|
|
232
|
+
!inst.abortController &&
|
|
233
|
+
!(inst.inFlightCount > 0)
|
|
234
|
+
) {
|
|
235
|
+
candidates.push(inst);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (candidates.length === 0) return;
|
|
205
239
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
240
|
+
const deduped = deduplicateParentChildFragments(candidates);
|
|
241
|
+
const targets = deduped.map((inst) => {
|
|
242
|
+
inst.version += 1;
|
|
243
|
+
return {
|
|
244
|
+
instanceId: inst.instanceId,
|
|
245
|
+
name: inst.name,
|
|
246
|
+
pagePath: inst.pagePath,
|
|
247
|
+
input: { ...inst.input },
|
|
248
|
+
reqVersion: inst.version,
|
|
249
|
+
};
|
|
250
|
+
});
|
|
211
251
|
|
|
212
|
-
|
|
213
|
-
|
|
252
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
253
|
+
debug.recordEvent('fragment:auto-refetch', {
|
|
254
|
+
reason: reasons.length === 1 ? reasons[0] : reasons.join('+'),
|
|
255
|
+
reasons,
|
|
256
|
+
fragments: targets.map((t) => t.instanceId),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
214
259
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
if (candidates.length === 0) return;
|
|
260
|
+
try {
|
|
261
|
+
await executeBatchRefresh(targets);
|
|
262
|
+
} catch (_err) {
|
|
263
|
+
// Background auto-refetch errors are non-fatal
|
|
264
|
+
}
|
|
265
|
+
}
|
|
223
266
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
input: { ...inst.input },
|
|
232
|
-
reqVersion: inst.version,
|
|
233
|
-
};
|
|
234
|
-
});
|
|
267
|
+
function scheduleAutoRefresh(instanceIds, reason = 'auto') {
|
|
268
|
+
if (isDisposed || !isArray(instanceIds) || instanceIds.length === 0) return;
|
|
269
|
+
for (const id of instanceIds) {
|
|
270
|
+
if (id) pendingAutoInstanceIds.add(id);
|
|
271
|
+
}
|
|
272
|
+
if (reason) pendingAutoReasons.add(reason);
|
|
273
|
+
if (pendingAutoInstanceIds.size === 0) return;
|
|
235
274
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
});
|
|
275
|
+
if (reason === 'mount') {
|
|
276
|
+
if (!autoBatchScheduled) {
|
|
277
|
+
autoBatchScheduled = true;
|
|
278
|
+
Promise.resolve().then(flushAutoRefresh);
|
|
241
279
|
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
242
282
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
} catch (_err) {
|
|
246
|
-
// Background auto-refetch errors are non-fatal
|
|
247
|
-
}
|
|
248
|
-
});
|
|
283
|
+
if (autoBatchTimer || autoBatchScheduled) return;
|
|
284
|
+
autoBatchTimer = setTimeout(flushAutoRefresh, 5);
|
|
249
285
|
}
|
|
250
286
|
|
|
251
287
|
function triggerFocusRefetch() {
|
|
252
288
|
if (isDisposed || isDocumentHidden()) return;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
const matchedIds = [];
|
|
259
|
-
for (const inst of instances.values()) {
|
|
260
|
-
if (isInstanceStale(inst, inst.refetchOnWindowFocus)) {
|
|
261
|
-
matchedIds.push(inst.instanceId);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (matchedIds.length > 0) {
|
|
265
|
-
scheduleAutoRefresh(matchedIds, 'window-focus');
|
|
289
|
+
scanDomInstances();
|
|
290
|
+
const matchedIds = [];
|
|
291
|
+
for (const inst of instances.values()) {
|
|
292
|
+
if (isInstanceStale(inst, inst.refetchOnWindowFocus)) {
|
|
293
|
+
matchedIds.push(inst.instanceId);
|
|
266
294
|
}
|
|
267
|
-
}
|
|
295
|
+
}
|
|
296
|
+
if (matchedIds.length > 0) {
|
|
297
|
+
scheduleAutoRefresh(matchedIds, 'window-focus');
|
|
298
|
+
}
|
|
268
299
|
}
|
|
269
300
|
|
|
270
301
|
function triggerReconnectRefetch() {
|
|
@@ -394,12 +425,21 @@ function createFragmentCoordinator(options = {}) {
|
|
|
394
425
|
`frag_input:${name}`;
|
|
395
426
|
|
|
396
427
|
let effectiveInput = input;
|
|
397
|
-
|
|
428
|
+
let pendingRestoredInput = null;
|
|
429
|
+
if (!existing && isInputPersistEnabled) {
|
|
398
430
|
const store = inputStorageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
431
|
+
let stored = store.get(inputStorageKey, null);
|
|
432
|
+
if (
|
|
433
|
+
(!stored || typeof stored !== 'object' || isArray(stored)) &&
|
|
434
|
+
inputStorageKey !== `frag_input:${instanceId}`
|
|
435
|
+
) {
|
|
436
|
+
stored = store.get(`frag_input:${instanceId}`, null);
|
|
437
|
+
}
|
|
438
|
+
if (stored && typeof stored === 'object' && !isArray(stored)) {
|
|
439
|
+
if (rawInput === null || Object.keys(effectiveInput).length === 0) {
|
|
440
|
+
effectiveInput = { ...stored };
|
|
441
|
+
} else if (JSON.stringify(stored) !== JSON.stringify(effectiveInput)) {
|
|
442
|
+
pendingRestoredInput = { ...stored };
|
|
403
443
|
}
|
|
404
444
|
}
|
|
405
445
|
}
|
|
@@ -434,8 +474,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
434
474
|
existing.pagePath = pagePath;
|
|
435
475
|
existing.el = el;
|
|
436
476
|
existing.dependsOn = dependsOn;
|
|
437
|
-
if (rawInput !== null
|
|
438
|
-
existing.input =
|
|
477
|
+
if (rawInput !== null && rawInput !== existing._rawInput) {
|
|
478
|
+
existing.input = input;
|
|
439
479
|
}
|
|
440
480
|
existing.persistInput = isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null;
|
|
441
481
|
existing.staleTime = staleTime;
|
|
@@ -455,10 +495,12 @@ function createFragmentCoordinator(options = {}) {
|
|
|
455
495
|
name,
|
|
456
496
|
pagePath,
|
|
457
497
|
input: effectiveInput,
|
|
498
|
+
restoredInput: pendingRestoredInput,
|
|
458
499
|
dependsOn,
|
|
459
500
|
version: 0,
|
|
460
501
|
el,
|
|
461
502
|
abortController: null,
|
|
503
|
+
inFlightCount: 0,
|
|
462
504
|
persistInput: isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null,
|
|
463
505
|
lastUpdatedAt: Date.now(),
|
|
464
506
|
staleTime,
|
|
@@ -476,7 +518,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
476
518
|
};
|
|
477
519
|
instances.set(instanceId, created);
|
|
478
520
|
syncInstanceInterval(created);
|
|
479
|
-
if (
|
|
521
|
+
if (pendingRestoredInput) {
|
|
522
|
+
Promise.resolve().then(() => {
|
|
523
|
+
if (!isDisposed && created.version === 0 && instances.get(instanceId) === created) {
|
|
524
|
+
refresh(instanceId, pendingRestoredInput).catch(() => {});
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
} else if (refetchOnMount === 'always' || (refetchOnMount === true && staleTime <= 0)) {
|
|
480
528
|
scheduleAutoRefresh([instanceId], 'mount');
|
|
481
529
|
}
|
|
482
530
|
}
|
|
@@ -535,7 +583,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
535
583
|
* Apply a single server fragment result `{ instanceId, name, html, input, loadMs, renderMs }`
|
|
536
584
|
* to the DOM if and only if `reqVersion >= inst.version`.
|
|
537
585
|
*/
|
|
538
|
-
function applyFragmentPatch(inst, fragmentPayload, reqVersion, chainId = null) {
|
|
586
|
+
function applyFragmentPatch(inst, fragmentPayload, reqVersion, chainId = null, requestedInput = null) {
|
|
539
587
|
if (!inst || !inst.el) return false;
|
|
540
588
|
|
|
541
589
|
if (reqVersion < inst.version) {
|
|
@@ -557,19 +605,27 @@ function createFragmentCoordinator(options = {}) {
|
|
|
557
605
|
|
|
558
606
|
const patchStart = Date.now();
|
|
559
607
|
|
|
608
|
+
const committedInput =
|
|
609
|
+
fragmentPayload.input && typeof fragmentPayload.input === 'object' && !isArray(fragmentPayload.input)
|
|
610
|
+
? fragmentPayload.input
|
|
611
|
+
: requestedInput && typeof requestedInput === 'object' && !isArray(requestedInput)
|
|
612
|
+
? requestedInput
|
|
613
|
+
: null;
|
|
614
|
+
|
|
560
615
|
if (
|
|
561
|
-
|
|
562
|
-
typeof fragmentPayload.input === 'object' &&
|
|
616
|
+
committedInput &&
|
|
563
617
|
!followUpNeeded &&
|
|
564
618
|
pendingTags.size === 0
|
|
565
619
|
) {
|
|
566
|
-
inst.input =
|
|
567
|
-
const serializedInput = JSON.stringify(
|
|
568
|
-
inst.el.setAttribute
|
|
620
|
+
inst.input = { ...committedInput };
|
|
621
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
622
|
+
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
623
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
624
|
+
}
|
|
569
625
|
inst._rawInput = serializedInput;
|
|
570
626
|
if (inst.persistInput) {
|
|
571
627
|
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
572
|
-
store.set(inst.persistInput.key,
|
|
628
|
+
store.set(inst.persistInput.key, inst.input);
|
|
573
629
|
}
|
|
574
630
|
}
|
|
575
631
|
|
|
@@ -583,10 +639,16 @@ function createFragmentCoordinator(options = {}) {
|
|
|
583
639
|
}
|
|
584
640
|
|
|
585
641
|
const patchMs = Math.max(0, Date.now() - patchStart);
|
|
586
|
-
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
inst.lastUpdatedAt = now;
|
|
587
644
|
|
|
588
645
|
// Re-sync nested fragment instances that may have been updated inside this parent fragment
|
|
589
646
|
scanDomInstances(inst.el);
|
|
647
|
+
for (const otherInst of instances.values()) {
|
|
648
|
+
if (otherInst !== inst && otherInst.el && isStrictAncestor(inst.el, otherInst.el)) {
|
|
649
|
+
otherInst.lastUpdatedAt = now;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
590
652
|
|
|
591
653
|
if (typeof options.onAfterPatch === 'function') {
|
|
592
654
|
options.onAfterPatch(inst.el, inst);
|
|
@@ -633,6 +695,26 @@ function createFragmentCoordinator(options = {}) {
|
|
|
633
695
|
targets[0].pagePath ||
|
|
634
696
|
(typeof window !== 'undefined' && window.location ? window.location.pathname : '/');
|
|
635
697
|
|
|
698
|
+
// Collect active inputs of any nested descendant fragments inside the requested parent targets
|
|
699
|
+
// so server-side parent re-rendering preserves nested child filters & pagination.
|
|
700
|
+
const childInputs = {};
|
|
701
|
+
let hasChildInputs = false;
|
|
702
|
+
for (const t of targets) {
|
|
703
|
+
const parentInst = instances.get(t.instanceId);
|
|
704
|
+
if (!parentInst || !parentInst.el) continue;
|
|
705
|
+
for (const otherInst of instances.values()) {
|
|
706
|
+
if (otherInst !== parentInst && otherInst.el && isStrictAncestor(parentInst.el, otherInst.el)) {
|
|
707
|
+
if (otherInst.input && typeof otherInst.input === 'object') {
|
|
708
|
+
childInputs[otherInst.instanceId] = { ...otherInst.input };
|
|
709
|
+
if (!childInputs[otherInst.name]) {
|
|
710
|
+
childInputs[otherInst.name] = { ...otherInst.input };
|
|
711
|
+
}
|
|
712
|
+
hasChildInputs = true;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
636
718
|
const requestPayload = {
|
|
637
719
|
pagePath,
|
|
638
720
|
fragments: targets.map((t) => ({
|
|
@@ -641,6 +723,29 @@ function createFragmentCoordinator(options = {}) {
|
|
|
641
723
|
input: t.input || {},
|
|
642
724
|
version: t.reqVersion,
|
|
643
725
|
})),
|
|
726
|
+
...(hasChildInputs ? { childInputs } : {}),
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
for (const t of targets) {
|
|
730
|
+
const inst = instances.get(t.instanceId);
|
|
731
|
+
if (inst) {
|
|
732
|
+
inst.inFlightCount = (inst.inFlightCount || 0) + 1;
|
|
733
|
+
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
734
|
+
inst.el.setAttribute('aria-busy', 'true');
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const decrementTargetBusy = () => {
|
|
740
|
+
for (const t of targets) {
|
|
741
|
+
const inst = instances.get(t.instanceId);
|
|
742
|
+
if (inst) {
|
|
743
|
+
inst.inFlightCount = Math.max(0, (inst.inFlightCount || 1) - 1);
|
|
744
|
+
if (inst.inFlightCount === 0 && inst.el && typeof inst.el.removeAttribute === 'function') {
|
|
745
|
+
inst.el.removeAttribute('aria-busy');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
644
749
|
};
|
|
645
750
|
|
|
646
751
|
const reqId = uid('frag_req', 4);
|
|
@@ -686,6 +791,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
686
791
|
signal: batchAbortController.signal,
|
|
687
792
|
});
|
|
688
793
|
} catch (err) {
|
|
794
|
+
decrementTargetBusy();
|
|
689
795
|
if (timeoutTimer) {
|
|
690
796
|
clearTimeout(timeoutTimer);
|
|
691
797
|
timeoutTimer = null;
|
|
@@ -766,6 +872,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
766
872
|
}
|
|
767
873
|
} catch (_e) {
|
|
768
874
|
body = null;
|
|
875
|
+
} finally {
|
|
876
|
+
decrementTargetBusy();
|
|
769
877
|
}
|
|
770
878
|
|
|
771
879
|
if (!response.ok) {
|
|
@@ -801,8 +909,20 @@ function createFragmentCoordinator(options = {}) {
|
|
|
801
909
|
}
|
|
802
910
|
|
|
803
911
|
lastFailedBatch = null;
|
|
804
|
-
const
|
|
912
|
+
const rawFragments = (body && body.fragments) || {};
|
|
913
|
+
const resultsMap = {};
|
|
914
|
+
if (isArray(rawFragments)) {
|
|
915
|
+
for (const item of rawFragments) {
|
|
916
|
+
if (item && typeof item === 'object') {
|
|
917
|
+
if (item.instanceId) resultsMap[item.instanceId] = item;
|
|
918
|
+
if (item.name && !resultsMap[item.name]) resultsMap[item.name] = item;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} else if (rawFragments && typeof rawFragments === 'object') {
|
|
922
|
+
Object.assign(resultsMap, rawFragments);
|
|
923
|
+
}
|
|
805
924
|
const refreshedIds = [];
|
|
925
|
+
let staleDiscardCount = 0;
|
|
806
926
|
let totalLoadMs = 0;
|
|
807
927
|
let totalRenderMs = 0;
|
|
808
928
|
|
|
@@ -810,6 +930,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
810
930
|
const inst = instances.get(target.instanceId);
|
|
811
931
|
const fragResult = resultsMap[target.instanceId] || resultsMap[target.name];
|
|
812
932
|
if (!inst || !fragResult) continue;
|
|
933
|
+
if (target.reqVersion < inst.version) {
|
|
934
|
+
staleDiscardCount += 1;
|
|
935
|
+
}
|
|
813
936
|
if (isNumber(fragResult.loadMs)) totalLoadMs += fragResult.loadMs;
|
|
814
937
|
if (isNumber(fragResult.renderMs)) totalRenderMs += fragResult.renderMs;
|
|
815
938
|
|
|
@@ -817,18 +940,22 @@ function createFragmentCoordinator(options = {}) {
|
|
|
817
940
|
inst,
|
|
818
941
|
fragResult,
|
|
819
942
|
target.reqVersion,
|
|
820
|
-
chainIds[0] || null
|
|
943
|
+
chainIds[0] || null,
|
|
944
|
+
target.input
|
|
821
945
|
);
|
|
822
946
|
if (applied) {
|
|
823
947
|
refreshedIds.push(target.instanceId);
|
|
824
948
|
}
|
|
825
949
|
}
|
|
826
950
|
|
|
951
|
+
const allSuperseded =
|
|
952
|
+
refreshedIds.length === 0 && staleDiscardCount > 0 && staleDiscardCount === targets.length;
|
|
953
|
+
|
|
827
954
|
for (const cid of chainIds) {
|
|
828
955
|
if (debug) {
|
|
829
956
|
debug.recordChainRefresh(cid, {
|
|
830
957
|
fragmentIds: refreshedIds,
|
|
831
|
-
status: 'ok',
|
|
958
|
+
status: allSuperseded ? 'superseded' : 'ok',
|
|
832
959
|
loadMs: totalLoadMs,
|
|
833
960
|
renderMs: totalRenderMs,
|
|
834
961
|
totalServerMs: body && isNumber(body.totalMs) ? body.totalMs : null,
|
|
@@ -836,6 +963,20 @@ function createFragmentCoordinator(options = {}) {
|
|
|
836
963
|
}
|
|
837
964
|
}
|
|
838
965
|
|
|
966
|
+
if (allSuperseded) {
|
|
967
|
+
return {
|
|
968
|
+
ok: false,
|
|
969
|
+
canceled: true,
|
|
970
|
+
superseded: true,
|
|
971
|
+
refreshed: [],
|
|
972
|
+
errors: [],
|
|
973
|
+
timings: {
|
|
974
|
+
loadMs: totalLoadMs,
|
|
975
|
+
renderMs: totalRenderMs,
|
|
976
|
+
},
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
|
|
839
980
|
return {
|
|
840
981
|
ok: true,
|
|
841
982
|
refreshed: refreshedIds,
|
|
@@ -1038,34 +1179,35 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1038
1179
|
inst.abortController = abortController;
|
|
1039
1180
|
inst.version += 1;
|
|
1040
1181
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
?
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1048
|
-
inst._rawInput = serializedInput;
|
|
1049
|
-
}
|
|
1050
|
-
if (inst.persistInput) {
|
|
1051
|
-
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
1052
|
-
store.set(inst.persistInput.key, inst.input);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1182
|
+
const candidateInput =
|
|
1183
|
+
nextInput && typeof nextInput === 'object'
|
|
1184
|
+
? refreshOpts.replaceInput
|
|
1185
|
+
? { ...nextInput }
|
|
1186
|
+
: { ...inst.input, ...nextInput }
|
|
1187
|
+
: { ...inst.input };
|
|
1055
1188
|
|
|
1056
1189
|
return {
|
|
1057
1190
|
instanceId: inst.instanceId,
|
|
1058
1191
|
name: inst.name,
|
|
1059
1192
|
pagePath: inst.pagePath,
|
|
1060
|
-
input:
|
|
1193
|
+
input: candidateInput,
|
|
1061
1194
|
reqVersion: inst.version,
|
|
1062
1195
|
};
|
|
1063
1196
|
});
|
|
1064
1197
|
|
|
1198
|
+
const clearOwnedAbortControllers = () => {
|
|
1199
|
+
for (const inst of deduped) {
|
|
1200
|
+
if (inst.abortController === abortController) {
|
|
1201
|
+
inst.abortController = null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1065
1206
|
try {
|
|
1066
1207
|
const result = await executeBatchRefresh(targets, [], abortController.signal, refreshOpts.timeout);
|
|
1208
|
+
clearOwnedAbortControllers();
|
|
1067
1209
|
const enrichedResult = { ok: true, ...result };
|
|
1068
|
-
if (!enrichedResult.canceled && enrichedResult.ok !== false) {
|
|
1210
|
+
if (!enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.ok !== false) {
|
|
1069
1211
|
if (typeof refreshOpts.onSuccess === 'function') {
|
|
1070
1212
|
await refreshOpts.onSuccess(enrichedResult);
|
|
1071
1213
|
}
|
|
@@ -1077,6 +1219,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1077
1219
|
}
|
|
1078
1220
|
return enrichedResult;
|
|
1079
1221
|
} catch (err) {
|
|
1222
|
+
clearOwnedAbortControllers();
|
|
1080
1223
|
const failHandler = refreshOpts.onFailure || refreshOpts.onError;
|
|
1081
1224
|
if (typeof failHandler === 'function') {
|
|
1082
1225
|
try {
|
|
@@ -1121,7 +1264,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1121
1264
|
inst.version += 1;
|
|
1122
1265
|
retryTargets.push({
|
|
1123
1266
|
...t,
|
|
1124
|
-
input: { ...inst.input },
|
|
1267
|
+
input: t.input ? { ...t.input } : { ...inst.input },
|
|
1125
1268
|
reqVersion: inst.version,
|
|
1126
1269
|
});
|
|
1127
1270
|
}
|
|
@@ -1192,10 +1335,22 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1192
1335
|
},
|
|
1193
1336
|
getStoredInput(instanceIdOrName, storageType = 'session') {
|
|
1194
1337
|
const store = storageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
1195
|
-
|
|
1338
|
+
const direct = store.get(`frag_input:${instanceIdOrName}`, null);
|
|
1339
|
+
if (direct !== null) return direct;
|
|
1340
|
+
const inst = instances.get(instanceIdOrName) || Array.from(instances.values()).find((i) => i.name === instanceIdOrName);
|
|
1341
|
+
if (inst && inst.persistInput) {
|
|
1342
|
+
const instStore = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
1343
|
+
return instStore.get(inst.persistInput.key, null);
|
|
1344
|
+
}
|
|
1345
|
+
return null;
|
|
1196
1346
|
},
|
|
1197
1347
|
clearStoredInput(instanceIdOrName, storageType = 'session') {
|
|
1198
1348
|
const store = storageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
1349
|
+
const inst = instances.get(instanceIdOrName) || Array.from(instances.values()).find((i) => i.name === instanceIdOrName);
|
|
1350
|
+
if (inst && inst.persistInput) {
|
|
1351
|
+
const instStore = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
1352
|
+
instStore.remove(inst.persistInput.key);
|
|
1353
|
+
}
|
|
1199
1354
|
return store.remove(`frag_input:${instanceIdOrName}`);
|
|
1200
1355
|
},
|
|
1201
1356
|
get hasFailedRefresh() {
|
|
@@ -1207,7 +1362,12 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1207
1362
|
clearTimeout(focusDebounceTimer);
|
|
1208
1363
|
focusDebounceTimer = null;
|
|
1209
1364
|
}
|
|
1365
|
+
if (autoBatchTimer) {
|
|
1366
|
+
clearTimeout(autoBatchTimer);
|
|
1367
|
+
autoBatchTimer = null;
|
|
1368
|
+
}
|
|
1210
1369
|
pendingAutoInstanceIds.clear();
|
|
1370
|
+
pendingAutoReasons.clear();
|
|
1211
1371
|
removeLifecycleListeners();
|
|
1212
1372
|
for (const inst of instances.values()) {
|
|
1213
1373
|
clearInstanceInterval(inst);
|
package/core/client/host.js
CHANGED
|
@@ -41,10 +41,14 @@ function resolveDocument(root) {
|
|
|
41
41
|
*/
|
|
42
42
|
function resolveDefaultContainer(root, doc) {
|
|
43
43
|
if (root && root.nodeType === 1 && root.tagName !== 'HTML') {
|
|
44
|
-
// If a custom element root was passed and is not inside a fragment, use it or doc.body
|
|
45
44
|
if (doc && doc.body && (root === doc.body || root === doc.documentElement)) {
|
|
46
45
|
return doc.body;
|
|
47
46
|
}
|
|
47
|
+
const insideFragment =
|
|
48
|
+
typeof root.closest === 'function' ? Boolean(root.closest('[data-w-fragment]')) : false;
|
|
49
|
+
if (!insideFragment) {
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
48
52
|
if (!doc || !doc.body) {
|
|
49
53
|
return root;
|
|
50
54
|
}
|
package/core/client/lite.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* Webspresso Lightweight Client Runtime (`core/client/lite.js`)
|
|
5
5
|
*
|
|
6
|
-
* Micro-footprint (~16.
|
|
6
|
+
* Micro-footprint (~16.9 KB Brotli) client runtime for server-driven applications.
|
|
7
7
|
* Includes:
|
|
8
8
|
* - SSR Fragment coordinator (batch refresh, invalidation, auto-revalidation, DOM morphing, focus preservation)
|
|
9
9
|
* - Native HTML Form enhancement (`<form data-w-form>`, validation errors, lifecycle hooks, draft persistence)
|
|
@@ -112,6 +112,12 @@ function defineFragment(config = {}) {
|
|
|
112
112
|
refetchInterval: config.refetchInterval !== undefined ? config.refetchInterval : undefined,
|
|
113
113
|
refetchIntervalInBackground:
|
|
114
114
|
config.refetchIntervalInBackground !== undefined ? config.refetchIntervalInBackground : undefined,
|
|
115
|
+
persistInput:
|
|
116
|
+
config.persistInput !== undefined
|
|
117
|
+
? config.persistInput
|
|
118
|
+
: config.persist !== undefined
|
|
119
|
+
? config.persist
|
|
120
|
+
: undefined,
|
|
115
121
|
};
|
|
116
122
|
}
|
|
117
123
|
|
|
@@ -396,6 +402,7 @@ function wrapFragmentRootHtml({
|
|
|
396
402
|
refetchOnMount,
|
|
397
403
|
refetchInterval,
|
|
398
404
|
refetchIntervalInBackground,
|
|
405
|
+
persistInput,
|
|
399
406
|
}) {
|
|
400
407
|
const safeTag = /^[a-zA-Z][a-zA-Z0-9-]*$/.test(tag || 'div') ? tag : 'div';
|
|
401
408
|
const attrs = [
|
|
@@ -436,6 +443,12 @@ function wrapFragmentRootHtml({
|
|
|
436
443
|
attrs.push(`data-w-refetch-in-background="${escapeHtmlAttr(String(refetchIntervalInBackground))}"`);
|
|
437
444
|
}
|
|
438
445
|
|
|
446
|
+
if (persistInput !== undefined && persistInput !== null && persistInput !== false) {
|
|
447
|
+
const persistAttrVal =
|
|
448
|
+
persistInput === true || persistInput === 'input' ? 'session' : String(persistInput);
|
|
449
|
+
attrs.push(`data-w-persist-input="${escapeHtmlAttr(persistAttrVal)}"`);
|
|
450
|
+
}
|
|
451
|
+
|
|
439
452
|
if (className) {
|
|
440
453
|
attrs.push(`class="${escapeHtmlAttr(className)}"`);
|
|
441
454
|
}
|
|
@@ -572,6 +585,12 @@ async function executeAndRenderFragment({
|
|
|
572
585
|
options.refetchIntervalInBackground !== undefined
|
|
573
586
|
? options.refetchIntervalInBackground
|
|
574
587
|
: fragmentDef.refetchIntervalInBackground,
|
|
588
|
+
persistInput:
|
|
589
|
+
options.persistInput !== undefined
|
|
590
|
+
? options.persistInput
|
|
591
|
+
: options.persist !== undefined
|
|
592
|
+
? options.persist
|
|
593
|
+
: fragmentDef.persistInput,
|
|
575
594
|
});
|
|
576
595
|
|
|
577
596
|
return {
|
|
@@ -620,6 +639,24 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
620
639
|
|
|
621
640
|
const currentRenderCtx = typeof getRenderCtx === 'function' ? getRenderCtx() : { fsy };
|
|
622
641
|
|
|
642
|
+
// If refreshing a parent fragment that contains this nested child fragment,
|
|
643
|
+
// merge the active client-side child input (`req.__fragmentChildInputs`) over static template defaults
|
|
644
|
+
const childInputs = req && req.__fragmentChildInputs;
|
|
645
|
+
let effectiveRawInput = rawInput || {};
|
|
646
|
+
if (childInputs && typeof childInputs === 'object') {
|
|
647
|
+
const override = Object.prototype.hasOwnProperty.call(childInputs, instanceId)
|
|
648
|
+
? childInputs[instanceId]
|
|
649
|
+
: instanceId === `${cleanName}:default` && Object.prototype.hasOwnProperty.call(childInputs, cleanName)
|
|
650
|
+
? childInputs[cleanName]
|
|
651
|
+
: undefined;
|
|
652
|
+
if (override && typeof override === 'object' && !isArray(override)) {
|
|
653
|
+
effectiveRawInput =
|
|
654
|
+
effectiveRawInput && typeof effectiveRawInput === 'object' && !isArray(effectiveRawInput)
|
|
655
|
+
? { ...effectiveRawInput, ...override }
|
|
656
|
+
: { ...override };
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
623
660
|
// Queue execution on `req.__pendingSsrFragments` and return placeholder token
|
|
624
661
|
const placeholderIndex = req.__pendingSsrFragments.length;
|
|
625
662
|
const token = `<!--__WEBSPRESSO_SSR_FRAGMENT_${placeholderIndex}__-->`;
|
|
@@ -629,7 +666,7 @@ function attachSsrFragmentHelper({ req, res, ctx, pageMod, pagePath, nunjucksEnv
|
|
|
629
666
|
fragmentDef,
|
|
630
667
|
instanceId,
|
|
631
668
|
pagePath,
|
|
632
|
-
rawInput:
|
|
669
|
+
rawInput: effectiveRawInput,
|
|
633
670
|
req,
|
|
634
671
|
res,
|
|
635
672
|
ctx,
|
|
@@ -857,6 +894,11 @@ function createFragmentRefreshHandler(config) {
|
|
|
857
894
|
const ctx = typeof buildContext === 'function' ? buildContext(req, res, { fsy }) : { req, res, fsy };
|
|
858
895
|
ensureRequestMemo(req, ctx);
|
|
859
896
|
|
|
897
|
+
req.__fragmentChildInputs =
|
|
898
|
+
payload.childInputs && typeof payload.childInputs === 'object' && !isArray(payload.childInputs)
|
|
899
|
+
? payload.childInputs
|
|
900
|
+
: null;
|
|
901
|
+
|
|
860
902
|
const renderedInstanceIds = new Set();
|
|
861
903
|
attachSsrFragmentHelper({
|
|
862
904
|
req,
|