webspresso 0.0.112 → 0.0.114
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/README.md +26 -0
- package/bin/commands/admin-password.js +1 -7
- package/bin/commands/api.js +1 -7
- package/bin/commands/new.js +1 -7
- package/bin/commands/page.js +1 -7
- package/bin/commands/seed.js +1 -7
- package/bin/commands/service.js +1 -7
- package/bin/utils/prompt.js +17 -0
- package/core/cache/tagged-lru.js +9 -3
- package/core/client/adapter/datastar.js +602 -26
- package/core/client/api.js +151 -7
- package/core/client/controller.js +172 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +479 -29
- package/core/client/fragments.js +456 -27
- package/core/client/index.js +18 -1
- package/core/client/server-fragments.js +125 -40
- package/core/client/utils.js +72 -17
- package/core/console/commands.js +26 -5
- package/core/console/repl.js +7 -1
- 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/plugins/plugin-manager.js +155 -1
- 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 +113 -1
- package/package.json +12 -11
- 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 +140 -6
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/dev-reload/index.js +44 -2
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
- package/src/server.js +59 -3
package/core/client/fragments.js
CHANGED
|
@@ -157,6 +157,173 @@ function createFragmentCoordinator(options = {}) {
|
|
|
157
157
|
let isDisposed = false;
|
|
158
158
|
let lifecycleListenersAttached = false;
|
|
159
159
|
|
|
160
|
+
// Per-version patch options (mode, limit, target, removeKeys, onBeforePatch, onAfterPatch, hookChainDepth)
|
|
161
|
+
const pendingPatchOptsByKey = new Map();
|
|
162
|
+
let pendingInvalidateHookDepth = 0;
|
|
163
|
+
|
|
164
|
+
// Lazy fragment IntersectionObserver state
|
|
165
|
+
let lazyObserver = null;
|
|
166
|
+
const observedLazyEls = new WeakSet();
|
|
167
|
+
|
|
168
|
+
function getLazyObserver() {
|
|
169
|
+
if (lazyObserver) return lazyObserver;
|
|
170
|
+
const win =
|
|
171
|
+
typeof window !== 'undefined'
|
|
172
|
+
? window
|
|
173
|
+
: typeof globalThis !== 'undefined'
|
|
174
|
+
? globalThis
|
|
175
|
+
: null;
|
|
176
|
+
const IOCtor = win && typeof win.IntersectionObserver === 'function' ? win.IntersectionObserver : null;
|
|
177
|
+
if (!IOCtor) return null;
|
|
178
|
+
lazyObserver = new IOCtor((entries) => {
|
|
179
|
+
if (isDisposed) return;
|
|
180
|
+
const visibleIds = [];
|
|
181
|
+
for (let i = 0; i < entries.length; i++) {
|
|
182
|
+
const entry = entries[i];
|
|
183
|
+
if (!entry || !entry.isIntersecting) continue;
|
|
184
|
+
const targetEl = entry.target;
|
|
185
|
+
try {
|
|
186
|
+
lazyObserver.unobserve(targetEl);
|
|
187
|
+
} catch (_e) {}
|
|
188
|
+
const fragId = targetEl && targetEl.getAttribute ? targetEl.getAttribute('data-w-fragment-id') : null;
|
|
189
|
+
const inst = fragId ? instances.get(fragId) : null;
|
|
190
|
+
if (inst && inst.lazy && inst.lazyState === 'idle') {
|
|
191
|
+
inst.lazyState = 'loading';
|
|
192
|
+
visibleIds.push(inst.instanceId);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (visibleIds.length > 0) {
|
|
196
|
+
scheduleAutoRefresh(visibleIds, 'lazy');
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
return lazyObserver;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function observeLazyFragment(inst) {
|
|
203
|
+
if (!inst || !inst.el || !inst.lazy || inst.lazyState !== 'idle') return;
|
|
204
|
+
if (observedLazyEls.has(inst.el)) return;
|
|
205
|
+
const obs = getLazyObserver();
|
|
206
|
+
if (!obs) return;
|
|
207
|
+
observedLazyEls.add(inst.el);
|
|
208
|
+
try {
|
|
209
|
+
obs.observe(inst.el);
|
|
210
|
+
} catch (_e) {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function unobserveLazyFragment(inst) {
|
|
214
|
+
if (!inst || !inst.el || !lazyObserver) return;
|
|
215
|
+
try {
|
|
216
|
+
lazyObserver.unobserve(inst.el);
|
|
217
|
+
} catch (_e) {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getListItemKey(node) {
|
|
221
|
+
if (!node || node.nodeType !== 1 || typeof node.getAttribute !== 'function') return null;
|
|
222
|
+
if (node.id) return `id:${node.id}`;
|
|
223
|
+
const wKey = node.getAttribute('data-w-key');
|
|
224
|
+
if (wKey) return `wkey:${wKey}`;
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function patchIncrementalList(fragEl, html, opts = {}) {
|
|
229
|
+
const doc = fragEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
230
|
+
const parsed = datastar && typeof datastar.parseHtmlFragment === 'function'
|
|
231
|
+
? datastar.parseHtmlFragment(html, doc)
|
|
232
|
+
: null;
|
|
233
|
+
if (!parsed) return false;
|
|
234
|
+
|
|
235
|
+
const targetSel = opts.target || null;
|
|
236
|
+
const container =
|
|
237
|
+
(targetSel && fragEl.querySelector ? fragEl.querySelector(targetSel) : null) ||
|
|
238
|
+
(fragEl.querySelector ? fragEl.querySelector('[data-w-list]') : null) ||
|
|
239
|
+
fragEl;
|
|
240
|
+
const incomingContainer =
|
|
241
|
+
(targetSel && parsed.querySelector ? parsed.querySelector(targetSel) : null) ||
|
|
242
|
+
(parsed.querySelector ? parsed.querySelector('[data-w-list]') : null) ||
|
|
243
|
+
parsed;
|
|
244
|
+
|
|
245
|
+
// 1. Remove explicitly requested keys if any
|
|
246
|
+
if (isArray(opts.removeKeys) && opts.removeKeys.length > 0) {
|
|
247
|
+
const removeSet = new Set(opts.removeKeys.map(String));
|
|
248
|
+
const curChildren = Array.from(container.children || []);
|
|
249
|
+
for (let i = 0; i < curChildren.length; i++) {
|
|
250
|
+
const child = curChildren[i];
|
|
251
|
+
const rawKey = child.getAttribute('data-w-key') || child.id || '';
|
|
252
|
+
const fullKey = getListItemKey(child);
|
|
253
|
+
if (removeSet.has(rawKey) || (fullKey && removeSet.has(fullKey))) {
|
|
254
|
+
if (typeof opts.onBeforeNodeRemove === 'function') opts.onBeforeNodeRemove(child);
|
|
255
|
+
container.removeChild(child);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2. Index existing keyed items in container
|
|
261
|
+
const existingByKey = new Map();
|
|
262
|
+
const existingChildren = Array.from(container.children || []);
|
|
263
|
+
for (let i = 0; i < existingChildren.length; i++) {
|
|
264
|
+
const k = getListItemKey(existingChildren[i]);
|
|
265
|
+
if (k && !existingByKey.has(k)) {
|
|
266
|
+
existingByKey.set(k, existingChildren[i]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const incomingItems = Array.from(incomingContainer.children || []);
|
|
271
|
+
const mode = opts.mode === 'prepend' ? 'prepend' : 'append';
|
|
272
|
+
const itemsToInsert = mode === 'prepend' ? incomingItems.slice().reverse() : incomingItems;
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < itemsToInsert.length; i++) {
|
|
275
|
+
const incoming = itemsToInsert[i];
|
|
276
|
+
if (incoming.nodeType !== 1) continue;
|
|
277
|
+
const key = getListItemKey(incoming);
|
|
278
|
+
const existingMatch = key ? existingByKey.get(key) : null;
|
|
279
|
+
|
|
280
|
+
if (existingMatch && existingMatch.tagName === incoming.tagName) {
|
|
281
|
+
// Update existing keyed item in place without duplicating
|
|
282
|
+
if (datastar && typeof datastar.morphDomTrees === 'function') {
|
|
283
|
+
datastar.morphDomTrees(existingMatch, incoming, {
|
|
284
|
+
onBeforeNodeRemove: opts.onBeforeNodeRemove,
|
|
285
|
+
onNodeAdded: opts.onNodeAdded,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const imported = container.ownerDocument
|
|
292
|
+
? container.ownerDocument.importNode(incoming, true)
|
|
293
|
+
: incoming.cloneNode(true);
|
|
294
|
+
|
|
295
|
+
if (mode === 'prepend') {
|
|
296
|
+
const firstRef = (container.children && container.children[0]) || container.firstChild || null;
|
|
297
|
+
container.insertBefore(imported, firstRef);
|
|
298
|
+
} else {
|
|
299
|
+
container.appendChild(imported);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (key) {
|
|
303
|
+
existingByKey.set(key, imported);
|
|
304
|
+
}
|
|
305
|
+
if (typeof opts.onNodeAdded === 'function') {
|
|
306
|
+
opts.onNodeAdded(imported);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 3. Enforce optional retained item limit
|
|
311
|
+
const limitAttr = container.getAttribute ? container.getAttribute('data-w-limit') : null;
|
|
312
|
+
const rawLimit = opts.limit !== undefined ? opts.limit : limitAttr;
|
|
313
|
+
const limit = rawLimit !== null && rawLimit !== undefined && rawLimit !== '' ? Number(rawLimit) : 0;
|
|
314
|
+
if (Number.isFinite(limit) && limit > 0) {
|
|
315
|
+
while (container.children && container.children.length > limit) {
|
|
316
|
+
const ch = container.children;
|
|
317
|
+
const victim = mode === 'prepend' ? ch[ch.length - 1] : ch[0];
|
|
318
|
+
if (!victim) break;
|
|
319
|
+
if (typeof opts.onBeforeNodeRemove === 'function') opts.onBeforeNodeRemove(victim);
|
|
320
|
+
container.removeChild(victim);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
|
|
160
327
|
function isDocumentHidden() {
|
|
161
328
|
const root = getRoot();
|
|
162
329
|
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
@@ -260,7 +427,12 @@ function createFragmentCoordinator(options = {}) {
|
|
|
260
427
|
try {
|
|
261
428
|
await executeBatchRefresh(targets);
|
|
262
429
|
} catch (_err) {
|
|
263
|
-
// Background auto-refetch errors are non-fatal
|
|
430
|
+
// Background auto-refetch errors are non-fatal; reset lazyState if still unloaded so retry/re-observe works
|
|
431
|
+
for (const inst of deduped) {
|
|
432
|
+
if (inst.lazy && inst.lazyState === 'loading') {
|
|
433
|
+
inst.lazyState = 'idle';
|
|
434
|
+
}
|
|
435
|
+
}
|
|
264
436
|
}
|
|
265
437
|
}
|
|
266
438
|
|
|
@@ -272,7 +444,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
272
444
|
if (reason) pendingAutoReasons.add(reason);
|
|
273
445
|
if (pendingAutoInstanceIds.size === 0) return;
|
|
274
446
|
|
|
275
|
-
if (reason === 'mount') {
|
|
447
|
+
if (reason === 'mount' || reason === 'lazy') {
|
|
276
448
|
if (!autoBatchScheduled) {
|
|
277
449
|
autoBatchScheduled = true;
|
|
278
450
|
Promise.resolve().then(flushAutoRefresh);
|
|
@@ -469,6 +641,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
469
641
|
ensureLifecycleListeners();
|
|
470
642
|
}
|
|
471
643
|
|
|
644
|
+
const rawLazyAttr = el.getAttribute('data-w-lazy');
|
|
645
|
+
const isLazyUnloaded = Boolean(rawLazyAttr && rawLazyAttr !== 'false' && rawLazyAttr !== 'loaded');
|
|
646
|
+
|
|
472
647
|
if (existing) {
|
|
473
648
|
existing.name = name;
|
|
474
649
|
existing.pagePath = pagePath;
|
|
@@ -489,6 +664,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
489
664
|
existing._rawStale = rawStale;
|
|
490
665
|
existing._rawInterval = rawInterval;
|
|
491
666
|
syncInstanceInterval(existing);
|
|
667
|
+
if (existing.lazy && existing.lazyState === 'idle') {
|
|
668
|
+
observeLazyFragment(existing);
|
|
669
|
+
}
|
|
492
670
|
} else {
|
|
493
671
|
const created = {
|
|
494
672
|
instanceId,
|
|
@@ -511,6 +689,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
511
689
|
refetchIntervalInBackground,
|
|
512
690
|
intervalTimer: null,
|
|
513
691
|
activeIntervalMs: 0,
|
|
692
|
+
lazy: isLazyUnloaded,
|
|
693
|
+
lazyState: isLazyUnloaded ? 'idle' : 'loaded',
|
|
694
|
+
staleWhileUnloaded: false,
|
|
514
695
|
_rawInput: rawInput,
|
|
515
696
|
_rawDepends: rawDepends,
|
|
516
697
|
_rawStale: rawStale,
|
|
@@ -518,7 +699,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
518
699
|
};
|
|
519
700
|
instances.set(instanceId, created);
|
|
520
701
|
syncInstanceInterval(created);
|
|
521
|
-
if (
|
|
702
|
+
if (isLazyUnloaded) {
|
|
703
|
+
observeLazyFragment(created);
|
|
704
|
+
} else if (pendingRestoredInput) {
|
|
522
705
|
Promise.resolve().then(() => {
|
|
523
706
|
if (!isDisposed && created.version === 0 && instances.get(instanceId) === created) {
|
|
524
707
|
refresh(instanceId, pendingRestoredInput).catch(() => {});
|
|
@@ -558,6 +741,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
558
741
|
!seenIds.has(id) || (inst.el && inst.el.isConnected === false);
|
|
559
742
|
if (disconnected) {
|
|
560
743
|
clearInstanceInterval(inst);
|
|
744
|
+
unobserveLazyFragment(inst);
|
|
561
745
|
if (inst.abortController) {
|
|
562
746
|
try {
|
|
563
747
|
inst.abortController.abort();
|
|
@@ -603,7 +787,27 @@ function createFragmentCoordinator(options = {}) {
|
|
|
603
787
|
const html = fragmentPayload && typeof fragmentPayload.html === 'string' ? fragmentPayload.html : '';
|
|
604
788
|
if (!html) return false;
|
|
605
789
|
|
|
790
|
+
const patchKey = `${inst.instanceId}:${reqVersion}`;
|
|
791
|
+
const patchOpts = pendingPatchOptsByKey.get(patchKey) || null;
|
|
792
|
+
if (patchOpts) {
|
|
793
|
+
pendingPatchOptsByKey.delete(patchKey);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const patchMeta = {
|
|
797
|
+
hookChainDepth: (patchOpts && patchOpts.hookChainDepth) || 0,
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
if (typeof options.onBeforePatch === 'function') {
|
|
801
|
+
options.onBeforePatch(inst.el, inst, patchMeta);
|
|
802
|
+
}
|
|
803
|
+
if (patchOpts && typeof patchOpts.onBeforePatch === 'function') {
|
|
804
|
+
try {
|
|
805
|
+
patchOpts.onBeforePatch(inst.el, inst, patchMeta);
|
|
806
|
+
} catch (_e) {}
|
|
807
|
+
}
|
|
808
|
+
|
|
606
809
|
const patchStart = Date.now();
|
|
810
|
+
const patchMode = fragmentPayload.mode || (patchOpts && patchOpts.mode) || 'replace';
|
|
607
811
|
|
|
608
812
|
const committedInput =
|
|
609
813
|
fragmentPayload.input && typeof fragmentPayload.input === 'object' && !isArray(fragmentPayload.input)
|
|
@@ -613,6 +817,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
613
817
|
: null;
|
|
614
818
|
|
|
615
819
|
if (
|
|
820
|
+
patchMode !== 'append' &&
|
|
821
|
+
patchMode !== 'prepend' &&
|
|
616
822
|
committedInput &&
|
|
617
823
|
!followUpNeeded &&
|
|
618
824
|
pendingTags.size === 0
|
|
@@ -629,7 +835,26 @@ function createFragmentCoordinator(options = {}) {
|
|
|
629
835
|
}
|
|
630
836
|
}
|
|
631
837
|
|
|
632
|
-
|
|
838
|
+
// Track existing strict descendant fragments before replace morph so removed children can be cleaned up
|
|
839
|
+
const prevDescendants = [];
|
|
840
|
+
if (patchMode !== 'append' && patchMode !== 'prepend') {
|
|
841
|
+
for (const otherInst of instances.values()) {
|
|
842
|
+
if (otherInst !== inst && otherInst.el && isStrictAncestor(inst.el, otherInst.el)) {
|
|
843
|
+
prevDescendants.push(otherInst);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (patchMode === 'append' || patchMode === 'prepend') {
|
|
849
|
+
patchIncrementalList(inst.el, html, {
|
|
850
|
+
mode: patchMode,
|
|
851
|
+
limit: fragmentPayload.limit !== undefined ? fragmentPayload.limit : patchOpts && patchOpts.limit,
|
|
852
|
+
target: fragmentPayload.target || (patchOpts && patchOpts.target),
|
|
853
|
+
removeKeys: fragmentPayload.removeKeys || (patchOpts && patchOpts.removeKeys),
|
|
854
|
+
onBeforeNodeRemove: options.onBeforeNodeRemove,
|
|
855
|
+
onNodeAdded: options.onNodeAdded,
|
|
856
|
+
});
|
|
857
|
+
} else if (datastar && typeof datastar.patchFragmentElement === 'function') {
|
|
633
858
|
datastar.patchFragmentElement(inst.el, html, {
|
|
634
859
|
onBeforeNodeRemove: options.onBeforeNodeRemove,
|
|
635
860
|
onNodeAdded: options.onNodeAdded,
|
|
@@ -638,20 +863,56 @@ function createFragmentCoordinator(options = {}) {
|
|
|
638
863
|
inst.el.innerHTML = html;
|
|
639
864
|
}
|
|
640
865
|
|
|
866
|
+
// Clear lazy unloaded markers once a real patch is applied
|
|
867
|
+
if (inst.lazy || inst.lazyState !== 'loaded') {
|
|
868
|
+
inst.lazy = false;
|
|
869
|
+
inst.lazyState = 'loaded';
|
|
870
|
+
inst.staleWhileUnloaded = false;
|
|
871
|
+
unobserveLazyFragment(inst);
|
|
872
|
+
if (inst.el && typeof inst.el.removeAttribute === 'function') {
|
|
873
|
+
inst.el.removeAttribute('data-w-lazy');
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
641
877
|
const patchMs = Math.max(0, Date.now() - patchStart);
|
|
642
878
|
const now = Date.now();
|
|
643
879
|
inst.lastUpdatedAt = now;
|
|
644
880
|
|
|
881
|
+
// Clean up any nested child fragments removed during this parent patch
|
|
882
|
+
for (let i = 0; i < prevDescendants.length; i++) {
|
|
883
|
+
const prevChild = prevDescendants[i];
|
|
884
|
+
if (!prevChild.el || prevChild.el.isConnected === false || !isStrictAncestor(inst.el, prevChild.el)) {
|
|
885
|
+
clearInstanceInterval(prevChild);
|
|
886
|
+
unobserveLazyFragment(prevChild);
|
|
887
|
+
if (prevChild.abortController) {
|
|
888
|
+
try {
|
|
889
|
+
prevChild.abortController.abort();
|
|
890
|
+
} catch (_e) {}
|
|
891
|
+
prevChild.abortController = null;
|
|
892
|
+
}
|
|
893
|
+
instances.delete(prevChild.instanceId);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
645
897
|
// Re-sync nested fragment instances that may have been updated inside this parent fragment
|
|
646
898
|
scanDomInstances(inst.el);
|
|
647
899
|
for (const otherInst of instances.values()) {
|
|
648
900
|
if (otherInst !== inst && otherInst.el && isStrictAncestor(inst.el, otherInst.el)) {
|
|
649
|
-
otherInst.
|
|
901
|
+
if (otherInst.el.__wPreservedLazyChild) {
|
|
902
|
+
delete otherInst.el.__wPreservedLazyChild;
|
|
903
|
+
} else if (!(otherInst.lazy && otherInst.lazyState === 'idle')) {
|
|
904
|
+
otherInst.lastUpdatedAt = now;
|
|
905
|
+
}
|
|
650
906
|
}
|
|
651
907
|
}
|
|
652
908
|
|
|
653
909
|
if (typeof options.onAfterPatch === 'function') {
|
|
654
|
-
options.onAfterPatch(inst.el, inst);
|
|
910
|
+
options.onAfterPatch(inst.el, inst, patchMeta);
|
|
911
|
+
}
|
|
912
|
+
if (patchOpts && typeof patchOpts.onAfterPatch === 'function') {
|
|
913
|
+
try {
|
|
914
|
+
patchOpts.onAfterPatch(inst.el, inst, patchMeta);
|
|
915
|
+
} catch (_e) {}
|
|
655
916
|
}
|
|
656
917
|
|
|
657
918
|
if (debug) {
|
|
@@ -704,6 +965,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
704
965
|
if (!parentInst || !parentInst.el) continue;
|
|
705
966
|
for (const otherInst of instances.values()) {
|
|
706
967
|
if (otherInst !== parentInst && otherInst.el && isStrictAncestor(parentInst.el, otherInst.el)) {
|
|
968
|
+
// Supersede any older in-flight refresh on the nested child so a late child response
|
|
969
|
+
// cannot overwrite the newer parent+child snapshot.
|
|
970
|
+
otherInst.version += 1;
|
|
707
971
|
if (otherInst.input && typeof otherInst.input === 'object') {
|
|
708
972
|
childInputs[otherInst.instanceId] = { ...otherInst.input };
|
|
709
973
|
if (!childInputs[otherInst.name]) {
|
|
@@ -908,8 +1172,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
908
1172
|
debug.trackRequestEnd(reqId, { status: 'completed', httpStatus: response.status });
|
|
909
1173
|
}
|
|
910
1174
|
|
|
911
|
-
lastFailedBatch = null;
|
|
912
1175
|
const rawFragments = (body && body.fragments) || {};
|
|
1176
|
+
const rawErrors = (body && body.errors && typeof body.errors === 'object' ? body.errors : {}) || {};
|
|
913
1177
|
const resultsMap = {};
|
|
914
1178
|
if (isArray(rawFragments)) {
|
|
915
1179
|
for (const item of rawFragments) {
|
|
@@ -922,12 +1186,42 @@ function createFragmentCoordinator(options = {}) {
|
|
|
922
1186
|
Object.assign(resultsMap, rawFragments);
|
|
923
1187
|
}
|
|
924
1188
|
const refreshedIds = [];
|
|
1189
|
+
const batchErrors = [];
|
|
1190
|
+
const failedTargets = [];
|
|
925
1191
|
let staleDiscardCount = 0;
|
|
926
1192
|
let totalLoadMs = 0;
|
|
927
1193
|
let totalRenderMs = 0;
|
|
928
1194
|
|
|
929
1195
|
for (const target of targets) {
|
|
930
1196
|
const inst = instances.get(target.instanceId);
|
|
1197
|
+
const fragError =
|
|
1198
|
+
rawErrors[target.instanceId] ||
|
|
1199
|
+
rawErrors[target.name] ||
|
|
1200
|
+
(resultsMap[target.instanceId] && resultsMap[target.instanceId].error) ||
|
|
1201
|
+
(resultsMap[target.name] && resultsMap[target.name].error) ||
|
|
1202
|
+
null;
|
|
1203
|
+
if (fragError) {
|
|
1204
|
+
const errObj =
|
|
1205
|
+
fragError instanceof Error
|
|
1206
|
+
? fragError
|
|
1207
|
+
: Object.assign(
|
|
1208
|
+
new Error(
|
|
1209
|
+
(fragError && typeof fragError === 'object' && fragError.message) ||
|
|
1210
|
+
String(fragError) ||
|
|
1211
|
+
`Fragment "${target.name}" failed to refresh`
|
|
1212
|
+
),
|
|
1213
|
+
{
|
|
1214
|
+
status: (fragError && fragError.status) || 500,
|
|
1215
|
+
code: (fragError && fragError.code) || 'FRAGMENT_REFRESH_ERROR',
|
|
1216
|
+
instanceId: target.instanceId,
|
|
1217
|
+
fragmentName: target.name,
|
|
1218
|
+
}
|
|
1219
|
+
);
|
|
1220
|
+
batchErrors.push(errObj);
|
|
1221
|
+
failedTargets.push(target);
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
931
1225
|
const fragResult = resultsMap[target.instanceId] || resultsMap[target.name];
|
|
932
1226
|
if (!inst || !fragResult) continue;
|
|
933
1227
|
if (target.reqVersion < inst.version) {
|
|
@@ -949,13 +1243,33 @@ function createFragmentCoordinator(options = {}) {
|
|
|
949
1243
|
}
|
|
950
1244
|
|
|
951
1245
|
const allSuperseded =
|
|
952
|
-
refreshedIds.length === 0 &&
|
|
1246
|
+
refreshedIds.length === 0 &&
|
|
1247
|
+
batchErrors.length === 0 &&
|
|
1248
|
+
staleDiscardCount > 0 &&
|
|
1249
|
+
staleDiscardCount === targets.length;
|
|
1250
|
+
|
|
1251
|
+
if (refreshedIds.length > 0 && lastFailedBatch && isArray(lastFailedBatch.targets)) {
|
|
1252
|
+
const refreshedSet = new Set(refreshedIds);
|
|
1253
|
+
const remainingFailed = lastFailedBatch.targets.filter((t) => !refreshedSet.has(t.instanceId));
|
|
1254
|
+
lastFailedBatch =
|
|
1255
|
+
remainingFailed.length > 0
|
|
1256
|
+
? { targets: remainingFailed, chainIds: lastFailedBatch.chainIds }
|
|
1257
|
+
: null;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (batchErrors.length > 0) {
|
|
1261
|
+
lastFailedBatch = {
|
|
1262
|
+
targets: failedTargets,
|
|
1263
|
+
chainIds,
|
|
1264
|
+
};
|
|
1265
|
+
}
|
|
953
1266
|
|
|
954
1267
|
for (const cid of chainIds) {
|
|
955
1268
|
if (debug) {
|
|
956
1269
|
debug.recordChainRefresh(cid, {
|
|
957
1270
|
fragmentIds: refreshedIds,
|
|
958
|
-
status: allSuperseded ? 'superseded' : 'ok',
|
|
1271
|
+
status: batchErrors.length > 0 ? (refreshedIds.length > 0 ? 'partial-error' : 'error') : allSuperseded ? 'superseded' : 'ok',
|
|
1272
|
+
error: batchErrors[0] || null,
|
|
959
1273
|
loadMs: totalLoadMs,
|
|
960
1274
|
renderMs: totalRenderMs,
|
|
961
1275
|
totalServerMs: body && isNumber(body.totalMs) ? body.totalMs : null,
|
|
@@ -977,6 +1291,20 @@ function createFragmentCoordinator(options = {}) {
|
|
|
977
1291
|
};
|
|
978
1292
|
}
|
|
979
1293
|
|
|
1294
|
+
if (batchErrors.length > 0) {
|
|
1295
|
+
return {
|
|
1296
|
+
ok: false,
|
|
1297
|
+
partial: refreshedIds.length > 0,
|
|
1298
|
+
refreshed: refreshedIds,
|
|
1299
|
+
errors: batchErrors,
|
|
1300
|
+
error: batchErrors[0],
|
|
1301
|
+
timings: {
|
|
1302
|
+
loadMs: totalLoadMs,
|
|
1303
|
+
renderMs: totalRenderMs,
|
|
1304
|
+
},
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
|
|
980
1308
|
return {
|
|
981
1309
|
ok: true,
|
|
982
1310
|
refreshed: refreshedIds,
|
|
@@ -1017,13 +1345,18 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1017
1345
|
for (const inst of instances.values()) {
|
|
1018
1346
|
let matched = false;
|
|
1019
1347
|
for (const tag of tagsToFlush) {
|
|
1020
|
-
if (inst.dependsOn.has(tag) || inst.name === tag) {
|
|
1348
|
+
if (inst.dependsOn.has(tag) || inst.name === tag || inst.instanceId === tag) {
|
|
1021
1349
|
matched = true;
|
|
1022
1350
|
break;
|
|
1023
1351
|
}
|
|
1024
1352
|
}
|
|
1025
1353
|
if (matched) {
|
|
1026
|
-
|
|
1354
|
+
if (inst.lazy && inst.lazyState === 'idle') {
|
|
1355
|
+
// Do not fetch off-screen unloaded lazy fragments on invalidation; mark stale for when visible
|
|
1356
|
+
inst.staleWhileUnloaded = true;
|
|
1357
|
+
} else {
|
|
1358
|
+
matchingInstances.push(inst);
|
|
1359
|
+
}
|
|
1027
1360
|
}
|
|
1028
1361
|
}
|
|
1029
1362
|
|
|
@@ -1043,8 +1376,16 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1043
1376
|
// Parent/Child deduplication: if parent & child are both invalidated, parent refresh covers child
|
|
1044
1377
|
const dedupedInstances = deduplicateParentChildFragments(matchingInstances);
|
|
1045
1378
|
|
|
1379
|
+
const batchHookDepth = pendingInvalidateHookDepth;
|
|
1380
|
+
pendingInvalidateHookDepth = 0;
|
|
1381
|
+
|
|
1046
1382
|
const targets = dedupedInstances.map((inst) => {
|
|
1047
1383
|
inst.version += 1;
|
|
1384
|
+
if (batchHookDepth > 0) {
|
|
1385
|
+
pendingPatchOptsByKey.set(`${inst.instanceId}:${inst.version}`, {
|
|
1386
|
+
hookChainDepth: batchHookDepth,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1048
1389
|
return {
|
|
1049
1390
|
instanceId: inst.instanceId,
|
|
1050
1391
|
name: inst.name,
|
|
@@ -1089,6 +1430,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1089
1430
|
* @returns {Promise<{ refreshed: string[], skipped?: boolean }>}
|
|
1090
1431
|
*/
|
|
1091
1432
|
function invalidate(tags, meta = {}) {
|
|
1433
|
+
const activeHook = typeof options.getPatchHookContext === 'function' ? options.getPatchHookContext() : null;
|
|
1434
|
+
if (activeHook) {
|
|
1435
|
+
const nextDepth = (activeHook.depth || 0) + 1;
|
|
1436
|
+
if (nextDepth > pendingInvalidateHookDepth) {
|
|
1437
|
+
pendingInvalidateHookDepth = nextDepth;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1092
1440
|
const tagList = isArray(tags) ? tags : [tags];
|
|
1093
1441
|
for (const raw of tagList) {
|
|
1094
1442
|
if (typeof raw === 'string' && raw.trim()) {
|
|
@@ -1179,6 +1527,34 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1179
1527
|
inst.abortController = abortController;
|
|
1180
1528
|
inst.version += 1;
|
|
1181
1529
|
|
|
1530
|
+
if (inst.lazy && inst.lazyState === 'idle') {
|
|
1531
|
+
inst.lazyState = 'loading';
|
|
1532
|
+
unobserveLazyFragment(inst);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const activeHook = typeof options.getPatchHookContext === 'function' ? options.getPatchHookContext() : null;
|
|
1536
|
+
const hookChainDepth = activeHook ? (activeHook.depth || 0) + 1 : 0;
|
|
1537
|
+
|
|
1538
|
+
if (
|
|
1539
|
+
refreshOpts.mode ||
|
|
1540
|
+
refreshOpts.limit !== undefined ||
|
|
1541
|
+
refreshOpts.target ||
|
|
1542
|
+
refreshOpts.removeKeys ||
|
|
1543
|
+
hookChainDepth > 0 ||
|
|
1544
|
+
typeof refreshOpts.onBeforePatch === 'function' ||
|
|
1545
|
+
typeof refreshOpts.onAfterPatch === 'function'
|
|
1546
|
+
) {
|
|
1547
|
+
pendingPatchOptsByKey.set(`${inst.instanceId}:${inst.version}`, {
|
|
1548
|
+
mode: refreshOpts.mode || null,
|
|
1549
|
+
limit: refreshOpts.limit,
|
|
1550
|
+
target: refreshOpts.target,
|
|
1551
|
+
removeKeys: refreshOpts.removeKeys,
|
|
1552
|
+
hookChainDepth,
|
|
1553
|
+
onBeforePatch: refreshOpts.onBeforePatch,
|
|
1554
|
+
onAfterPatch: refreshOpts.onAfterPatch,
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1182
1558
|
const candidateInput =
|
|
1183
1559
|
nextInput && typeof nextInput === 'object'
|
|
1184
1560
|
? refreshOpts.replaceInput
|
|
@@ -1207,6 +1583,21 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1207
1583
|
const result = await executeBatchRefresh(targets, [], abortController.signal, refreshOpts.timeout);
|
|
1208
1584
|
clearOwnedAbortControllers();
|
|
1209
1585
|
const enrichedResult = { ok: true, ...result };
|
|
1586
|
+
if (enrichedResult.ok === false && !enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.error) {
|
|
1587
|
+
const failHandler = refreshOpts.onFailure || refreshOpts.onError;
|
|
1588
|
+
if (typeof failHandler === 'function') {
|
|
1589
|
+
await failHandler(enrichedResult.error);
|
|
1590
|
+
}
|
|
1591
|
+
if (typeof refreshOpts.onSettled === 'function') {
|
|
1592
|
+
await refreshOpts.onSettled(enrichedResult, enrichedResult.error);
|
|
1593
|
+
} else if (typeof refreshOpts.onComplete === 'function') {
|
|
1594
|
+
await refreshOpts.onComplete(enrichedResult, enrichedResult.error);
|
|
1595
|
+
}
|
|
1596
|
+
if (refreshOpts.throwOnError === true) {
|
|
1597
|
+
throw enrichedResult.error;
|
|
1598
|
+
}
|
|
1599
|
+
return enrichedResult;
|
|
1600
|
+
}
|
|
1210
1601
|
if (!enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.ok !== false) {
|
|
1211
1602
|
if (typeof refreshOpts.onSuccess === 'function') {
|
|
1212
1603
|
await refreshOpts.onSuccess(enrichedResult);
|
|
@@ -1277,6 +1668,24 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1277
1668
|
return executeBatchRefresh(retryTargets, chainIds);
|
|
1278
1669
|
}
|
|
1279
1670
|
|
|
1671
|
+
function resolveInstanceByIdOrName(idOrName) {
|
|
1672
|
+
if (!idOrName) return null;
|
|
1673
|
+
const direct = instances.get(idOrName);
|
|
1674
|
+
if (direct && (!direct.el || direct.el.isConnected !== false)) {
|
|
1675
|
+
return direct;
|
|
1676
|
+
}
|
|
1677
|
+
const defaultInst = instances.get(`${idOrName}:default`);
|
|
1678
|
+
if (defaultInst && (!defaultInst.el || defaultInst.el.isConnected !== false)) {
|
|
1679
|
+
return defaultInst;
|
|
1680
|
+
}
|
|
1681
|
+
for (const inst of instances.values()) {
|
|
1682
|
+
if (inst.name === idOrName && (!inst.el || inst.el.isConnected !== false)) {
|
|
1683
|
+
return inst;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1280
1689
|
return {
|
|
1281
1690
|
syncDomInstances,
|
|
1282
1691
|
invalidate,
|
|
@@ -1288,17 +1697,28 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1288
1697
|
scanDomInstances();
|
|
1289
1698
|
for (const item of items) {
|
|
1290
1699
|
const key = item.instanceId || item.name;
|
|
1291
|
-
const
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1700
|
+
const matchedList = [];
|
|
1701
|
+
if (item.instanceId && instances.has(item.instanceId)) {
|
|
1702
|
+
matchedList.push(instances.get(item.instanceId));
|
|
1703
|
+
} else {
|
|
1704
|
+
for (const inst of instances.values()) {
|
|
1705
|
+
if (inst.instanceId === key || inst.name === key) {
|
|
1706
|
+
matchedList.push(inst);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (matchedList.length > 0) {
|
|
1711
|
+
for (const inst of matchedList) {
|
|
1712
|
+
if (item.input && typeof item.input === 'object') {
|
|
1713
|
+
inst.input = { ...item.input };
|
|
1714
|
+
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
1715
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
1716
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1717
|
+
inst._rawInput = serializedInput;
|
|
1718
|
+
}
|
|
1299
1719
|
}
|
|
1720
|
+
pendingTags.add(item.instanceId ? inst.instanceId : inst.name);
|
|
1300
1721
|
}
|
|
1301
|
-
pendingTags.add(inst.name);
|
|
1302
1722
|
} else if (key) {
|
|
1303
1723
|
pendingTags.add(String(key).trim());
|
|
1304
1724
|
}
|
|
@@ -1311,7 +1731,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1311
1731
|
const matching = [];
|
|
1312
1732
|
for (const inst of instances.values()) {
|
|
1313
1733
|
for (const t of tagSet) {
|
|
1314
|
-
if (inst.dependsOn.has(t) || inst.name === t) {
|
|
1734
|
+
if (inst.dependsOn.has(t) || inst.name === t || inst.instanceId === t) {
|
|
1315
1735
|
matching.push(inst);
|
|
1316
1736
|
break;
|
|
1317
1737
|
}
|
|
@@ -1321,17 +1741,19 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1321
1741
|
},
|
|
1322
1742
|
retryFailed,
|
|
1323
1743
|
retryLastFailed: retryFailed,
|
|
1324
|
-
getInstance(
|
|
1325
|
-
const hit =
|
|
1326
|
-
if (hit
|
|
1744
|
+
getInstance(instanceIdOrName) {
|
|
1745
|
+
const hit = resolveInstanceByIdOrName(instanceIdOrName);
|
|
1746
|
+
if (hit) {
|
|
1327
1747
|
return hit;
|
|
1328
1748
|
}
|
|
1329
1749
|
scanDomInstances();
|
|
1330
|
-
return
|
|
1750
|
+
return resolveInstanceByIdOrName(instanceIdOrName);
|
|
1331
1751
|
},
|
|
1332
|
-
getInstances() {
|
|
1752
|
+
getInstances(nameOrId) {
|
|
1333
1753
|
scanDomInstances();
|
|
1334
|
-
|
|
1754
|
+
const all = Array.from(instances.values());
|
|
1755
|
+
if (!nameOrId) return all;
|
|
1756
|
+
return all.filter((inst) => inst.instanceId === nameOrId || inst.name === nameOrId);
|
|
1335
1757
|
},
|
|
1336
1758
|
getStoredInput(instanceIdOrName, storageType = 'session') {
|
|
1337
1759
|
const store = storageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
@@ -1368,6 +1790,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1368
1790
|
}
|
|
1369
1791
|
pendingAutoInstanceIds.clear();
|
|
1370
1792
|
pendingAutoReasons.clear();
|
|
1793
|
+
pendingPatchOptsByKey.clear();
|
|
1794
|
+
if (lazyObserver && typeof lazyObserver.disconnect === 'function') {
|
|
1795
|
+
try {
|
|
1796
|
+
lazyObserver.disconnect();
|
|
1797
|
+
} catch (_e) {}
|
|
1798
|
+
lazyObserver = null;
|
|
1799
|
+
}
|
|
1371
1800
|
removeLifecycleListeners();
|
|
1372
1801
|
for (const inst of instances.values()) {
|
|
1373
1802
|
clearInstanceInterval(inst);
|