webspresso 0.0.110 → 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 +190 -51
- package/core/client/api.js +23 -21
- package/core/client/controller.js +407 -40
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +163 -29
- package/core/client/fragments.js +616 -118
- package/core/client/host.js +369 -0
- package/core/client/index.d.ts +18 -0
- package/core/client/index.js +154 -23
- package/core/client/lite.js +101 -4
- package/core/client/server-fragments.js +142 -22
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +334 -29
- package/index.js +3 -0
- package/package.json +1 -1
- package/src/bundle/index.js +175 -14
- package/src/dev/banner.js +4 -1
- package/src/dev-reload/index.js +29 -1
- package/src/server.js +28 -0
package/core/client/fragments.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { defaultStorage } = require('./storage');
|
|
4
|
+
const { isArray, isNumber, parseDurationMs, parseRefetchMode, uid } = require('./utils');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Webspresso Client-Side SSR Fragment Coordinator & Invalidation Engine
|
|
@@ -33,7 +34,7 @@ function parseDependsOnAttr(raw) {
|
|
|
33
34
|
if (trimmed.startsWith('[')) {
|
|
34
35
|
try {
|
|
35
36
|
const parsed = JSON.parse(trimmed);
|
|
36
|
-
if (
|
|
37
|
+
if (isArray(parsed)) {
|
|
37
38
|
return parsed.map((s) => String(s).trim()).filter(Boolean);
|
|
38
39
|
}
|
|
39
40
|
} catch (_e) {
|
|
@@ -58,12 +59,22 @@ function parseFragmentInputAttr(raw) {
|
|
|
58
59
|
if (!trimmed) return {};
|
|
59
60
|
try {
|
|
60
61
|
const parsed = JSON.parse(trimmed);
|
|
61
|
-
return parsed && typeof parsed === 'object' && !
|
|
62
|
+
return parsed && typeof parsed === 'object' && !isArray(parsed) ? parsed : {};
|
|
62
63
|
} catch (_e) {
|
|
63
64
|
return {};
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
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
|
+
|
|
67
78
|
/**
|
|
68
79
|
* Filter a list of fragment records so that if fragment B's DOM element is inside
|
|
69
80
|
* fragment A's DOM subtree, B is omitted from the batch because refreshing A will
|
|
@@ -78,7 +89,7 @@ function deduplicateParentChildFragments(candidates) {
|
|
|
78
89
|
if (!candidate.el) return true;
|
|
79
90
|
for (const other of candidates) {
|
|
80
91
|
if (other === candidate || !other.el) continue;
|
|
81
|
-
if (other.el
|
|
92
|
+
if (isStrictAncestor(other.el, candidate.el)) {
|
|
82
93
|
return false;
|
|
83
94
|
}
|
|
84
95
|
}
|
|
@@ -108,9 +119,24 @@ function createFragmentCoordinator(options = {}) {
|
|
|
108
119
|
const endpoint = options.endpoint || DEFAULT_FRAGMENT_ENDPOINT;
|
|
109
120
|
const datastar = options.datastar || options.adapter;
|
|
110
121
|
const debug = options.debug || null;
|
|
111
|
-
const defaultTimeout =
|
|
112
|
-
|
|
113
|
-
//
|
|
122
|
+
const defaultTimeout = isNumber(options.timeout) ? options.timeout : 15000;
|
|
123
|
+
|
|
124
|
+
// React Query-style automatic revalidation defaults
|
|
125
|
+
const defaultStaleTime = parseDurationMs(options.staleTime, 0);
|
|
126
|
+
const defaultRefetchOnWindowFocus = parseRefetchMode(
|
|
127
|
+
options.refetchOnWindowFocus !== undefined ? options.refetchOnWindowFocus : options.refetchOnFocus,
|
|
128
|
+
false
|
|
129
|
+
);
|
|
130
|
+
const defaultRefetchOnReconnect = parseRefetchMode(options.refetchOnReconnect, false);
|
|
131
|
+
const defaultRefetchOnMount = parseRefetchMode(
|
|
132
|
+
options.refetchOnMount !== undefined ? options.refetchOnMount : options.refetchOnVisit,
|
|
133
|
+
false
|
|
134
|
+
);
|
|
135
|
+
const defaultRefetchInterval = parseDurationMs(options.refetchInterval, 0);
|
|
136
|
+
const defaultRefetchIntervalInBackground =
|
|
137
|
+
parseRefetchMode(options.refetchIntervalInBackground, false) === true;
|
|
138
|
+
|
|
139
|
+
// Map<instanceId, { instanceId, name, pagePath, input, dependsOn: Set<string>, version: number, el: Element, abortController: AbortController|null, ... }>
|
|
114
140
|
const instances = new Map();
|
|
115
141
|
|
|
116
142
|
// Coalescing & in-flight state for tag invalidations
|
|
@@ -122,91 +148,416 @@ function createFragmentCoordinator(options = {}) {
|
|
|
122
148
|
let lastFailedBatch = null;
|
|
123
149
|
const inflightBatchWaiters = [];
|
|
124
150
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
151
|
+
// Automatic refetch (focus / reconnect / mount / interval) state
|
|
152
|
+
const pendingAutoInstanceIds = new Set();
|
|
153
|
+
const pendingAutoReasons = new Set();
|
|
154
|
+
let autoBatchScheduled = false;
|
|
155
|
+
let autoBatchTimer = null;
|
|
156
|
+
let focusDebounceTimer = null;
|
|
157
|
+
let isDisposed = false;
|
|
158
|
+
let lifecycleListenersAttached = false;
|
|
159
|
+
|
|
160
|
+
function isDocumentHidden() {
|
|
161
|
+
const root = getRoot();
|
|
162
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
163
|
+
return Boolean(doc && doc.visibilityState === 'hidden');
|
|
164
|
+
}
|
|
131
165
|
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
166
|
+
function isInstanceStale(inst, mode) {
|
|
167
|
+
if (!inst || !mode) return false;
|
|
168
|
+
if (mode === 'always') return true;
|
|
169
|
+
const staleMs = isNumber(inst.staleTime) ? inst.staleTime : 0;
|
|
170
|
+
if (staleMs <= 0) return true;
|
|
171
|
+
const elapsed = Date.now() - (inst.lastUpdatedAt || 0);
|
|
172
|
+
return elapsed >= staleMs;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clearInstanceInterval(inst) {
|
|
176
|
+
if (inst && inst.intervalTimer) {
|
|
177
|
+
clearInterval(inst.intervalTimer);
|
|
178
|
+
inst.intervalTimer = null;
|
|
179
|
+
inst.activeIntervalMs = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function syncInstanceInterval(inst) {
|
|
184
|
+
if (!inst || isDisposed) return;
|
|
185
|
+
const targetMs = isNumber(inst.refetchInterval) && inst.refetchInterval > 0 ? inst.refetchInterval : 0;
|
|
186
|
+
if (targetMs <= 0) {
|
|
187
|
+
clearInstanceInterval(inst);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (inst.intervalTimer && inst.activeIntervalMs === targetMs) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
clearInstanceInterval(inst);
|
|
194
|
+
inst.activeIntervalMs = targetMs;
|
|
195
|
+
inst.intervalTimer = setInterval(() => {
|
|
196
|
+
if (isDisposed || !inst.el || inst.el.isConnected === false) {
|
|
197
|
+
clearInstanceInterval(inst);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (!inst.refetchIntervalInBackground && isDocumentHidden()) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
scheduleAutoRefresh([inst.instanceId], 'interval');
|
|
204
|
+
}, targetMs);
|
|
205
|
+
if (inst.intervalTimer && typeof inst.intervalTimer.unref === 'function') {
|
|
206
|
+
inst.intervalTimer.unref();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function flushAutoRefresh() {
|
|
211
|
+
autoBatchScheduled = false;
|
|
212
|
+
if (autoBatchTimer) {
|
|
213
|
+
clearTimeout(autoBatchTimer);
|
|
214
|
+
autoBatchTimer = null;
|
|
215
|
+
}
|
|
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;
|
|
239
|
+
|
|
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
|
+
});
|
|
251
|
+
|
|
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
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await executeBatchRefresh(targets);
|
|
262
|
+
} catch (_err) {
|
|
263
|
+
// Background auto-refetch errors are non-fatal
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
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;
|
|
274
|
+
|
|
275
|
+
if (reason === 'mount') {
|
|
276
|
+
if (!autoBatchScheduled) {
|
|
277
|
+
autoBatchScheduled = true;
|
|
278
|
+
Promise.resolve().then(flushAutoRefresh);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (autoBatchTimer || autoBatchScheduled) return;
|
|
284
|
+
autoBatchTimer = setTimeout(flushAutoRefresh, 5);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function triggerFocusRefetch() {
|
|
288
|
+
if (isDisposed || isDocumentHidden()) return;
|
|
289
|
+
scanDomInstances();
|
|
290
|
+
const matchedIds = [];
|
|
291
|
+
for (const inst of instances.values()) {
|
|
292
|
+
if (isInstanceStale(inst, inst.refetchOnWindowFocus)) {
|
|
293
|
+
matchedIds.push(inst.instanceId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (matchedIds.length > 0) {
|
|
297
|
+
scheduleAutoRefresh(matchedIds, 'window-focus');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function triggerReconnectRefetch() {
|
|
302
|
+
if (isDisposed) return;
|
|
303
|
+
scanDomInstances();
|
|
304
|
+
const matchedIds = [];
|
|
305
|
+
for (const inst of instances.values()) {
|
|
306
|
+
if (isInstanceStale(inst, inst.refetchOnReconnect)) {
|
|
307
|
+
matchedIds.push(inst.instanceId);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (matchedIds.length > 0) {
|
|
311
|
+
scheduleAutoRefresh(matchedIds, 'reconnect');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function onVisibilityChange() {
|
|
316
|
+
if (!isDocumentHidden()) {
|
|
317
|
+
triggerFocusRefetch();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function onPageShow(event) {
|
|
322
|
+
if (isDisposed) return;
|
|
323
|
+
if (event && event.persisted) {
|
|
324
|
+
scanDomInstances();
|
|
325
|
+
const matchedIds = [];
|
|
326
|
+
for (const inst of instances.values()) {
|
|
327
|
+
const mode = inst.refetchOnMount || inst.refetchOnWindowFocus;
|
|
328
|
+
if (isInstanceStale(inst, mode)) {
|
|
329
|
+
matchedIds.push(inst.instanceId);
|
|
176
330
|
}
|
|
177
331
|
}
|
|
332
|
+
if (matchedIds.length > 0) {
|
|
333
|
+
scheduleAutoRefresh(matchedIds, 'pageshow');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function ensureLifecycleListeners() {
|
|
339
|
+
if (lifecycleListenersAttached || isDisposed) return;
|
|
340
|
+
const root = getRoot();
|
|
341
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
342
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
343
|
+
|
|
344
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
345
|
+
doc.addEventListener('visibilitychange', onVisibilityChange);
|
|
346
|
+
}
|
|
347
|
+
if (win && typeof win.addEventListener === 'function') {
|
|
348
|
+
win.addEventListener('focus', triggerFocusRefetch);
|
|
349
|
+
win.addEventListener('online', triggerReconnectRefetch);
|
|
350
|
+
win.addEventListener('pageshow', onPageShow);
|
|
351
|
+
}
|
|
352
|
+
lifecycleListenersAttached = true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function removeLifecycleListeners() {
|
|
356
|
+
if (!lifecycleListenersAttached) return;
|
|
357
|
+
const root = getRoot();
|
|
358
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
359
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
360
|
+
|
|
361
|
+
if (doc && typeof doc.removeEventListener === 'function') {
|
|
362
|
+
doc.removeEventListener('visibilitychange', onVisibilityChange);
|
|
363
|
+
}
|
|
364
|
+
if (win && typeof win.removeEventListener === 'function') {
|
|
365
|
+
win.removeEventListener('focus', triggerFocusRefetch);
|
|
366
|
+
win.removeEventListener('online', triggerReconnectRefetch);
|
|
367
|
+
win.removeEventListener('pageshow', onPageShow);
|
|
368
|
+
}
|
|
369
|
+
lifecycleListenersAttached = false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function syncSingleElement(el, index, seenIds) {
|
|
373
|
+
const name = (el.getAttribute('data-w-fragment') || '').trim();
|
|
374
|
+
if (!name) return;
|
|
375
|
+
|
|
376
|
+
let instanceId = (el.getAttribute('data-w-fragment-id') || '').trim();
|
|
377
|
+
if (!instanceId) {
|
|
378
|
+
instanceId = index === 0 ? `${name}:default` : `${name}:${index}`;
|
|
379
|
+
el.setAttribute('data-w-fragment-id', instanceId);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (seenIds) seenIds.add(instanceId);
|
|
383
|
+
|
|
384
|
+
const existing = instances.get(instanceId);
|
|
385
|
+
const rawInput = el.getAttribute('data-w-fragment-input');
|
|
386
|
+
const rawDepends = el.getAttribute('data-w-depends-on');
|
|
387
|
+
const rawStale = el.getAttribute('data-w-stale-time');
|
|
388
|
+
const rawInterval = el.getAttribute('data-w-refetch-interval');
|
|
389
|
+
|
|
390
|
+
// Fast-path: if the same DOM element is already registered and its reactive attributes are unchanged,
|
|
391
|
+
// skip JSON.parse, Set allocation, storage lookup, and timer reconfiguration.
|
|
392
|
+
if (
|
|
393
|
+
existing &&
|
|
394
|
+
existing.el === el &&
|
|
395
|
+
existing.name === name &&
|
|
396
|
+
existing._rawInput === rawInput &&
|
|
397
|
+
existing._rawDepends === rawDepends &&
|
|
398
|
+
existing._rawStale === rawStale &&
|
|
399
|
+
existing._rawInterval === rawInterval
|
|
400
|
+
) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
178
403
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
existing.
|
|
185
|
-
|
|
186
|
-
|
|
404
|
+
const pagePath =
|
|
405
|
+
(el.getAttribute('data-w-fragment-page') || '').trim() ||
|
|
406
|
+
(typeof window !== 'undefined' && window.location ? window.location.pathname : '/');
|
|
407
|
+
const input =
|
|
408
|
+
existing && existing.el === el && existing._rawInput === rawInput
|
|
409
|
+
? existing.input
|
|
410
|
+
: parseFragmentInputAttr(rawInput);
|
|
411
|
+
const dependsOn =
|
|
412
|
+
existing && existing.el === el && existing._rawDepends === rawDepends
|
|
413
|
+
? existing.dependsOn
|
|
414
|
+
: new Set(parseDependsOnAttr(rawDepends));
|
|
415
|
+
|
|
416
|
+
// Fragment input persistence setup
|
|
417
|
+
const rawPersistInput =
|
|
418
|
+
el.getAttribute('data-w-persist-input') ||
|
|
419
|
+
(el.getAttribute('data-w-persist') === 'input' ? 'session' : null);
|
|
420
|
+
const isInputPersistEnabled = Boolean(rawPersistInput && rawPersistInput !== 'false');
|
|
421
|
+
const inputStorageType = rawPersistInput && rawPersistInput.toLowerCase() === 'local' ? 'local' : 'session';
|
|
422
|
+
const inputStorageKey =
|
|
423
|
+
el.getAttribute('data-w-persist-key') ||
|
|
424
|
+
el.getAttribute('data-w-input-key') ||
|
|
425
|
+
`frag_input:${name}`;
|
|
426
|
+
|
|
427
|
+
let effectiveInput = input;
|
|
428
|
+
let pendingRestoredInput = null;
|
|
429
|
+
if (!existing && isInputPersistEnabled) {
|
|
430
|
+
const store = inputStorageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
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 };
|
|
187
443
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Parse React Query-style automatic revalidation attributes
|
|
448
|
+
const staleTime = parseDurationMs(rawStale, defaultStaleTime);
|
|
449
|
+
const rawFocusAttr =
|
|
450
|
+
el.getAttribute('data-w-refetch-on-focus') ??
|
|
451
|
+
el.getAttribute('data-w-refetch-on-window-focus');
|
|
452
|
+
const refetchOnWindowFocus = parseRefetchMode(rawFocusAttr, defaultRefetchOnWindowFocus);
|
|
453
|
+
const refetchOnReconnect = parseRefetchMode(
|
|
454
|
+
el.getAttribute('data-w-refetch-on-reconnect'),
|
|
455
|
+
defaultRefetchOnReconnect
|
|
456
|
+
);
|
|
457
|
+
const rawMountAttr =
|
|
458
|
+
el.getAttribute('data-w-refetch-on-mount') ??
|
|
459
|
+
el.getAttribute('data-w-refetch-on-visit');
|
|
460
|
+
const refetchOnMount = parseRefetchMode(rawMountAttr, defaultRefetchOnMount);
|
|
461
|
+
const refetchInterval = parseDurationMs(rawInterval, defaultRefetchInterval);
|
|
462
|
+
const refetchIntervalInBackground =
|
|
463
|
+
parseRefetchMode(
|
|
464
|
+
el.getAttribute('data-w-refetch-in-background'),
|
|
465
|
+
defaultRefetchIntervalInBackground
|
|
466
|
+
) === true;
|
|
467
|
+
|
|
468
|
+
if (refetchOnWindowFocus || refetchOnReconnect || refetchOnMount) {
|
|
469
|
+
ensureLifecycleListeners();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (existing) {
|
|
473
|
+
existing.name = name;
|
|
474
|
+
existing.pagePath = pagePath;
|
|
475
|
+
existing.el = el;
|
|
476
|
+
existing.dependsOn = dependsOn;
|
|
477
|
+
if (rawInput !== null && rawInput !== existing._rawInput) {
|
|
478
|
+
existing.input = input;
|
|
479
|
+
}
|
|
480
|
+
existing.persistInput = isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null;
|
|
481
|
+
existing.staleTime = staleTime;
|
|
482
|
+
existing.refetchOnWindowFocus = refetchOnWindowFocus;
|
|
483
|
+
existing.refetchOnReconnect = refetchOnReconnect;
|
|
484
|
+
existing.refetchOnMount = refetchOnMount;
|
|
485
|
+
existing.refetchInterval = refetchInterval;
|
|
486
|
+
existing.refetchIntervalInBackground = refetchIntervalInBackground;
|
|
487
|
+
existing._rawInput = rawInput;
|
|
488
|
+
existing._rawDepends = rawDepends;
|
|
489
|
+
existing._rawStale = rawStale;
|
|
490
|
+
existing._rawInterval = rawInterval;
|
|
491
|
+
syncInstanceInterval(existing);
|
|
492
|
+
} else {
|
|
493
|
+
const created = {
|
|
494
|
+
instanceId,
|
|
495
|
+
name,
|
|
496
|
+
pagePath,
|
|
497
|
+
input: effectiveInput,
|
|
498
|
+
restoredInput: pendingRestoredInput,
|
|
499
|
+
dependsOn,
|
|
500
|
+
version: 0,
|
|
501
|
+
el,
|
|
502
|
+
abortController: null,
|
|
503
|
+
inFlightCount: 0,
|
|
504
|
+
persistInput: isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null,
|
|
505
|
+
lastUpdatedAt: Date.now(),
|
|
506
|
+
staleTime,
|
|
507
|
+
refetchOnWindowFocus,
|
|
508
|
+
refetchOnReconnect,
|
|
509
|
+
refetchOnMount,
|
|
510
|
+
refetchInterval,
|
|
511
|
+
refetchIntervalInBackground,
|
|
512
|
+
intervalTimer: null,
|
|
513
|
+
activeIntervalMs: 0,
|
|
514
|
+
_rawInput: rawInput,
|
|
515
|
+
_rawDepends: rawDepends,
|
|
516
|
+
_rawStale: rawStale,
|
|
517
|
+
_rawInterval: rawInterval,
|
|
518
|
+
};
|
|
519
|
+
instances.set(instanceId, created);
|
|
520
|
+
syncInstanceInterval(created);
|
|
521
|
+
if (pendingRestoredInput) {
|
|
522
|
+
Promise.resolve().then(() => {
|
|
523
|
+
if (!isDisposed && created.version === 0 && instances.get(instanceId) === created) {
|
|
524
|
+
refresh(instanceId, pendingRestoredInput).catch(() => {});
|
|
525
|
+
}
|
|
200
526
|
});
|
|
527
|
+
} else if (refetchOnMount === 'always' || (refetchOnMount === true && staleTime <= 0)) {
|
|
528
|
+
scheduleAutoRefresh([instanceId], 'mount');
|
|
201
529
|
}
|
|
202
530
|
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Internal DOM scan without allocating a return array.
|
|
535
|
+
*/
|
|
536
|
+
function scanDomInstances(containerEl) {
|
|
537
|
+
const defaultRoot = getRoot();
|
|
538
|
+
const root = containerEl || defaultRoot;
|
|
539
|
+
if (!root || typeof root.querySelectorAll !== 'function') return;
|
|
540
|
+
|
|
541
|
+
const isFullScan = !containerEl || containerEl === defaultRoot;
|
|
542
|
+
const seenIds = isFullScan ? new Set() : null;
|
|
543
|
+
const nodeList = root.querySelectorAll('[data-w-fragment]');
|
|
544
|
+
let idx = 0;
|
|
545
|
+
|
|
546
|
+
if (root.nodeType === 1 && root.hasAttribute && root.hasAttribute('data-w-fragment')) {
|
|
547
|
+
syncSingleElement(root, idx++, seenIds);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
for (let i = 0, len = nodeList.length; i < len; i++) {
|
|
551
|
+
syncSingleElement(nodeList[i], idx++, seenIds);
|
|
552
|
+
}
|
|
203
553
|
|
|
204
554
|
// Prune disconnected instances if scanning full root
|
|
205
|
-
if (
|
|
555
|
+
if (isFullScan) {
|
|
206
556
|
for (const [id, inst] of instances.entries()) {
|
|
207
557
|
const disconnected =
|
|
208
558
|
!seenIds.has(id) || (inst.el && inst.el.isConnected === false);
|
|
209
559
|
if (disconnected) {
|
|
560
|
+
clearInstanceInterval(inst);
|
|
210
561
|
if (inst.abortController) {
|
|
211
562
|
try {
|
|
212
563
|
inst.abortController.abort();
|
|
@@ -218,7 +569,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
218
569
|
}
|
|
219
570
|
}
|
|
220
571
|
}
|
|
572
|
+
}
|
|
221
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Scan DOM and synchronize tracked `[data-w-fragment]` instances.
|
|
576
|
+
*/
|
|
577
|
+
function syncDomInstances(containerEl) {
|
|
578
|
+
scanDomInstances(containerEl);
|
|
222
579
|
return Array.from(instances.values());
|
|
223
580
|
}
|
|
224
581
|
|
|
@@ -226,7 +583,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
226
583
|
* Apply a single server fragment result `{ instanceId, name, html, input, loadMs, renderMs }`
|
|
227
584
|
* to the DOM if and only if `reqVersion >= inst.version`.
|
|
228
585
|
*/
|
|
229
|
-
function applyFragmentPatch(inst, fragmentPayload, reqVersion, chainId = null) {
|
|
586
|
+
function applyFragmentPatch(inst, fragmentPayload, reqVersion, chainId = null, requestedInput = null) {
|
|
230
587
|
if (!inst || !inst.el) return false;
|
|
231
588
|
|
|
232
589
|
if (reqVersion < inst.version) {
|
|
@@ -248,17 +605,27 @@ function createFragmentCoordinator(options = {}) {
|
|
|
248
605
|
|
|
249
606
|
const patchStart = Date.now();
|
|
250
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
|
+
|
|
251
615
|
if (
|
|
252
|
-
|
|
253
|
-
typeof fragmentPayload.input === 'object' &&
|
|
616
|
+
committedInput &&
|
|
254
617
|
!followUpNeeded &&
|
|
255
618
|
pendingTags.size === 0
|
|
256
619
|
) {
|
|
257
|
-
inst.input =
|
|
258
|
-
|
|
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
|
+
}
|
|
625
|
+
inst._rawInput = serializedInput;
|
|
259
626
|
if (inst.persistInput) {
|
|
260
627
|
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
261
|
-
store.set(inst.persistInput.key,
|
|
628
|
+
store.set(inst.persistInput.key, inst.input);
|
|
262
629
|
}
|
|
263
630
|
}
|
|
264
631
|
|
|
@@ -272,9 +639,16 @@ function createFragmentCoordinator(options = {}) {
|
|
|
272
639
|
}
|
|
273
640
|
|
|
274
641
|
const patchMs = Math.max(0, Date.now() - patchStart);
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
inst.lastUpdatedAt = now;
|
|
275
644
|
|
|
276
645
|
// Re-sync nested fragment instances that may have been updated inside this parent fragment
|
|
277
|
-
|
|
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
|
+
}
|
|
278
652
|
|
|
279
653
|
if (typeof options.onAfterPatch === 'function') {
|
|
280
654
|
options.onAfterPatch(inst.el, inst);
|
|
@@ -321,6 +695,26 @@ function createFragmentCoordinator(options = {}) {
|
|
|
321
695
|
targets[0].pagePath ||
|
|
322
696
|
(typeof window !== 'undefined' && window.location ? window.location.pathname : '/');
|
|
323
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
|
+
|
|
324
718
|
const requestPayload = {
|
|
325
719
|
pagePath,
|
|
326
720
|
fragments: targets.map((t) => ({
|
|
@@ -329,9 +723,32 @@ function createFragmentCoordinator(options = {}) {
|
|
|
329
723
|
input: t.input || {},
|
|
330
724
|
version: t.reqVersion,
|
|
331
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
|
+
}
|
|
332
749
|
};
|
|
333
750
|
|
|
334
|
-
const reqId =
|
|
751
|
+
const reqId = uid('frag_req', 4);
|
|
335
752
|
if (debug) {
|
|
336
753
|
debug.trackRequestStart({
|
|
337
754
|
requestId: reqId,
|
|
@@ -346,7 +763,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
346
763
|
const batchAbortController = new AbortController();
|
|
347
764
|
let timeoutTimer = null;
|
|
348
765
|
let timedOut = false;
|
|
349
|
-
const timeoutMs =
|
|
766
|
+
const timeoutMs = isNumber(batchTimeout) ? batchTimeout : defaultTimeout;
|
|
350
767
|
|
|
351
768
|
if (timeoutMs > 0) {
|
|
352
769
|
timeoutTimer = setTimeout(() => {
|
|
@@ -374,6 +791,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
374
791
|
signal: batchAbortController.signal,
|
|
375
792
|
});
|
|
376
793
|
} catch (err) {
|
|
794
|
+
decrementTargetBusy();
|
|
377
795
|
if (timeoutTimer) {
|
|
378
796
|
clearTimeout(timeoutTimer);
|
|
379
797
|
timeoutTimer = null;
|
|
@@ -454,6 +872,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
454
872
|
}
|
|
455
873
|
} catch (_e) {
|
|
456
874
|
body = null;
|
|
875
|
+
} finally {
|
|
876
|
+
decrementTargetBusy();
|
|
457
877
|
}
|
|
458
878
|
|
|
459
879
|
if (!response.ok) {
|
|
@@ -489,8 +909,20 @@ function createFragmentCoordinator(options = {}) {
|
|
|
489
909
|
}
|
|
490
910
|
|
|
491
911
|
lastFailedBatch = null;
|
|
492
|
-
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
|
+
}
|
|
493
924
|
const refreshedIds = [];
|
|
925
|
+
let staleDiscardCount = 0;
|
|
494
926
|
let totalLoadMs = 0;
|
|
495
927
|
let totalRenderMs = 0;
|
|
496
928
|
|
|
@@ -498,32 +930,53 @@ function createFragmentCoordinator(options = {}) {
|
|
|
498
930
|
const inst = instances.get(target.instanceId);
|
|
499
931
|
const fragResult = resultsMap[target.instanceId] || resultsMap[target.name];
|
|
500
932
|
if (!inst || !fragResult) continue;
|
|
501
|
-
if (
|
|
502
|
-
|
|
933
|
+
if (target.reqVersion < inst.version) {
|
|
934
|
+
staleDiscardCount += 1;
|
|
935
|
+
}
|
|
936
|
+
if (isNumber(fragResult.loadMs)) totalLoadMs += fragResult.loadMs;
|
|
937
|
+
if (isNumber(fragResult.renderMs)) totalRenderMs += fragResult.renderMs;
|
|
503
938
|
|
|
504
939
|
const applied = applyFragmentPatch(
|
|
505
940
|
inst,
|
|
506
941
|
fragResult,
|
|
507
942
|
target.reqVersion,
|
|
508
|
-
chainIds[0] || null
|
|
943
|
+
chainIds[0] || null,
|
|
944
|
+
target.input
|
|
509
945
|
);
|
|
510
946
|
if (applied) {
|
|
511
947
|
refreshedIds.push(target.instanceId);
|
|
512
948
|
}
|
|
513
949
|
}
|
|
514
950
|
|
|
951
|
+
const allSuperseded =
|
|
952
|
+
refreshedIds.length === 0 && staleDiscardCount > 0 && staleDiscardCount === targets.length;
|
|
953
|
+
|
|
515
954
|
for (const cid of chainIds) {
|
|
516
955
|
if (debug) {
|
|
517
956
|
debug.recordChainRefresh(cid, {
|
|
518
957
|
fragmentIds: refreshedIds,
|
|
519
|
-
status: 'ok',
|
|
958
|
+
status: allSuperseded ? 'superseded' : 'ok',
|
|
520
959
|
loadMs: totalLoadMs,
|
|
521
960
|
renderMs: totalRenderMs,
|
|
522
|
-
totalServerMs: body &&
|
|
961
|
+
totalServerMs: body && isNumber(body.totalMs) ? body.totalMs : null,
|
|
523
962
|
});
|
|
524
963
|
}
|
|
525
964
|
}
|
|
526
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
|
+
|
|
527
980
|
return {
|
|
528
981
|
ok: true,
|
|
529
982
|
refreshed: refreshedIds,
|
|
@@ -557,7 +1010,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
557
1010
|
pendingTags.clear();
|
|
558
1011
|
pendingChains.length = 0;
|
|
559
1012
|
|
|
560
|
-
|
|
1013
|
+
scanDomInstances();
|
|
561
1014
|
|
|
562
1015
|
// Find all mounted fragment instances whose `dependsOn` intersects `tagsToFlush`
|
|
563
1016
|
const matchingInstances = [];
|
|
@@ -636,7 +1089,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
636
1089
|
* @returns {Promise<{ refreshed: string[], skipped?: boolean }>}
|
|
637
1090
|
*/
|
|
638
1091
|
function invalidate(tags, meta = {}) {
|
|
639
|
-
const tagList =
|
|
1092
|
+
const tagList = isArray(tags) ? tags : [tags];
|
|
640
1093
|
for (const raw of tagList) {
|
|
641
1094
|
if (typeof raw === 'string' && raw.trim()) {
|
|
642
1095
|
pendingTags.add(raw.trim());
|
|
@@ -685,7 +1138,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
685
1138
|
* @param {object} [refreshOpts]
|
|
686
1139
|
*/
|
|
687
1140
|
async function refresh(idOrName, nextInput, refreshOpts = {}) {
|
|
688
|
-
|
|
1141
|
+
scanDomInstances();
|
|
689
1142
|
|
|
690
1143
|
const matched = [];
|
|
691
1144
|
if (instances.has(idOrName)) {
|
|
@@ -726,32 +1179,35 @@ function createFragmentCoordinator(options = {}) {
|
|
|
726
1179
|
inst.abortController = abortController;
|
|
727
1180
|
inst.version += 1;
|
|
728
1181
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
?
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
inst.
|
|
735
|
-
}
|
|
736
|
-
if (inst.persistInput) {
|
|
737
|
-
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
738
|
-
store.set(inst.persistInput.key, inst.input);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
1182
|
+
const candidateInput =
|
|
1183
|
+
nextInput && typeof nextInput === 'object'
|
|
1184
|
+
? refreshOpts.replaceInput
|
|
1185
|
+
? { ...nextInput }
|
|
1186
|
+
: { ...inst.input, ...nextInput }
|
|
1187
|
+
: { ...inst.input };
|
|
741
1188
|
|
|
742
1189
|
return {
|
|
743
1190
|
instanceId: inst.instanceId,
|
|
744
1191
|
name: inst.name,
|
|
745
1192
|
pagePath: inst.pagePath,
|
|
746
|
-
input:
|
|
1193
|
+
input: candidateInput,
|
|
747
1194
|
reqVersion: inst.version,
|
|
748
1195
|
};
|
|
749
1196
|
});
|
|
750
1197
|
|
|
1198
|
+
const clearOwnedAbortControllers = () => {
|
|
1199
|
+
for (const inst of deduped) {
|
|
1200
|
+
if (inst.abortController === abortController) {
|
|
1201
|
+
inst.abortController = null;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
751
1206
|
try {
|
|
752
1207
|
const result = await executeBatchRefresh(targets, [], abortController.signal, refreshOpts.timeout);
|
|
1208
|
+
clearOwnedAbortControllers();
|
|
753
1209
|
const enrichedResult = { ok: true, ...result };
|
|
754
|
-
if (!enrichedResult.canceled && enrichedResult.ok !== false) {
|
|
1210
|
+
if (!enrichedResult.canceled && !enrichedResult.superseded && enrichedResult.ok !== false) {
|
|
755
1211
|
if (typeof refreshOpts.onSuccess === 'function') {
|
|
756
1212
|
await refreshOpts.onSuccess(enrichedResult);
|
|
757
1213
|
}
|
|
@@ -763,6 +1219,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
763
1219
|
}
|
|
764
1220
|
return enrichedResult;
|
|
765
1221
|
} catch (err) {
|
|
1222
|
+
clearOwnedAbortControllers();
|
|
766
1223
|
const failHandler = refreshOpts.onFailure || refreshOpts.onError;
|
|
767
1224
|
if (typeof failHandler === 'function') {
|
|
768
1225
|
try {
|
|
@@ -807,7 +1264,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
807
1264
|
inst.version += 1;
|
|
808
1265
|
retryTargets.push({
|
|
809
1266
|
...t,
|
|
810
|
-
input: { ...inst.input },
|
|
1267
|
+
input: t.input ? { ...t.input } : { ...inst.input },
|
|
811
1268
|
reqVersion: inst.version,
|
|
812
1269
|
});
|
|
813
1270
|
}
|
|
@@ -825,10 +1282,10 @@ function createFragmentCoordinator(options = {}) {
|
|
|
825
1282
|
invalidate,
|
|
826
1283
|
refresh,
|
|
827
1284
|
refreshFragments(items = []) {
|
|
828
|
-
if (!
|
|
1285
|
+
if (!isArray(items) || items.length === 0) {
|
|
829
1286
|
return Promise.resolve({ refreshed: [], skipped: true });
|
|
830
1287
|
}
|
|
831
|
-
|
|
1288
|
+
scanDomInstances();
|
|
832
1289
|
for (const item of items) {
|
|
833
1290
|
const key = item.instanceId || item.name;
|
|
834
1291
|
const inst = instances.get(key) || Array.from(instances.values()).find((i) => i.name === key);
|
|
@@ -836,7 +1293,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
836
1293
|
if (item.input && typeof item.input === 'object') {
|
|
837
1294
|
inst.input = { ...item.input };
|
|
838
1295
|
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
839
|
-
|
|
1296
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
1297
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1298
|
+
inst._rawInput = serializedInput;
|
|
840
1299
|
}
|
|
841
1300
|
}
|
|
842
1301
|
pendingTags.add(inst.name);
|
|
@@ -847,8 +1306,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
847
1306
|
return flushInvalidations();
|
|
848
1307
|
},
|
|
849
1308
|
findFragmentsByTags(tags = []) {
|
|
850
|
-
|
|
851
|
-
const tagSet = new Set((
|
|
1309
|
+
scanDomInstances();
|
|
1310
|
+
const tagSet = new Set((isArray(tags) ? tags : [tags]).map((t) => String(t || '').trim()).filter(Boolean));
|
|
852
1311
|
const matching = [];
|
|
853
1312
|
for (const inst of instances.values()) {
|
|
854
1313
|
for (const t of tagSet) {
|
|
@@ -863,24 +1322,63 @@ function createFragmentCoordinator(options = {}) {
|
|
|
863
1322
|
retryFailed,
|
|
864
1323
|
retryLastFailed: retryFailed,
|
|
865
1324
|
getInstance(instanceId) {
|
|
866
|
-
|
|
1325
|
+
const hit = instances.get(instanceId);
|
|
1326
|
+
if (hit && (!hit.el || hit.el.isConnected !== false)) {
|
|
1327
|
+
return hit;
|
|
1328
|
+
}
|
|
1329
|
+
scanDomInstances();
|
|
867
1330
|
return instances.get(instanceId) || null;
|
|
868
1331
|
},
|
|
869
1332
|
getInstances() {
|
|
870
|
-
|
|
1333
|
+
scanDomInstances();
|
|
871
1334
|
return Array.from(instances.values());
|
|
872
1335
|
},
|
|
873
1336
|
getStoredInput(instanceIdOrName, storageType = 'session') {
|
|
874
1337
|
const store = storageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
875
|
-
|
|
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;
|
|
876
1346
|
},
|
|
877
1347
|
clearStoredInput(instanceIdOrName, storageType = 'session') {
|
|
878
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
|
+
}
|
|
879
1354
|
return store.remove(`frag_input:${instanceIdOrName}`);
|
|
880
1355
|
},
|
|
881
1356
|
get hasFailedRefresh() {
|
|
882
1357
|
return Boolean(lastFailedBatch);
|
|
883
1358
|
},
|
|
1359
|
+
dispose() {
|
|
1360
|
+
isDisposed = true;
|
|
1361
|
+
if (focusDebounceTimer) {
|
|
1362
|
+
clearTimeout(focusDebounceTimer);
|
|
1363
|
+
focusDebounceTimer = null;
|
|
1364
|
+
}
|
|
1365
|
+
if (autoBatchTimer) {
|
|
1366
|
+
clearTimeout(autoBatchTimer);
|
|
1367
|
+
autoBatchTimer = null;
|
|
1368
|
+
}
|
|
1369
|
+
pendingAutoInstanceIds.clear();
|
|
1370
|
+
pendingAutoReasons.clear();
|
|
1371
|
+
removeLifecycleListeners();
|
|
1372
|
+
for (const inst of instances.values()) {
|
|
1373
|
+
clearInstanceInterval(inst);
|
|
1374
|
+
if (inst.abortController) {
|
|
1375
|
+
try {
|
|
1376
|
+
inst.abortController.abort();
|
|
1377
|
+
} catch (_e) {}
|
|
1378
|
+
inst.abortController = null;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
},
|
|
884
1382
|
};
|
|
885
1383
|
}
|
|
886
1384
|
|