webspresso 0.0.110 → 0.0.111
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 +73 -23
- package/core/client/api.js +23 -21
- package/core/client/controller.js +288 -30
- package/core/client/debug.js +12 -12
- package/core/client/devtool.js +124 -19
- package/core/client/forms.js +39 -19
- package/core/client/fragments.js +433 -95
- package/core/client/host.js +365 -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 +99 -21
- package/core/client/services.js +588 -0
- package/core/client/utils.js +188 -2
- package/index.d.ts +322 -28
- 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,7 +59,7 @@ 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
|
}
|
|
@@ -108,9 +109,24 @@ function createFragmentCoordinator(options = {}) {
|
|
|
108
109
|
const endpoint = options.endpoint || DEFAULT_FRAGMENT_ENDPOINT;
|
|
109
110
|
const datastar = options.datastar || options.adapter;
|
|
110
111
|
const debug = options.debug || null;
|
|
111
|
-
const defaultTimeout =
|
|
112
|
-
|
|
113
|
-
//
|
|
112
|
+
const defaultTimeout = isNumber(options.timeout) ? options.timeout : 15000;
|
|
113
|
+
|
|
114
|
+
// React Query-style automatic revalidation defaults
|
|
115
|
+
const defaultStaleTime = parseDurationMs(options.staleTime, 0);
|
|
116
|
+
const defaultRefetchOnWindowFocus = parseRefetchMode(
|
|
117
|
+
options.refetchOnWindowFocus !== undefined ? options.refetchOnWindowFocus : options.refetchOnFocus,
|
|
118
|
+
false
|
|
119
|
+
);
|
|
120
|
+
const defaultRefetchOnReconnect = parseRefetchMode(options.refetchOnReconnect, false);
|
|
121
|
+
const defaultRefetchOnMount = parseRefetchMode(
|
|
122
|
+
options.refetchOnMount !== undefined ? options.refetchOnMount : options.refetchOnVisit,
|
|
123
|
+
false
|
|
124
|
+
);
|
|
125
|
+
const defaultRefetchInterval = parseDurationMs(options.refetchInterval, 0);
|
|
126
|
+
const defaultRefetchIntervalInBackground =
|
|
127
|
+
parseRefetchMode(options.refetchIntervalInBackground, false) === true;
|
|
128
|
+
|
|
129
|
+
// Map<instanceId, { instanceId, name, pagePath, input, dependsOn: Set<string>, version: number, el: Element, abortController: AbortController|null, ... }>
|
|
114
130
|
const instances = new Map();
|
|
115
131
|
|
|
116
132
|
// Coalescing & in-flight state for tag invalidations
|
|
@@ -122,91 +138,378 @@ function createFragmentCoordinator(options = {}) {
|
|
|
122
138
|
let lastFailedBatch = null;
|
|
123
139
|
const inflightBatchWaiters = [];
|
|
124
140
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
// Automatic refetch (focus / reconnect / mount / interval) state
|
|
142
|
+
const pendingAutoInstanceIds = new Set();
|
|
143
|
+
let autoBatchScheduled = false;
|
|
144
|
+
let focusDebounceTimer = null;
|
|
145
|
+
let isDisposed = false;
|
|
146
|
+
let lifecycleListenersAttached = false;
|
|
147
|
+
|
|
148
|
+
function isDocumentHidden() {
|
|
149
|
+
const root = getRoot();
|
|
150
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
151
|
+
return Boolean(doc && doc.visibilityState === 'hidden');
|
|
152
|
+
}
|
|
131
153
|
|
|
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
|
-
let effectiveInput = input;
|
|
169
|
-
if (isInputPersistEnabled) {
|
|
170
|
-
const store = inputStorageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
171
|
-
if (!el.hasAttribute('data-w-fragment-input') || Object.keys(effectiveInput).length === 0) {
|
|
172
|
-
const stored = store.get(inputStorageKey, null);
|
|
173
|
-
if (stored && typeof stored === 'object' && !Array.isArray(stored)) {
|
|
174
|
-
effectiveInput = stored;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
154
|
+
function isInstanceStale(inst, mode) {
|
|
155
|
+
if (!inst || !mode) return false;
|
|
156
|
+
if (mode === 'always') return true;
|
|
157
|
+
const staleMs = isNumber(inst.staleTime) ? inst.staleTime : 0;
|
|
158
|
+
if (staleMs <= 0) return true;
|
|
159
|
+
const elapsed = Date.now() - (inst.lastUpdatedAt || 0);
|
|
160
|
+
return elapsed >= staleMs;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function clearInstanceInterval(inst) {
|
|
164
|
+
if (inst && inst.intervalTimer) {
|
|
165
|
+
clearInterval(inst.intervalTimer);
|
|
166
|
+
inst.intervalTimer = null;
|
|
167
|
+
inst.activeIntervalMs = 0;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function syncInstanceInterval(inst) {
|
|
172
|
+
if (!inst || isDisposed) return;
|
|
173
|
+
const targetMs = isNumber(inst.refetchInterval) && inst.refetchInterval > 0 ? inst.refetchInterval : 0;
|
|
174
|
+
if (targetMs <= 0) {
|
|
175
|
+
clearInstanceInterval(inst);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (inst.intervalTimer && inst.activeIntervalMs === targetMs) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
clearInstanceInterval(inst);
|
|
182
|
+
inst.activeIntervalMs = targetMs;
|
|
183
|
+
inst.intervalTimer = setInterval(() => {
|
|
184
|
+
if (isDisposed || !inst.el || inst.el.isConnected === false) {
|
|
185
|
+
clearInstanceInterval(inst);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (!inst.refetchIntervalInBackground && isDocumentHidden()) {
|
|
189
|
+
return;
|
|
177
190
|
}
|
|
191
|
+
scheduleAutoRefresh([inst.instanceId], 'interval');
|
|
192
|
+
}, targetMs);
|
|
193
|
+
if (inst.intervalTimer && typeof inst.intervalTimer.unref === 'function') {
|
|
194
|
+
inst.intervalTimer.unref();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
178
197
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
198
|
+
function scheduleAutoRefresh(instanceIds, reason = 'auto') {
|
|
199
|
+
if (isDisposed || !isArray(instanceIds) || instanceIds.length === 0) return;
|
|
200
|
+
for (const id of instanceIds) {
|
|
201
|
+
if (id) pendingAutoInstanceIds.add(id);
|
|
202
|
+
}
|
|
203
|
+
if (autoBatchScheduled || pendingAutoInstanceIds.size === 0) return;
|
|
204
|
+
autoBatchScheduled = true;
|
|
205
|
+
|
|
206
|
+
Promise.resolve().then(async () => {
|
|
207
|
+
autoBatchScheduled = false;
|
|
208
|
+
if (isDisposed || pendingAutoInstanceIds.size === 0) return;
|
|
209
|
+
const idsToRefresh = Array.from(pendingAutoInstanceIds);
|
|
210
|
+
pendingAutoInstanceIds.clear();
|
|
211
|
+
|
|
212
|
+
// Skip background auto-refresh if a mutation invalidation batch is currently in flight
|
|
213
|
+
if (inFlightBatchCount > 0) return;
|
|
214
|
+
|
|
215
|
+
const candidates = [];
|
|
216
|
+
for (const id of idsToRefresh) {
|
|
217
|
+
const inst = instances.get(id);
|
|
218
|
+
if (inst && inst.el && inst.el.isConnected !== false && !inst.abortController) {
|
|
219
|
+
candidates.push(inst);
|
|
187
220
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
221
|
+
}
|
|
222
|
+
if (candidates.length === 0) return;
|
|
223
|
+
|
|
224
|
+
const deduped = deduplicateParentChildFragments(candidates);
|
|
225
|
+
const targets = deduped.map((inst) => {
|
|
226
|
+
inst.version += 1;
|
|
227
|
+
return {
|
|
228
|
+
instanceId: inst.instanceId,
|
|
229
|
+
name: inst.name,
|
|
230
|
+
pagePath: inst.pagePath,
|
|
231
|
+
input: { ...inst.input },
|
|
232
|
+
reqVersion: inst.version,
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (debug && typeof debug.recordEvent === 'function') {
|
|
237
|
+
debug.recordEvent('fragment:auto-refetch', {
|
|
238
|
+
reason,
|
|
239
|
+
fragments: targets.map((t) => t.instanceId),
|
|
200
240
|
});
|
|
201
241
|
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
await executeBatchRefresh(targets);
|
|
245
|
+
} catch (_err) {
|
|
246
|
+
// Background auto-refetch errors are non-fatal
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function triggerFocusRefetch() {
|
|
252
|
+
if (isDisposed || isDocumentHidden()) return;
|
|
253
|
+
if (focusDebounceTimer) clearTimeout(focusDebounceTimer);
|
|
254
|
+
focusDebounceTimer = setTimeout(() => {
|
|
255
|
+
focusDebounceTimer = null;
|
|
256
|
+
if (isDisposed || isDocumentHidden()) return;
|
|
257
|
+
scanDomInstances();
|
|
258
|
+
const matchedIds = [];
|
|
259
|
+
for (const inst of instances.values()) {
|
|
260
|
+
if (isInstanceStale(inst, inst.refetchOnWindowFocus)) {
|
|
261
|
+
matchedIds.push(inst.instanceId);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (matchedIds.length > 0) {
|
|
265
|
+
scheduleAutoRefresh(matchedIds, 'window-focus');
|
|
266
|
+
}
|
|
267
|
+
}, 20);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function triggerReconnectRefetch() {
|
|
271
|
+
if (isDisposed) return;
|
|
272
|
+
scanDomInstances();
|
|
273
|
+
const matchedIds = [];
|
|
274
|
+
for (const inst of instances.values()) {
|
|
275
|
+
if (isInstanceStale(inst, inst.refetchOnReconnect)) {
|
|
276
|
+
matchedIds.push(inst.instanceId);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (matchedIds.length > 0) {
|
|
280
|
+
scheduleAutoRefresh(matchedIds, 'reconnect');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function onVisibilityChange() {
|
|
285
|
+
if (!isDocumentHidden()) {
|
|
286
|
+
triggerFocusRefetch();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function onPageShow(event) {
|
|
291
|
+
if (isDisposed) return;
|
|
292
|
+
if (event && event.persisted) {
|
|
293
|
+
scanDomInstances();
|
|
294
|
+
const matchedIds = [];
|
|
295
|
+
for (const inst of instances.values()) {
|
|
296
|
+
const mode = inst.refetchOnMount || inst.refetchOnWindowFocus;
|
|
297
|
+
if (isInstanceStale(inst, mode)) {
|
|
298
|
+
matchedIds.push(inst.instanceId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (matchedIds.length > 0) {
|
|
302
|
+
scheduleAutoRefresh(matchedIds, 'pageshow');
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function ensureLifecycleListeners() {
|
|
308
|
+
if (lifecycleListenersAttached || isDisposed) return;
|
|
309
|
+
const root = getRoot();
|
|
310
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
311
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
312
|
+
|
|
313
|
+
if (doc && typeof doc.addEventListener === 'function') {
|
|
314
|
+
doc.addEventListener('visibilitychange', onVisibilityChange);
|
|
315
|
+
}
|
|
316
|
+
if (win && typeof win.addEventListener === 'function') {
|
|
317
|
+
win.addEventListener('focus', triggerFocusRefetch);
|
|
318
|
+
win.addEventListener('online', triggerReconnectRefetch);
|
|
319
|
+
win.addEventListener('pageshow', onPageShow);
|
|
320
|
+
}
|
|
321
|
+
lifecycleListenersAttached = true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function removeLifecycleListeners() {
|
|
325
|
+
if (!lifecycleListenersAttached) return;
|
|
326
|
+
const root = getRoot();
|
|
327
|
+
const doc = root && root.nodeType === 9 ? root : root && root.ownerDocument ? root.ownerDocument : typeof document !== 'undefined' ? document : null;
|
|
328
|
+
const win = (doc && doc.defaultView) || (typeof window !== 'undefined' ? window : null);
|
|
329
|
+
|
|
330
|
+
if (doc && typeof doc.removeEventListener === 'function') {
|
|
331
|
+
doc.removeEventListener('visibilitychange', onVisibilityChange);
|
|
332
|
+
}
|
|
333
|
+
if (win && typeof win.removeEventListener === 'function') {
|
|
334
|
+
win.removeEventListener('focus', triggerFocusRefetch);
|
|
335
|
+
win.removeEventListener('online', triggerReconnectRefetch);
|
|
336
|
+
win.removeEventListener('pageshow', onPageShow);
|
|
337
|
+
}
|
|
338
|
+
lifecycleListenersAttached = false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function syncSingleElement(el, index, seenIds) {
|
|
342
|
+
const name = (el.getAttribute('data-w-fragment') || '').trim();
|
|
343
|
+
if (!name) return;
|
|
344
|
+
|
|
345
|
+
let instanceId = (el.getAttribute('data-w-fragment-id') || '').trim();
|
|
346
|
+
if (!instanceId) {
|
|
347
|
+
instanceId = index === 0 ? `${name}:default` : `${name}:${index}`;
|
|
348
|
+
el.setAttribute('data-w-fragment-id', instanceId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (seenIds) seenIds.add(instanceId);
|
|
352
|
+
|
|
353
|
+
const existing = instances.get(instanceId);
|
|
354
|
+
const rawInput = el.getAttribute('data-w-fragment-input');
|
|
355
|
+
const rawDepends = el.getAttribute('data-w-depends-on');
|
|
356
|
+
const rawStale = el.getAttribute('data-w-stale-time');
|
|
357
|
+
const rawInterval = el.getAttribute('data-w-refetch-interval');
|
|
358
|
+
|
|
359
|
+
// Fast-path: if the same DOM element is already registered and its reactive attributes are unchanged,
|
|
360
|
+
// skip JSON.parse, Set allocation, storage lookup, and timer reconfiguration.
|
|
361
|
+
if (
|
|
362
|
+
existing &&
|
|
363
|
+
existing.el === el &&
|
|
364
|
+
existing.name === name &&
|
|
365
|
+
existing._rawInput === rawInput &&
|
|
366
|
+
existing._rawDepends === rawDepends &&
|
|
367
|
+
existing._rawStale === rawStale &&
|
|
368
|
+
existing._rawInterval === rawInterval
|
|
369
|
+
) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const pagePath =
|
|
374
|
+
(el.getAttribute('data-w-fragment-page') || '').trim() ||
|
|
375
|
+
(typeof window !== 'undefined' && window.location ? window.location.pathname : '/');
|
|
376
|
+
const input =
|
|
377
|
+
existing && existing.el === el && existing._rawInput === rawInput
|
|
378
|
+
? existing.input
|
|
379
|
+
: parseFragmentInputAttr(rawInput);
|
|
380
|
+
const dependsOn =
|
|
381
|
+
existing && existing.el === el && existing._rawDepends === rawDepends
|
|
382
|
+
? existing.dependsOn
|
|
383
|
+
: new Set(parseDependsOnAttr(rawDepends));
|
|
384
|
+
|
|
385
|
+
// Fragment input persistence setup
|
|
386
|
+
const rawPersistInput =
|
|
387
|
+
el.getAttribute('data-w-persist-input') ||
|
|
388
|
+
(el.getAttribute('data-w-persist') === 'input' ? 'session' : null);
|
|
389
|
+
const isInputPersistEnabled = Boolean(rawPersistInput && rawPersistInput !== 'false');
|
|
390
|
+
const inputStorageType = rawPersistInput && rawPersistInput.toLowerCase() === 'local' ? 'local' : 'session';
|
|
391
|
+
const inputStorageKey =
|
|
392
|
+
el.getAttribute('data-w-persist-key') ||
|
|
393
|
+
el.getAttribute('data-w-input-key') ||
|
|
394
|
+
`frag_input:${name}`;
|
|
395
|
+
|
|
396
|
+
let effectiveInput = input;
|
|
397
|
+
if (isInputPersistEnabled) {
|
|
398
|
+
const store = inputStorageType === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
399
|
+
if (rawInput === null || Object.keys(effectiveInput).length === 0) {
|
|
400
|
+
const stored = store.get(inputStorageKey, null);
|
|
401
|
+
if (stored && typeof stored === 'object' && !isArray(stored)) {
|
|
402
|
+
effectiveInput = stored;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Parse React Query-style automatic revalidation attributes
|
|
408
|
+
const staleTime = parseDurationMs(rawStale, defaultStaleTime);
|
|
409
|
+
const rawFocusAttr =
|
|
410
|
+
el.getAttribute('data-w-refetch-on-focus') ??
|
|
411
|
+
el.getAttribute('data-w-refetch-on-window-focus');
|
|
412
|
+
const refetchOnWindowFocus = parseRefetchMode(rawFocusAttr, defaultRefetchOnWindowFocus);
|
|
413
|
+
const refetchOnReconnect = parseRefetchMode(
|
|
414
|
+
el.getAttribute('data-w-refetch-on-reconnect'),
|
|
415
|
+
defaultRefetchOnReconnect
|
|
416
|
+
);
|
|
417
|
+
const rawMountAttr =
|
|
418
|
+
el.getAttribute('data-w-refetch-on-mount') ??
|
|
419
|
+
el.getAttribute('data-w-refetch-on-visit');
|
|
420
|
+
const refetchOnMount = parseRefetchMode(rawMountAttr, defaultRefetchOnMount);
|
|
421
|
+
const refetchInterval = parseDurationMs(rawInterval, defaultRefetchInterval);
|
|
422
|
+
const refetchIntervalInBackground =
|
|
423
|
+
parseRefetchMode(
|
|
424
|
+
el.getAttribute('data-w-refetch-in-background'),
|
|
425
|
+
defaultRefetchIntervalInBackground
|
|
426
|
+
) === true;
|
|
427
|
+
|
|
428
|
+
if (refetchOnWindowFocus || refetchOnReconnect || refetchOnMount) {
|
|
429
|
+
ensureLifecycleListeners();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (existing) {
|
|
433
|
+
existing.name = name;
|
|
434
|
+
existing.pagePath = pagePath;
|
|
435
|
+
existing.el = el;
|
|
436
|
+
existing.dependsOn = dependsOn;
|
|
437
|
+
if (rawInput !== null || isInputPersistEnabled) {
|
|
438
|
+
existing.input = effectiveInput;
|
|
439
|
+
}
|
|
440
|
+
existing.persistInput = isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null;
|
|
441
|
+
existing.staleTime = staleTime;
|
|
442
|
+
existing.refetchOnWindowFocus = refetchOnWindowFocus;
|
|
443
|
+
existing.refetchOnReconnect = refetchOnReconnect;
|
|
444
|
+
existing.refetchOnMount = refetchOnMount;
|
|
445
|
+
existing.refetchInterval = refetchInterval;
|
|
446
|
+
existing.refetchIntervalInBackground = refetchIntervalInBackground;
|
|
447
|
+
existing._rawInput = rawInput;
|
|
448
|
+
existing._rawDepends = rawDepends;
|
|
449
|
+
existing._rawStale = rawStale;
|
|
450
|
+
existing._rawInterval = rawInterval;
|
|
451
|
+
syncInstanceInterval(existing);
|
|
452
|
+
} else {
|
|
453
|
+
const created = {
|
|
454
|
+
instanceId,
|
|
455
|
+
name,
|
|
456
|
+
pagePath,
|
|
457
|
+
input: effectiveInput,
|
|
458
|
+
dependsOn,
|
|
459
|
+
version: 0,
|
|
460
|
+
el,
|
|
461
|
+
abortController: null,
|
|
462
|
+
persistInput: isInputPersistEnabled ? { type: inputStorageType, key: inputStorageKey } : null,
|
|
463
|
+
lastUpdatedAt: Date.now(),
|
|
464
|
+
staleTime,
|
|
465
|
+
refetchOnWindowFocus,
|
|
466
|
+
refetchOnReconnect,
|
|
467
|
+
refetchOnMount,
|
|
468
|
+
refetchInterval,
|
|
469
|
+
refetchIntervalInBackground,
|
|
470
|
+
intervalTimer: null,
|
|
471
|
+
activeIntervalMs: 0,
|
|
472
|
+
_rawInput: rawInput,
|
|
473
|
+
_rawDepends: rawDepends,
|
|
474
|
+
_rawStale: rawStale,
|
|
475
|
+
_rawInterval: rawInterval,
|
|
476
|
+
};
|
|
477
|
+
instances.set(instanceId, created);
|
|
478
|
+
syncInstanceInterval(created);
|
|
479
|
+
if (refetchOnMount === 'always' || (refetchOnMount === true && staleTime <= 0)) {
|
|
480
|
+
scheduleAutoRefresh([instanceId], 'mount');
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Internal DOM scan without allocating a return array.
|
|
487
|
+
*/
|
|
488
|
+
function scanDomInstances(containerEl) {
|
|
489
|
+
const defaultRoot = getRoot();
|
|
490
|
+
const root = containerEl || defaultRoot;
|
|
491
|
+
if (!root || typeof root.querySelectorAll !== 'function') return;
|
|
492
|
+
|
|
493
|
+
const isFullScan = !containerEl || containerEl === defaultRoot;
|
|
494
|
+
const seenIds = isFullScan ? new Set() : null;
|
|
495
|
+
const nodeList = root.querySelectorAll('[data-w-fragment]');
|
|
496
|
+
let idx = 0;
|
|
497
|
+
|
|
498
|
+
if (root.nodeType === 1 && root.hasAttribute && root.hasAttribute('data-w-fragment')) {
|
|
499
|
+
syncSingleElement(root, idx++, seenIds);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
for (let i = 0, len = nodeList.length; i < len; i++) {
|
|
503
|
+
syncSingleElement(nodeList[i], idx++, seenIds);
|
|
202
504
|
}
|
|
203
505
|
|
|
204
506
|
// Prune disconnected instances if scanning full root
|
|
205
|
-
if (
|
|
507
|
+
if (isFullScan) {
|
|
206
508
|
for (const [id, inst] of instances.entries()) {
|
|
207
509
|
const disconnected =
|
|
208
510
|
!seenIds.has(id) || (inst.el && inst.el.isConnected === false);
|
|
209
511
|
if (disconnected) {
|
|
512
|
+
clearInstanceInterval(inst);
|
|
210
513
|
if (inst.abortController) {
|
|
211
514
|
try {
|
|
212
515
|
inst.abortController.abort();
|
|
@@ -218,7 +521,13 @@ function createFragmentCoordinator(options = {}) {
|
|
|
218
521
|
}
|
|
219
522
|
}
|
|
220
523
|
}
|
|
524
|
+
}
|
|
221
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Scan DOM and synchronize tracked `[data-w-fragment]` instances.
|
|
528
|
+
*/
|
|
529
|
+
function syncDomInstances(containerEl) {
|
|
530
|
+
scanDomInstances(containerEl);
|
|
222
531
|
return Array.from(instances.values());
|
|
223
532
|
}
|
|
224
533
|
|
|
@@ -255,7 +564,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
255
564
|
pendingTags.size === 0
|
|
256
565
|
) {
|
|
257
566
|
inst.input = fragmentPayload.input;
|
|
258
|
-
|
|
567
|
+
const serializedInput = JSON.stringify(fragmentPayload.input);
|
|
568
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
569
|
+
inst._rawInput = serializedInput;
|
|
259
570
|
if (inst.persistInput) {
|
|
260
571
|
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
261
572
|
store.set(inst.persistInput.key, fragmentPayload.input);
|
|
@@ -272,9 +583,10 @@ function createFragmentCoordinator(options = {}) {
|
|
|
272
583
|
}
|
|
273
584
|
|
|
274
585
|
const patchMs = Math.max(0, Date.now() - patchStart);
|
|
586
|
+
inst.lastUpdatedAt = Date.now();
|
|
275
587
|
|
|
276
588
|
// Re-sync nested fragment instances that may have been updated inside this parent fragment
|
|
277
|
-
|
|
589
|
+
scanDomInstances(inst.el);
|
|
278
590
|
|
|
279
591
|
if (typeof options.onAfterPatch === 'function') {
|
|
280
592
|
options.onAfterPatch(inst.el, inst);
|
|
@@ -331,7 +643,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
331
643
|
})),
|
|
332
644
|
};
|
|
333
645
|
|
|
334
|
-
const reqId =
|
|
646
|
+
const reqId = uid('frag_req', 4);
|
|
335
647
|
if (debug) {
|
|
336
648
|
debug.trackRequestStart({
|
|
337
649
|
requestId: reqId,
|
|
@@ -346,7 +658,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
346
658
|
const batchAbortController = new AbortController();
|
|
347
659
|
let timeoutTimer = null;
|
|
348
660
|
let timedOut = false;
|
|
349
|
-
const timeoutMs =
|
|
661
|
+
const timeoutMs = isNumber(batchTimeout) ? batchTimeout : defaultTimeout;
|
|
350
662
|
|
|
351
663
|
if (timeoutMs > 0) {
|
|
352
664
|
timeoutTimer = setTimeout(() => {
|
|
@@ -498,8 +810,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
498
810
|
const inst = instances.get(target.instanceId);
|
|
499
811
|
const fragResult = resultsMap[target.instanceId] || resultsMap[target.name];
|
|
500
812
|
if (!inst || !fragResult) continue;
|
|
501
|
-
if (
|
|
502
|
-
if (
|
|
813
|
+
if (isNumber(fragResult.loadMs)) totalLoadMs += fragResult.loadMs;
|
|
814
|
+
if (isNumber(fragResult.renderMs)) totalRenderMs += fragResult.renderMs;
|
|
503
815
|
|
|
504
816
|
const applied = applyFragmentPatch(
|
|
505
817
|
inst,
|
|
@@ -519,7 +831,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
519
831
|
status: 'ok',
|
|
520
832
|
loadMs: totalLoadMs,
|
|
521
833
|
renderMs: totalRenderMs,
|
|
522
|
-
totalServerMs: body &&
|
|
834
|
+
totalServerMs: body && isNumber(body.totalMs) ? body.totalMs : null,
|
|
523
835
|
});
|
|
524
836
|
}
|
|
525
837
|
}
|
|
@@ -557,7 +869,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
557
869
|
pendingTags.clear();
|
|
558
870
|
pendingChains.length = 0;
|
|
559
871
|
|
|
560
|
-
|
|
872
|
+
scanDomInstances();
|
|
561
873
|
|
|
562
874
|
// Find all mounted fragment instances whose `dependsOn` intersects `tagsToFlush`
|
|
563
875
|
const matchingInstances = [];
|
|
@@ -636,7 +948,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
636
948
|
* @returns {Promise<{ refreshed: string[], skipped?: boolean }>}
|
|
637
949
|
*/
|
|
638
950
|
function invalidate(tags, meta = {}) {
|
|
639
|
-
const tagList =
|
|
951
|
+
const tagList = isArray(tags) ? tags : [tags];
|
|
640
952
|
for (const raw of tagList) {
|
|
641
953
|
if (typeof raw === 'string' && raw.trim()) {
|
|
642
954
|
pendingTags.add(raw.trim());
|
|
@@ -685,7 +997,7 @@ function createFragmentCoordinator(options = {}) {
|
|
|
685
997
|
* @param {object} [refreshOpts]
|
|
686
998
|
*/
|
|
687
999
|
async function refresh(idOrName, nextInput, refreshOpts = {}) {
|
|
688
|
-
|
|
1000
|
+
scanDomInstances();
|
|
689
1001
|
|
|
690
1002
|
const matched = [];
|
|
691
1003
|
if (instances.has(idOrName)) {
|
|
@@ -731,7 +1043,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
731
1043
|
? { ...nextInput }
|
|
732
1044
|
: { ...inst.input, ...nextInput };
|
|
733
1045
|
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
734
|
-
|
|
1046
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
1047
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1048
|
+
inst._rawInput = serializedInput;
|
|
735
1049
|
}
|
|
736
1050
|
if (inst.persistInput) {
|
|
737
1051
|
const store = inst.persistInput.type === 'local' ? defaultStorage.local : defaultStorage.session;
|
|
@@ -825,10 +1139,10 @@ function createFragmentCoordinator(options = {}) {
|
|
|
825
1139
|
invalidate,
|
|
826
1140
|
refresh,
|
|
827
1141
|
refreshFragments(items = []) {
|
|
828
|
-
if (!
|
|
1142
|
+
if (!isArray(items) || items.length === 0) {
|
|
829
1143
|
return Promise.resolve({ refreshed: [], skipped: true });
|
|
830
1144
|
}
|
|
831
|
-
|
|
1145
|
+
scanDomInstances();
|
|
832
1146
|
for (const item of items) {
|
|
833
1147
|
const key = item.instanceId || item.name;
|
|
834
1148
|
const inst = instances.get(key) || Array.from(instances.values()).find((i) => i.name === key);
|
|
@@ -836,7 +1150,9 @@ function createFragmentCoordinator(options = {}) {
|
|
|
836
1150
|
if (item.input && typeof item.input === 'object') {
|
|
837
1151
|
inst.input = { ...item.input };
|
|
838
1152
|
if (inst.el && typeof inst.el.setAttribute === 'function') {
|
|
839
|
-
|
|
1153
|
+
const serializedInput = JSON.stringify(inst.input);
|
|
1154
|
+
inst.el.setAttribute('data-w-fragment-input', serializedInput);
|
|
1155
|
+
inst._rawInput = serializedInput;
|
|
840
1156
|
}
|
|
841
1157
|
}
|
|
842
1158
|
pendingTags.add(inst.name);
|
|
@@ -847,8 +1163,8 @@ function createFragmentCoordinator(options = {}) {
|
|
|
847
1163
|
return flushInvalidations();
|
|
848
1164
|
},
|
|
849
1165
|
findFragmentsByTags(tags = []) {
|
|
850
|
-
|
|
851
|
-
const tagSet = new Set((
|
|
1166
|
+
scanDomInstances();
|
|
1167
|
+
const tagSet = new Set((isArray(tags) ? tags : [tags]).map((t) => String(t || '').trim()).filter(Boolean));
|
|
852
1168
|
const matching = [];
|
|
853
1169
|
for (const inst of instances.values()) {
|
|
854
1170
|
for (const t of tagSet) {
|
|
@@ -863,11 +1179,15 @@ function createFragmentCoordinator(options = {}) {
|
|
|
863
1179
|
retryFailed,
|
|
864
1180
|
retryLastFailed: retryFailed,
|
|
865
1181
|
getInstance(instanceId) {
|
|
866
|
-
|
|
1182
|
+
const hit = instances.get(instanceId);
|
|
1183
|
+
if (hit && (!hit.el || hit.el.isConnected !== false)) {
|
|
1184
|
+
return hit;
|
|
1185
|
+
}
|
|
1186
|
+
scanDomInstances();
|
|
867
1187
|
return instances.get(instanceId) || null;
|
|
868
1188
|
},
|
|
869
1189
|
getInstances() {
|
|
870
|
-
|
|
1190
|
+
scanDomInstances();
|
|
871
1191
|
return Array.from(instances.values());
|
|
872
1192
|
},
|
|
873
1193
|
getStoredInput(instanceIdOrName, storageType = 'session') {
|
|
@@ -881,6 +1201,24 @@ function createFragmentCoordinator(options = {}) {
|
|
|
881
1201
|
get hasFailedRefresh() {
|
|
882
1202
|
return Boolean(lastFailedBatch);
|
|
883
1203
|
},
|
|
1204
|
+
dispose() {
|
|
1205
|
+
isDisposed = true;
|
|
1206
|
+
if (focusDebounceTimer) {
|
|
1207
|
+
clearTimeout(focusDebounceTimer);
|
|
1208
|
+
focusDebounceTimer = null;
|
|
1209
|
+
}
|
|
1210
|
+
pendingAutoInstanceIds.clear();
|
|
1211
|
+
removeLifecycleListeners();
|
|
1212
|
+
for (const inst of instances.values()) {
|
|
1213
|
+
clearInstanceInterval(inst);
|
|
1214
|
+
if (inst.abortController) {
|
|
1215
|
+
try {
|
|
1216
|
+
inst.abortController.abort();
|
|
1217
|
+
} catch (_e) {}
|
|
1218
|
+
inst.abortController = null;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
884
1222
|
};
|
|
885
1223
|
}
|
|
886
1224
|
|