webspresso 0.0.112 → 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 +587 -26
- package/core/client/api.js +147 -7
- package/core/client/controller.js +134 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +475 -29
- package/core/client/fragments.js +397 -25
- package/core/client/index.js +13 -1
- package/core/client/server-fragments.js +102 -36
- 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 +98 -1
- 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/fragments.js
CHANGED
|
@@ -157,6 +157,172 @@ 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)
|
|
161
|
+
const pendingPatchOptsByKey = new Map();
|
|
162
|
+
|
|
163
|
+
// Lazy fragment IntersectionObserver state
|
|
164
|
+
let lazyObserver = null;
|
|
165
|
+
const observedLazyEls = new WeakSet();
|
|
166
|
+
|
|
167
|
+
function getLazyObserver() {
|
|
168
|
+
if (lazyObserver) return lazyObserver;
|
|
169
|
+
const win =
|
|
170
|
+
typeof window !== 'undefined'
|
|
171
|
+
? window
|
|
172
|
+
: typeof globalThis !== 'undefined'
|
|
173
|
+
? globalThis
|
|
174
|
+
: null;
|
|
175
|
+
const IOCtor = win && typeof win.IntersectionObserver === 'function' ? win.IntersectionObserver : null;
|
|
176
|
+
if (!IOCtor) return null;
|
|
177
|
+
lazyObserver = new IOCtor((entries) => {
|
|
178
|
+
if (isDisposed) return;
|
|
179
|
+
const visibleIds = [];
|
|
180
|
+
for (let i = 0; i < entries.length; i++) {
|
|
181
|
+
const entry = entries[i];
|
|
182
|
+
if (!entry || !entry.isIntersecting) continue;
|
|
183
|
+
const targetEl = entry.target;
|
|
184
|
+
try {
|
|
185
|
+
lazyObserver.unobserve(targetEl);
|
|
186
|
+
} catch (_e) {}
|
|
187
|
+
const fragId = targetEl && targetEl.getAttribute ? targetEl.getAttribute('data-w-fragment-id') : null;
|
|
188
|
+
const inst = fragId ? instances.get(fragId) : null;
|
|
189
|
+
if (inst && inst.lazy && inst.lazyState === 'idle') {
|
|
190
|
+
inst.lazyState = 'loading';
|
|
191
|
+
visibleIds.push(inst.instanceId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (visibleIds.length > 0) {
|
|
195
|
+
scheduleAutoRefresh(visibleIds, 'lazy');
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return lazyObserver;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function observeLazyFragment(inst) {
|
|
202
|
+
if (!inst || !inst.el || !inst.lazy || inst.lazyState !== 'idle') return;
|
|
203
|
+
if (observedLazyEls.has(inst.el)) return;
|
|
204
|
+
const obs = getLazyObserver();
|
|
205
|
+
if (!obs) return;
|
|
206
|
+
observedLazyEls.add(inst.el);
|
|
207
|
+
try {
|
|
208
|
+
obs.observe(inst.el);
|
|
209
|
+
} catch (_e) {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function unobserveLazyFragment(inst) {
|
|
213
|
+
if (!inst || !inst.el || !lazyObserver) return;
|
|
214
|
+
try {
|
|
215
|
+
lazyObserver.unobserve(inst.el);
|
|
216
|
+
} catch (_e) {}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function getListItemKey(node) {
|
|
220
|
+
if (!node || node.nodeType !== 1 || typeof node.getAttribute !== 'function') return null;
|
|
221
|
+
if (node.id) return `id:${node.id}`;
|
|
222
|
+
const wKey = node.getAttribute('data-w-key');
|
|
223
|
+
if (wKey) return `wkey:${wKey}`;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function patchIncrementalList(fragEl, html, opts = {}) {
|
|
228
|
+
const doc = fragEl.ownerDocument || (typeof document !== 'undefined' ? document : null);
|
|
229
|
+
const parsed = datastar && typeof datastar.parseHtmlFragment === 'function'
|
|
230
|
+
? datastar.parseHtmlFragment(html, doc)
|
|
231
|
+
: null;
|
|
232
|
+
if (!parsed) return false;
|
|
233
|
+
|
|
234
|
+
const targetSel = opts.target || null;
|
|
235
|
+
const container =
|
|
236
|
+
(targetSel && fragEl.querySelector ? fragEl.querySelector(targetSel) : null) ||
|
|
237
|
+
(fragEl.querySelector ? fragEl.querySelector('[data-w-list]') : null) ||
|
|
238
|
+
fragEl;
|
|
239
|
+
const incomingContainer =
|
|
240
|
+
(targetSel && parsed.querySelector ? parsed.querySelector(targetSel) : null) ||
|
|
241
|
+
(parsed.querySelector ? parsed.querySelector('[data-w-list]') : null) ||
|
|
242
|
+
parsed;
|
|
243
|
+
|
|
244
|
+
// 1. Remove explicitly requested keys if any
|
|
245
|
+
if (isArray(opts.removeKeys) && opts.removeKeys.length > 0) {
|
|
246
|
+
const removeSet = new Set(opts.removeKeys.map(String));
|
|
247
|
+
const curChildren = Array.from(container.children || []);
|
|
248
|
+
for (let i = 0; i < curChildren.length; i++) {
|
|
249
|
+
const child = curChildren[i];
|
|
250
|
+
const rawKey = child.getAttribute('data-w-key') || child.id || '';
|
|
251
|
+
const fullKey = getListItemKey(child);
|
|
252
|
+
if (removeSet.has(rawKey) || (fullKey && removeSet.has(fullKey))) {
|
|
253
|
+
if (typeof opts.onBeforeNodeRemove === 'function') opts.onBeforeNodeRemove(child);
|
|
254
|
+
container.removeChild(child);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 2. Index existing keyed items in container
|
|
260
|
+
const existingByKey = new Map();
|
|
261
|
+
const existingChildren = Array.from(container.children || []);
|
|
262
|
+
for (let i = 0; i < existingChildren.length; i++) {
|
|
263
|
+
const k = getListItemKey(existingChildren[i]);
|
|
264
|
+
if (k && !existingByKey.has(k)) {
|
|
265
|
+
existingByKey.set(k, existingChildren[i]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const incomingItems = Array.from(incomingContainer.children || []);
|
|
270
|
+
const mode = opts.mode === 'prepend' ? 'prepend' : 'append';
|
|
271
|
+
const itemsToInsert = mode === 'prepend' ? incomingItems.slice().reverse() : incomingItems;
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < itemsToInsert.length; i++) {
|
|
274
|
+
const incoming = itemsToInsert[i];
|
|
275
|
+
if (incoming.nodeType !== 1) continue;
|
|
276
|
+
const key = getListItemKey(incoming);
|
|
277
|
+
const existingMatch = key ? existingByKey.get(key) : null;
|
|
278
|
+
|
|
279
|
+
if (existingMatch && existingMatch.tagName === incoming.tagName) {
|
|
280
|
+
// Update existing keyed item in place without duplicating
|
|
281
|
+
if (datastar && typeof datastar.morphDomTrees === 'function') {
|
|
282
|
+
datastar.morphDomTrees(existingMatch, incoming, {
|
|
283
|
+
onBeforeNodeRemove: opts.onBeforeNodeRemove,
|
|
284
|
+
onNodeAdded: opts.onNodeAdded,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const imported = container.ownerDocument
|
|
291
|
+
? container.ownerDocument.importNode(incoming, true)
|
|
292
|
+
: incoming.cloneNode(true);
|
|
293
|
+
|
|
294
|
+
if (mode === 'prepend') {
|
|
295
|
+
const firstRef = (container.children && container.children[0]) || container.firstChild || null;
|
|
296
|
+
container.insertBefore(imported, firstRef);
|
|
297
|
+
} else {
|
|
298
|
+
container.appendChild(imported);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (key) {
|
|
302
|
+
existingByKey.set(key, imported);
|
|
303
|
+
}
|
|
304
|
+
if (typeof opts.onNodeAdded === 'function') {
|
|
305
|
+
opts.onNodeAdded(imported);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 3. Enforce optional retained item limit
|
|
310
|
+
const limitAttr = container.getAttribute ? container.getAttribute('data-w-limit') : null;
|
|
311
|
+
const rawLimit = opts.limit !== undefined ? opts.limit : limitAttr;
|
|
312
|
+
const limit = rawLimit !== null && rawLimit !== undefined && rawLimit !== '' ? Number(rawLimit) : 0;
|
|
313
|
+
if (Number.isFinite(limit) && limit > 0) {
|
|
314
|
+
while (container.children && container.children.length > limit) {
|
|
315
|
+
const ch = container.children;
|
|
316
|
+
const victim = mode === 'prepend' ? ch[ch.length - 1] : ch[0];
|
|
317
|
+
if (!victim) break;
|
|
318
|
+
if (typeof opts.onBeforeNodeRemove === 'function') opts.onBeforeNodeRemove(victim);
|
|
319
|
+
container.removeChild(victim);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
160
326
|
function isDocumentHidden() {
|
|
161
327
|
const root = getRoot();
|
|
162
328
|
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
@@ -260,7 +426,12 @@ function createFragmentCoordinator(options = {}) {
|
|
|
260
426
|
try {
|
|
261
427
|
await executeBatchRefresh(targets);
|
|
262
428
|
} catch (_err) {
|
|
263
|
-
// Background auto-refetch errors are non-fatal
|
|
429
|
+
// Background auto-refetch errors are non-fatal; reset lazyState if still unloaded so retry/re-observe works
|
|
430
|
+
for (const inst of deduped) {
|
|
431
|
+
if (inst.lazy && inst.lazyState === 'loading') {
|
|
432
|
+
inst.lazyState = 'idle';
|
|
433
|
+
}
|
|
434
|
+
}
|
|
264
435
|
}
|
|
265
436
|
}
|
|
266
437
|
|
|
@@ -272,7 +443,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
272
443
|
if (reason) pendingAutoReasons.add(reason);
|
|
273
444
|
if (pendingAutoInstanceIds.size === 0) return;
|
|
274
445
|
|
|
275
|
-
if (reason === 'mount') {
|
|
446
|
+
if (reason === 'mount' || reason === 'lazy') {
|
|
276
447
|
if (!autoBatchScheduled) {
|
|
277
448
|
autoBatchScheduled = true;
|
|
278
449
|
Promise.resolve().then(flushAutoRefresh);
|
|
@@ -469,6 +640,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
469
640
|
ensureLifecycleListeners();
|
|
470
641
|
}
|
|
471
642
|
|
|
643
|
+
const rawLazyAttr = el.getAttribute('data-w-lazy');
|
|
644
|
+
const isLazyUnloaded = Boolean(rawLazyAttr && rawLazyAttr !== 'false' && rawLazyAttr !== 'loaded');
|
|
645
|
+
|
|
472
646
|
if (existing) {
|
|
473
647
|
existing.name = name;
|
|
474
648
|
existing.pagePath = pagePath;
|
|
@@ -489,6 +663,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
489
663
|
existing._rawStale = rawStale;
|
|
490
664
|
existing._rawInterval = rawInterval;
|
|
491
665
|
syncInstanceInterval(existing);
|
|
666
|
+
if (existing.lazy && existing.lazyState === 'idle') {
|
|
667
|
+
observeLazyFragment(existing);
|
|
668
|
+
}
|
|
492
669
|
} else {
|
|
493
670
|
const created = {
|
|
494
671
|
instanceId,
|
|
@@ -511,6 +688,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
511
688
|
refetchIntervalInBackground,
|
|
512
689
|
intervalTimer: null,
|
|
513
690
|
activeIntervalMs: 0,
|
|
691
|
+
lazy: isLazyUnloaded,
|
|
692
|
+
lazyState: isLazyUnloaded ? 'idle' : 'loaded',
|
|
693
|
+
staleWhileUnloaded: false,
|
|
514
694
|
_rawInput: rawInput,
|
|
515
695
|
_rawDepends: rawDepends,
|
|
516
696
|
_rawStale: rawStale,
|
|
@@ -518,7 +698,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
518
698
|
};
|
|
519
699
|
instances.set(instanceId, created);
|
|
520
700
|
syncInstanceInterval(created);
|
|
521
|
-
if (
|
|
701
|
+
if (isLazyUnloaded) {
|
|
702
|
+
observeLazyFragment(created);
|
|
703
|
+
} else if (pendingRestoredInput) {
|
|
522
704
|
Promise.resolve().then(() => {
|
|
523
705
|
if (!isDisposed && created.version === 0 && instances.get(instanceId) === created) {
|
|
524
706
|
refresh(instanceId, pendingRestoredInput).catch(() => {});
|
|
@@ -558,6 +740,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
558
740
|
!seenIds.has(id) || (inst.el && inst.el.isConnected === false);
|
|
559
741
|
if (disconnected) {
|
|
560
742
|
clearInstanceInterval(inst);
|
|
743
|
+
unobserveLazyFragment(inst);
|
|
561
744
|
if (inst.abortController) {
|
|
562
745
|
try {
|
|
563
746
|
inst.abortController.abort();
|
|
@@ -603,6 +786,21 @@ function createFragmentCoordinator(options = {}) {
|
|
|
603
786
|
const html = fragmentPayload && typeof fragmentPayload.html === 'string' ? fragmentPayload.html : '';
|
|
604
787
|
if (!html) return false;
|
|
605
788
|
|
|
789
|
+
const patchKey = `${inst.instanceId}:${reqVersion}`;
|
|
790
|
+
const patchOpts = pendingPatchOptsByKey.get(patchKey) || null;
|
|
791
|
+
if (patchOpts) {
|
|
792
|
+
pendingPatchOptsByKey.delete(patchKey);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (typeof options.onBeforePatch === 'function') {
|
|
796
|
+
options.onBeforePatch(inst.el, inst);
|
|
797
|
+
}
|
|
798
|
+
if (patchOpts && typeof patchOpts.onBeforePatch === 'function') {
|
|
799
|
+
try {
|
|
800
|
+
patchOpts.onBeforePatch(inst.el, inst);
|
|
801
|
+
} catch (_e) {}
|
|
802
|
+
}
|
|
803
|
+
|
|
606
804
|
const patchStart = Date.now();
|
|
607
805
|
|
|
608
806
|
const committedInput =
|
|
@@ -629,7 +827,17 @@ function createFragmentCoordinator(options = {}) {
|
|
|
629
827
|
}
|
|
630
828
|
}
|
|
631
829
|
|
|
632
|
-
|
|
830
|
+
const patchMode = fragmentPayload.mode || (patchOpts && patchOpts.mode) || 'replace';
|
|
831
|
+
if (patchMode === 'append' || patchMode === 'prepend') {
|
|
832
|
+
patchIncrementalList(inst.el, html, {
|
|
833
|
+
mode: patchMode,
|
|
834
|
+
limit: fragmentPayload.limit !== undefined ? fragmentPayload.limit : patchOpts && patchOpts.limit,
|
|
835
|
+
target: fragmentPayload.target || (patchOpts && patchOpts.target),
|
|
836
|
+
removeKeys: fragmentPayload.removeKeys || (patchOpts && patchOpts.removeKeys),
|
|
837
|
+
onBeforeNodeRemove: options.onBeforeNodeRemove,
|
|
838
|
+
onNodeAdded: options.onNodeAdded,
|
|
839
|
+
});
|
|
840
|
+
} else if (datastar && typeof datastar.patchFragmentElement === 'function') {
|
|
633
841
|
datastar.patchFragmentElement(inst.el, html, {
|
|
634
842
|
onBeforeNodeRemove: options.onBeforeNodeRemove,
|
|
635
843
|
onNodeAdded: options.onNodeAdded,
|
|
@@ -638,6 +846,17 @@ function createFragmentCoordinator(options = {}) {
|
|
|
638
846
|
inst.el.innerHTML = html;
|
|
639
847
|
}
|
|
640
848
|
|
|
849
|
+
// Clear lazy unloaded markers once a real patch is applied
|
|
850
|
+
if (inst.lazy || inst.lazyState !== 'loaded') {
|
|
851
|
+
inst.lazy = false;
|
|
852
|
+
inst.lazyState = 'loaded';
|
|
853
|
+
inst.staleWhileUnloaded = false;
|
|
854
|
+
unobserveLazyFragment(inst);
|
|
855
|
+
if (inst.el && typeof inst.el.removeAttribute === 'function') {
|
|
856
|
+
inst.el.removeAttribute('data-w-lazy');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
641
860
|
const patchMs = Math.max(0, Date.now() - patchStart);
|
|
642
861
|
const now = Date.now();
|
|
643
862
|
inst.lastUpdatedAt = now;
|
|
@@ -653,6 +872,11 @@ function createFragmentCoordinator(options = {}) {
|
|
|
653
872
|
if (typeof options.onAfterPatch === 'function') {
|
|
654
873
|
options.onAfterPatch(inst.el, inst);
|
|
655
874
|
}
|
|
875
|
+
if (patchOpts && typeof patchOpts.onAfterPatch === 'function') {
|
|
876
|
+
try {
|
|
877
|
+
patchOpts.onAfterPatch(inst.el, inst);
|
|
878
|
+
} catch (_e) {}
|
|
879
|
+
}
|
|
656
880
|
|
|
657
881
|
if (debug) {
|
|
658
882
|
debug.recordChainPatch(chainId, {
|
|
@@ -704,6 +928,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
704
928
|
if (!parentInst || !parentInst.el) continue;
|
|
705
929
|
for (const otherInst of instances.values()) {
|
|
706
930
|
if (otherInst !== parentInst && otherInst.el && isStrictAncestor(parentInst.el, otherInst.el)) {
|
|
931
|
+
// Supersede any older in-flight refresh on the nested child so a late child response
|
|
932
|
+
// cannot overwrite the newer parent+child snapshot.
|
|
933
|
+
otherInst.version += 1;
|
|
707
934
|
if (otherInst.input && typeof otherInst.input === 'object') {
|
|
708
935
|
childInputs[otherInst.instanceId] = { ...otherInst.input };
|
|
709
936
|
if (!childInputs[otherInst.name]) {
|
|
@@ -908,8 +1135,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
908
1135
|
debug.trackRequestEnd(reqId, { status: 'completed', httpStatus: response.status });
|
|
909
1136
|
}
|
|
910
1137
|
|
|
911
|
-
lastFailedBatch = null;
|
|
912
1138
|
const rawFragments = (body && body.fragments) || {};
|
|
1139
|
+
const rawErrors = (body && body.errors && typeof body.errors === 'object' ? body.errors : {}) || {};
|
|
913
1140
|
const resultsMap = {};
|
|
914
1141
|
if (isArray(rawFragments)) {
|
|
915
1142
|
for (const item of rawFragments) {
|
|
@@ -922,12 +1149,42 @@ function createFragmentCoordinator(options = {}) {
|
|
|
922
1149
|
Object.assign(resultsMap, rawFragments);
|
|
923
1150
|
}
|
|
924
1151
|
const refreshedIds = [];
|
|
1152
|
+
const batchErrors = [];
|
|
1153
|
+
const failedTargets = [];
|
|
925
1154
|
let staleDiscardCount = 0;
|
|
926
1155
|
let totalLoadMs = 0;
|
|
927
1156
|
let totalRenderMs = 0;
|
|
928
1157
|
|
|
929
1158
|
for (const target of targets) {
|
|
930
1159
|
const inst = instances.get(target.instanceId);
|
|
1160
|
+
const fragError =
|
|
1161
|
+
rawErrors[target.instanceId] ||
|
|
1162
|
+
rawErrors[target.name] ||
|
|
1163
|
+
(resultsMap[target.instanceId] && resultsMap[target.instanceId].error) ||
|
|
1164
|
+
(resultsMap[target.name] && resultsMap[target.name].error) ||
|
|
1165
|
+
null;
|
|
1166
|
+
if (fragError) {
|
|
1167
|
+
const errObj =
|
|
1168
|
+
fragError instanceof Error
|
|
1169
|
+
? fragError
|
|
1170
|
+
: Object.assign(
|
|
1171
|
+
new Error(
|
|
1172
|
+
(fragError && typeof fragError === 'object' && fragError.message) ||
|
|
1173
|
+
String(fragError) ||
|
|
1174
|
+
`Fragment "${target.name}" failed to refresh`
|
|
1175
|
+
),
|
|
1176
|
+
{
|
|
1177
|
+
status: (fragError && fragError.status) || 500,
|
|
1178
|
+
code: (fragError && fragError.code) || 'FRAGMENT_REFRESH_ERROR',
|
|
1179
|
+
instanceId: target.instanceId,
|
|
1180
|
+
fragmentName: target.name,
|
|
1181
|
+
}
|
|
1182
|
+
);
|
|
1183
|
+
batchErrors.push(errObj);
|
|
1184
|
+
failedTargets.push(target);
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
931
1188
|
const fragResult = resultsMap[target.instanceId] || resultsMap[target.name];
|
|
932
1189
|
if (!inst || !fragResult) continue;
|
|
933
1190
|
if (target.reqVersion < inst.version) {
|
|
@@ -949,13 +1206,33 @@ function createFragmentCoordinator(options = {}) {
|
|
|
949
1206
|
}
|
|
950
1207
|
|
|
951
1208
|
const allSuperseded =
|
|
952
|
-
refreshedIds.length === 0 &&
|
|
1209
|
+
refreshedIds.length === 0 &&
|
|
1210
|
+
batchErrors.length === 0 &&
|
|
1211
|
+
staleDiscardCount > 0 &&
|
|
1212
|
+
staleDiscardCount === targets.length;
|
|
1213
|
+
|
|
1214
|
+
if (refreshedIds.length > 0 && lastFailedBatch && isArray(lastFailedBatch.targets)) {
|
|
1215
|
+
const refreshedSet = new Set(refreshedIds);
|
|
1216
|
+
const remainingFailed = lastFailedBatch.targets.filter((t) => !refreshedSet.has(t.instanceId));
|
|
1217
|
+
lastFailedBatch =
|
|
1218
|
+
remainingFailed.length > 0
|
|
1219
|
+
? { targets: remainingFailed, chainIds: lastFailedBatch.chainIds }
|
|
1220
|
+
: null;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (batchErrors.length > 0) {
|
|
1224
|
+
lastFailedBatch = {
|
|
1225
|
+
targets: failedTargets,
|
|
1226
|
+
chainIds,
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
953
1229
|
|
|
954
1230
|
for (const cid of chainIds) {
|
|
955
1231
|
if (debug) {
|
|
956
1232
|
debug.recordChainRefresh(cid, {
|
|
957
1233
|
fragmentIds: refreshedIds,
|
|
958
|
-
status: allSuperseded ? 'superseded' : 'ok',
|
|
1234
|
+
status: batchErrors.length > 0 ? (refreshedIds.length > 0 ? 'partial-error' : 'error') : allSuperseded ? 'superseded' : 'ok',
|
|
1235
|
+
error: batchErrors[0] || null,
|
|
959
1236
|
loadMs: totalLoadMs,
|
|
960
1237
|
renderMs: totalRenderMs,
|
|
961
1238
|
totalServerMs: body && isNumber(body.totalMs) ? body.totalMs : null,
|
|
@@ -977,6 +1254,20 @@ function createFragmentCoordinator(options = {}) {
|
|
|
977
1254
|
};
|
|
978
1255
|
}
|
|
979
1256
|
|
|
1257
|
+
if (batchErrors.length > 0) {
|
|
1258
|
+
return {
|
|
1259
|
+
ok: false,
|
|
1260
|
+
partial: refreshedIds.length > 0,
|
|
1261
|
+
refreshed: refreshedIds,
|
|
1262
|
+
errors: batchErrors,
|
|
1263
|
+
error: batchErrors[0],
|
|
1264
|
+
timings: {
|
|
1265
|
+
loadMs: totalLoadMs,
|
|
1266
|
+
renderMs: totalRenderMs,
|
|
1267
|
+
},
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
980
1271
|
return {
|
|
981
1272
|
ok: true,
|
|
982
1273
|
refreshed: refreshedIds,
|
|
@@ -1017,13 +1308,18 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1017
1308
|
for (const inst of instances.values()) {
|
|
1018
1309
|
let matched = false;
|
|
1019
1310
|
for (const tag of tagsToFlush) {
|
|
1020
|
-
if (inst.dependsOn.has(tag) || inst.name === tag) {
|
|
1311
|
+
if (inst.dependsOn.has(tag) || inst.name === tag || inst.instanceId === tag) {
|
|
1021
1312
|
matched = true;
|
|
1022
1313
|
break;
|
|
1023
1314
|
}
|
|
1024
1315
|
}
|
|
1025
1316
|
if (matched) {
|
|
1026
|
-
|
|
1317
|
+
if (inst.lazy && inst.lazyState === 'idle') {
|
|
1318
|
+
// Do not fetch off-screen unloaded lazy fragments on invalidation; mark stale for when visible
|
|
1319
|
+
inst.staleWhileUnloaded = true;
|
|
1320
|
+
} else {
|
|
1321
|
+
matchingInstances.push(inst);
|
|
1322
|
+
}
|
|
1027
1323
|
}
|
|
1028
1324
|
}
|
|
1029
1325
|
|
|
@@ -1179,6 +1475,29 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1179
1475
|
inst.abortController = abortController;
|
|
1180
1476
|
inst.version += 1;
|
|
1181
1477
|
|
|
1478
|
+
if (inst.lazy && inst.lazyState === 'idle') {
|
|
1479
|
+
inst.lazyState = 'loading';
|
|
1480
|
+
unobserveLazyFragment(inst);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (
|
|
1484
|
+
refreshOpts.mode ||
|
|
1485
|
+
refreshOpts.limit !== undefined ||
|
|
1486
|
+
refreshOpts.target ||
|
|
1487
|
+
refreshOpts.removeKeys ||
|
|
1488
|
+
typeof refreshOpts.onBeforePatch === 'function' ||
|
|
1489
|
+
typeof refreshOpts.onAfterPatch === 'function'
|
|
1490
|
+
) {
|
|
1491
|
+
pendingPatchOptsByKey.set(`${inst.instanceId}:${inst.version}`, {
|
|
1492
|
+
mode: refreshOpts.mode || null,
|
|
1493
|
+
limit: refreshOpts.limit,
|
|
1494
|
+
target: refreshOpts.target,
|
|
1495
|
+
removeKeys: refreshOpts.removeKeys,
|
|
1496
|
+
onBeforePatch: refreshOpts.onBeforePatch,
|
|
1497
|
+
onAfterPatch: refreshOpts.onAfterPatch,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1182
1501
|
const candidateInput =
|
|
1183
1502
|
nextInput && typeof nextInput === 'object'
|
|
1184
1503
|
? refreshOpts.replaceInput
|
|
@@ -1207,6 +1526,21 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1207
1526
|
const result = await executeBatchRefresh(targets, [], abortController.signal, refreshOpts.timeout);
|
|
1208
1527
|
clearOwnedAbortControllers();
|
|
1209
1528
|
const enrichedResult = { ok: true, ...result };
|
|
1529
|
+
if (enrichedResult.ok === false && !enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.error) {
|
|
1530
|
+
const failHandler = refreshOpts.onFailure || refreshOpts.onError;
|
|
1531
|
+
if (typeof failHandler === 'function') {
|
|
1532
|
+
await failHandler(enrichedResult.error);
|
|
1533
|
+
}
|
|
1534
|
+
if (typeof refreshOpts.onSettled === 'function') {
|
|
1535
|
+
await refreshOpts.onSettled(enrichedResult, enrichedResult.error);
|
|
1536
|
+
} else if (typeof refreshOpts.onComplete === 'function') {
|
|
1537
|
+
await refreshOpts.onComplete(enrichedResult, enrichedResult.error);
|
|
1538
|
+
}
|
|
1539
|
+
if (refreshOpts.throwOnError === true) {
|
|
1540
|
+
throw enrichedResult.error;
|
|
1541
|
+
}
|
|
1542
|
+
return enrichedResult;
|
|
1543
|
+
}
|
|
1210
1544
|
if (!enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.ok !== false) {
|
|
1211
1545
|
if (typeof refreshOpts.onSuccess === 'function') {
|
|
1212
1546
|
await refreshOpts.onSuccess(enrichedResult);
|
|
@@ -1277,6 +1611,24 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1277
1611
|
return executeBatchRefresh(retryTargets, chainIds);
|
|
1278
1612
|
}
|
|
1279
1613
|
|
|
1614
|
+
function resolveInstanceByIdOrName(idOrName) {
|
|
1615
|
+
if (!idOrName) return null;
|
|
1616
|
+
const direct = instances.get(idOrName);
|
|
1617
|
+
if (direct && (!direct.el || direct.el.isConnected !== false)) {
|
|
1618
|
+
return direct;
|
|
1619
|
+
}
|
|
1620
|
+
const defaultInst = instances.get(`${idOrName}:default`);
|
|
1621
|
+
if (defaultInst && (!defaultInst.el || defaultInst.el.isConnected !== false)) {
|
|
1622
|
+
return defaultInst;
|
|
1623
|
+
}
|
|
1624
|
+
for (const inst of instances.values()) {
|
|
1625
|
+
if (inst.name === idOrName && (!inst.el || inst.el.isConnected !== false)) {
|
|
1626
|
+
return inst;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1280
1632
|
return {
|
|
1281
1633
|
syncDomInstances,
|
|
1282
1634
|
invalidate,
|
|
@@ -1288,17 +1640,28 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1288
1640
|
scanDomInstances();
|
|
1289
1641
|
for (const item of items) {
|
|
1290
1642
|
const key = item.instanceId || item.name;
|
|
1291
|
-
const
|
|
1292
|
-
if (
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
inst._rawInput = serializedInput;
|
|
1643
|
+
const matchedList = [];
|
|
1644
|
+
if (item.instanceId && instances.has(item.instanceId)) {
|
|
1645
|
+
matchedList.push(instances.get(item.instanceId));
|
|
1646
|
+
} else {
|
|
1647
|
+
for (const inst of instances.values()) {
|
|
1648
|
+
if (inst.instanceId === key || inst.name === key) {
|
|
1649
|
+
matchedList.push(inst);
|
|
1299
1650
|
}
|
|
1300
1651
|
}
|
|
1301
|
-
|
|
1652
|
+
}
|
|
1653
|
+
if (matchedList.length > 0) {
|
|
1654
|
+
for (const inst of matchedList) {
|
|
1655
|
+
if (item.input && typeof item.input === 'object') {
|
|
1656
|
+
inst.input = { ...item.input };
|
|
1657
|
+
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
1658
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
1659
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1660
|
+
inst._rawInput = serializedInput;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
pendingTags.add(item.instanceId ? inst.instanceId : inst.name);
|
|
1664
|
+
}
|
|
1302
1665
|
} else if (key) {
|
|
1303
1666
|
pendingTags.add(String(key).trim());
|
|
1304
1667
|
}
|
|
@@ -1311,7 +1674,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1311
1674
|
const matching = [];
|
|
1312
1675
|
for (const inst of instances.values()) {
|
|
1313
1676
|
for (const t of tagSet) {
|
|
1314
|
-
if (inst.dependsOn.has(t) || inst.name === t) {
|
|
1677
|
+
if (inst.dependsOn.has(t) || inst.name === t || inst.instanceId === t) {
|
|
1315
1678
|
matching.push(inst);
|
|
1316
1679
|
break;
|
|
1317
1680
|
}
|
|
@@ -1321,17 +1684,19 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1321
1684
|
},
|
|
1322
1685
|
retryFailed,
|
|
1323
1686
|
retryLastFailed: retryFailed,
|
|
1324
|
-
getInstance(
|
|
1325
|
-
const hit =
|
|
1326
|
-
if (hit
|
|
1687
|
+
getInstance(instanceIdOrName) {
|
|
1688
|
+
const hit = resolveInstanceByIdOrName(instanceIdOrName);
|
|
1689
|
+
if (hit) {
|
|
1327
1690
|
return hit;
|
|
1328
1691
|
}
|
|
1329
1692
|
scanDomInstances();
|
|
1330
|
-
return
|
|
1693
|
+
return resolveInstanceByIdOrName(instanceIdOrName);
|
|
1331
1694
|
},
|
|
1332
|
-
getInstances() {
|
|
1695
|
+
getInstances(nameOrId) {
|
|
1333
1696
|
scanDomInstances();
|
|
1334
|
-
|
|
1697
|
+
const all = Array.from(instances.values());
|
|
1698
|
+
if (!nameOrId) return all;
|
|
1699
|
+
return all.filter((inst) => inst.instanceId === nameOrId || inst.name === nameOrId);
|
|
1335
1700
|
},
|
|
1336
1701
|
getStoredInput(instanceIdOrName, storageType = 'session') {
|
|
1337
1702
|
const store = storageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
@@ -1368,6 +1733,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
1368
1733
|
}
|
|
1369
1734
|
pendingAutoInstanceIds.clear();
|
|
1370
1735
|
pendingAutoReasons.clear();
|
|
1736
|
+
pendingPatchOptsByKey.clear();
|
|
1737
|
+
if (lazyObserver && typeof lazyObserver.disconnect === 'function') {
|
|
1738
|
+
try {
|
|
1739
|
+
lazyObserver.disconnect();
|
|
1740
|
+
} catch (_e) {}
|
|
1741
|
+
lazyObserver = null;
|
|
1742
|
+
}
|
|
1371
1743
|
removeLifecycleListeners();
|
|
1372
1744
|
for (const inst of instances.values()) {
|
|
1373
1745
|
clearInstanceInterval(inst);
|
package/core/client/index.js
CHANGED
|
@@ -192,9 +192,21 @@ function createClientRuntime(options = {}) {
|
|
|
192
192
|
controllers.mountSubtree(addedEl);
|
|
193
193
|
}
|
|
194
194
|
},
|
|
195
|
-
|
|
195
|
+
onBeforePatch(patchedEl, inst) {
|
|
196
|
+
if (controllers) {
|
|
197
|
+
controllers.notifyBeforePatch(patchedEl, {
|
|
198
|
+
fragmentId: inst && inst.instanceId ? inst.instanceId : null,
|
|
199
|
+
fragmentName: inst && inst.name ? inst.name : null,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
onAfterPatch(patchedEl, inst) {
|
|
196
204
|
if (controllers) {
|
|
197
205
|
controllers.mountSubtree(patchedEl);
|
|
206
|
+
controllers.notifyAfterPatch(patchedEl, {
|
|
207
|
+
fragmentId: inst && inst.instanceId ? inst.instanceId : null,
|
|
208
|
+
fragmentName: inst && inst.name ? inst.name : null,
|
|
209
|
+
});
|
|
198
210
|
}
|
|
199
211
|
},
|
|
200
212
|
});
|