mnfst 0.5.198-next.2 → 0.5.198-next.4
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/lib/manifest.bindings.js +31 -0
- package/lib/manifest.data.js +211 -66
- package/lib/manifest.defer.js +171 -17
- package/lib/manifest.integrity.json +6 -5
- package/lib/manifest.js +1 -0
- package/lib/manifest.router.js +109 -93
- package/lib/manifest.virtual.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* Manifest Bindings */
|
|
2
|
+
|
|
3
|
+
(function () {
|
|
4
|
+
|
|
5
|
+
// x-text: same as Alpine's, but never rewrites an unchanged string (each write is a DOM mutation)
|
|
6
|
+
function initializeBindingsPlugin() {
|
|
7
|
+
Alpine.directive('text', (el, { expression }, { effect, evaluateLater }) => {
|
|
8
|
+
const evaluate = evaluateLater(expression);
|
|
9
|
+
effect(() => {
|
|
10
|
+
evaluate((value) => {
|
|
11
|
+
const next = value == null ? '' : String(value);
|
|
12
|
+
if (el.textContent === next) return;
|
|
13
|
+
Alpine.mutateDom(() => { el.textContent = next; });
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let bindingsPluginInitialized = false;
|
|
20
|
+
|
|
21
|
+
function ensureBindingsPluginInitialized() {
|
|
22
|
+
if (bindingsPluginInitialized) return;
|
|
23
|
+
if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
|
|
24
|
+
bindingsPluginInitialized = true;
|
|
25
|
+
initializeBindingsPlugin();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
document.addEventListener('alpine:init', ensureBindingsPluginInitialized);
|
|
29
|
+
if (window.Alpine && typeof window.Alpine.directive === 'function') ensureBindingsPluginInitialized();
|
|
30
|
+
|
|
31
|
+
})();
|
package/lib/manifest.data.js
CHANGED
|
@@ -279,6 +279,9 @@ const pendingLocal = new Map(); // source -> Map<$id, { patch, removed }>
|
|
|
279
279
|
let allCache = null;
|
|
280
280
|
let allDirty = true;
|
|
281
281
|
|
|
282
|
+
// Per-source freshness: `$fresh` resolves on the first network-fresh landing of this page-load
|
|
283
|
+
const freshness = new Map(); // source -> { promise, resolve, done }
|
|
284
|
+
|
|
282
285
|
// Deep seal so Alpine won't proxy (double-proxying causes recursion)
|
|
283
286
|
function deepSeal(obj) {
|
|
284
287
|
if (obj === null || typeof obj !== 'object') {
|
|
@@ -372,10 +375,48 @@ function mergeRowFields(target, fields, dataSourceName) {
|
|
|
372
375
|
}
|
|
373
376
|
|
|
374
377
|
function attachMethods(array, dataSourceName) {
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
const main = window.ManifestDataMain;
|
|
379
|
+
const reload = main?.reloadDataSource || main?.loadDataSource;
|
|
380
|
+
if (Array.isArray(array) && reload && window.ManifestDataProxies?.attachArrayMethods) {
|
|
381
|
+
window.ManifestDataProxies.attachArrayMethods(array, dataSourceName, reload);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function sourceFreshness(dataSourceName) {
|
|
386
|
+
let f = freshness.get(dataSourceName);
|
|
387
|
+
if (!f) {
|
|
388
|
+
f = { done: false, resolve: null, promise: null };
|
|
389
|
+
f.promise = new Promise(resolve => { f.resolve = resolve; });
|
|
390
|
+
freshness.set(dataSourceName, f);
|
|
378
391
|
}
|
|
392
|
+
return f;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function markFresh(dataSourceName) {
|
|
396
|
+
const f = sourceFreshness(dataSourceName);
|
|
397
|
+
if (!f.done) { f.done = true; f.resolve(); }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// State-only write (loading/error) — rows stay live, no version bump
|
|
401
|
+
function setSourceState(dataSourceName, patch) {
|
|
402
|
+
const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
|
|
403
|
+
if (!store) return null;
|
|
404
|
+
const key = `_${dataSourceName}_state`;
|
|
405
|
+
const current = store[key] || { loading: false, error: null, ready: false, stale: true };
|
|
406
|
+
const next = { ...current, ...patch };
|
|
407
|
+
if (patch.error !== undefined) next.errorTime = patch.error ? (patch.errorTime || Date.now()) : null;
|
|
408
|
+
store[key] = next;
|
|
409
|
+
return next;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Authoritative in-flight map: one promise per key, cleared on settle
|
|
413
|
+
function runDeduped(key, factory) {
|
|
414
|
+
if (loadingPromises.has(key)) return loadingPromises.get(key);
|
|
415
|
+
const promise = Promise.resolve().then(factory).finally(() => {
|
|
416
|
+
if (loadingPromises.get(key) === promise) loadingPromises.delete(key);
|
|
417
|
+
});
|
|
418
|
+
loadingPromises.set(key, promise);
|
|
419
|
+
return promise;
|
|
379
420
|
}
|
|
380
421
|
|
|
381
422
|
// Reactive source array, created on demand; raw map tracks the same identity
|
|
@@ -466,14 +507,18 @@ function writeSource(dataSourceName, data, options = {}) {
|
|
|
466
507
|
rawDataStore.set(dataSourceName, data);
|
|
467
508
|
}
|
|
468
509
|
|
|
469
|
-
const currentState = store[`_${dataSourceName}_state`] || { loading: false, error: null, ready: false };
|
|
510
|
+
const currentState = store[`_${dataSourceName}_state`] || { loading: false, error: null, ready: false, stale: true };
|
|
511
|
+
// `fresh`: network-origin rows → clears `stale` for the rest of the page-load
|
|
512
|
+
const fresh = options.fresh === true && data !== null && data !== undefined;
|
|
470
513
|
const newState = {
|
|
471
514
|
loading: options.loading !== undefined ? options.loading : currentState.loading,
|
|
472
515
|
error: options.error !== undefined ? options.error : currentState.error,
|
|
473
516
|
ready: options.ready !== undefined ? options.ready : (data !== null && data !== undefined),
|
|
474
|
-
errorTime: options.error !== undefined && options.error !== null ? Date.now() : (currentState.errorTime || null)
|
|
517
|
+
errorTime: options.error !== undefined && options.error !== null ? Date.now() : (currentState.errorTime || null),
|
|
518
|
+
stale: fresh ? false : currentState.stale !== false
|
|
475
519
|
};
|
|
476
520
|
store[`_${dataSourceName}_state`] = newState;
|
|
521
|
+
if (fresh) markFresh(dataSourceName);
|
|
477
522
|
store._initialized = true;
|
|
478
523
|
store._ready = true;
|
|
479
524
|
|
|
@@ -1386,6 +1431,10 @@ window.ManifestDataStore = {
|
|
|
1386
1431
|
landRemove,
|
|
1387
1432
|
flushLandings,
|
|
1388
1433
|
noteLocalWrite,
|
|
1434
|
+
// Stale-first + dedupe (§11.1)
|
|
1435
|
+
setSourceState,
|
|
1436
|
+
runDeduped,
|
|
1437
|
+
sourceFreshness,
|
|
1389
1438
|
touchSource,
|
|
1390
1439
|
bumpAllVersions,
|
|
1391
1440
|
mergeRowFields,
|
|
@@ -3231,6 +3280,25 @@ function attachArrayMethods(array, dataSourceName, reloadDataSource) {
|
|
|
3231
3280
|
}
|
|
3232
3281
|
});
|
|
3233
3282
|
|
|
3283
|
+
// $stale / $fresh: semantics in manifest.data.proxies.magic.state.js
|
|
3284
|
+
Object.defineProperty(array, '$stale', {
|
|
3285
|
+
enumerable: false,
|
|
3286
|
+
configurable: true,
|
|
3287
|
+
get: function () {
|
|
3288
|
+
const store = Alpine.store('data');
|
|
3289
|
+
const state = store?.[`_${dataSourceName}_state`];
|
|
3290
|
+
return !state || state.stale !== false;
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
|
|
3294
|
+
Object.defineProperty(array, '$fresh', {
|
|
3295
|
+
enumerable: false,
|
|
3296
|
+
configurable: true,
|
|
3297
|
+
get: function () {
|
|
3298
|
+
return window.ManifestDataStore?.sourceFreshness?.(dataSourceName)?.promise || Promise.resolve();
|
|
3299
|
+
}
|
|
3300
|
+
});
|
|
3301
|
+
|
|
3234
3302
|
// Attach $search method for client-side text filtering
|
|
3235
3303
|
Object.defineProperty(array, '$search', {
|
|
3236
3304
|
enumerable: false,
|
|
@@ -3902,7 +3970,10 @@ function createArrayProxyWithRoute(arrayTarget, dataSourceName = null, reloadDat
|
|
|
3902
3970
|
return undefined;
|
|
3903
3971
|
}
|
|
3904
3972
|
|
|
3905
|
-
// Handle state properties ($loading, $error, $ready)
|
|
3973
|
+
// Handle state properties ($loading, $error, $ready, $stale, $fresh)
|
|
3974
|
+
if (key === '$stale' || key === '$fresh') {
|
|
3975
|
+
return window.ManifestDataProxiesMagic?.getStateProperty?.(key, dataSourceName);
|
|
3976
|
+
}
|
|
3906
3977
|
if (key === '$loading' || key === '$error' || key === '$ready') {
|
|
3907
3978
|
const store = Alpine.store('data');
|
|
3908
3979
|
// Safely access dataSourceName - if it's not defined, use null
|
|
@@ -4089,7 +4160,7 @@ function createArrayProxyWithRoute(arrayTarget, dataSourceName = null, reloadDat
|
|
|
4089
4160
|
return true;
|
|
4090
4161
|
}
|
|
4091
4162
|
// Check custom methods we've added
|
|
4092
|
-
if (key === '$route' || key === '$loading' || key === '$error' || key === '$ready') {
|
|
4163
|
+
if (key === '$route' || key === '$loading' || key === '$error' || key === '$ready' || key === '$stale' || key === '$fresh') {
|
|
4093
4164
|
return true;
|
|
4094
4165
|
}
|
|
4095
4166
|
// For other string keys, check if they exist on the array
|
|
@@ -6947,16 +7018,22 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
|
|
|
6947
7018
|
queries || [],
|
|
6948
7019
|
scope
|
|
6949
7020
|
);
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
7021
|
+
// Dedupe key: source + serialized queries (never across different queries)
|
|
7022
|
+
const key = `${dataSourceName}:$query:${JSON.stringify(appwriteQueries.map(q => String(q)))}`;
|
|
7023
|
+
const { runDeduped, landRows } = window.ManifestDataStore || {};
|
|
7024
|
+
const run = async () => {
|
|
7025
|
+
const result = await window.ManifestDataAppwrite.loadTableRows(
|
|
7026
|
+
appwriteConfig.databaseId,
|
|
7027
|
+
tableId,
|
|
7028
|
+
appwriteQueries
|
|
7029
|
+
);
|
|
7030
|
+
// Network landing: query result replaces the source (coalesced, identity-preserving)
|
|
7031
|
+
if (result !== undefined && landRows) {
|
|
7032
|
+
await landRows(dataSourceName, result, { mode: 'replace', fresh: true });
|
|
7033
|
+
}
|
|
7034
|
+
return result;
|
|
7035
|
+
};
|
|
7036
|
+
return runDeduped ? await runDeduped(key, run) : await run();
|
|
6960
7037
|
}
|
|
6961
7038
|
}
|
|
6962
7039
|
|
|
@@ -7737,20 +7814,39 @@ window.ManifestDataProxiesAppwrite.createAppwriteMethodsHandler = createAppwrite
|
|
|
7737
7814
|
|
|
7738
7815
|
|
|
7739
7816
|
/* Manifest Data Sources - Magic Method State Properties */
|
|
7740
|
-
// $loading, $error, $ready state properties
|
|
7817
|
+
// $loading, $error, $ready, $stale, $fresh state properties
|
|
7818
|
+
//
|
|
7819
|
+
// $stale: true until the first network-fresh landing of this page-load is
|
|
7820
|
+
// applied (rows from a fetch, `$query`, or the memory cache of a fetch made
|
|
7821
|
+
// this page-load); false from then on — a later reload keeps it false and
|
|
7822
|
+
// reports through $loading. Single-reveal UIs gate on `!$stale`.
|
|
7823
|
+
// $fresh: promise resolving at that same first fresh landing; never rejects
|
|
7824
|
+
// (a failed reload keeps old rows and sets $error). Per page-load.
|
|
7825
|
+
|
|
7826
|
+
const STATE_PROPS = ['$loading', '$error', '$ready', '$stale', '$fresh'];
|
|
7827
|
+
|
|
7828
|
+
function defaultStateValue(prop) {
|
|
7829
|
+
if (prop === '$error') return null;
|
|
7830
|
+
if (prop === '$stale') return true;
|
|
7831
|
+
return false;
|
|
7832
|
+
}
|
|
7741
7833
|
|
|
7742
7834
|
function getStateProperty(prop, dataSourceName) {
|
|
7835
|
+
if (prop === '$fresh') {
|
|
7836
|
+
return window.ManifestDataStore?.sourceFreshness?.(dataSourceName)?.promise || Promise.resolve();
|
|
7837
|
+
}
|
|
7838
|
+
|
|
7743
7839
|
if (typeof Alpine === 'undefined' || !Alpine.store) {
|
|
7744
|
-
return
|
|
7840
|
+
return defaultStateValue(prop);
|
|
7745
7841
|
}
|
|
7746
7842
|
|
|
7747
7843
|
const store = Alpine.store('data');
|
|
7748
7844
|
if (!store) {
|
|
7749
|
-
return
|
|
7845
|
+
return defaultStateValue(prop);
|
|
7750
7846
|
}
|
|
7751
7847
|
|
|
7752
7848
|
const stateKey = `_${dataSourceName}_state`;
|
|
7753
|
-
const state = store[stateKey] || { loading: false, error: null, ready: false };
|
|
7849
|
+
const state = store[stateKey] || { loading: false, error: null, ready: false, stale: true };
|
|
7754
7850
|
|
|
7755
7851
|
if (prop === '$loading') {
|
|
7756
7852
|
return state.loading !== false; // Default to true if loading
|
|
@@ -7758,6 +7854,8 @@ function getStateProperty(prop, dataSourceName) {
|
|
|
7758
7854
|
return state.error || null;
|
|
7759
7855
|
} else if (prop === '$ready') {
|
|
7760
7856
|
return state.ready || false;
|
|
7857
|
+
} else if (prop === '$stale') {
|
|
7858
|
+
return state.stale !== false;
|
|
7761
7859
|
}
|
|
7762
7860
|
|
|
7763
7861
|
return undefined;
|
|
@@ -7766,7 +7864,7 @@ function getStateProperty(prop, dataSourceName) {
|
|
|
7766
7864
|
// State property handler for use in proxy get() traps
|
|
7767
7865
|
function createStatePropertyHandler(dataSourceName) {
|
|
7768
7866
|
return function (key) {
|
|
7769
|
-
if (key
|
|
7867
|
+
if (STATE_PROPS.includes(key)) {
|
|
7770
7868
|
return getStateProperty(key, dataSourceName);
|
|
7771
7869
|
}
|
|
7772
7870
|
return undefined;
|
|
@@ -7777,11 +7875,11 @@ function createStatePropertyHandler(dataSourceName) {
|
|
|
7777
7875
|
if (!window.ManifestDataProxiesMagic) {
|
|
7778
7876
|
window.ManifestDataProxiesMagic = {};
|
|
7779
7877
|
}
|
|
7878
|
+
window.ManifestDataProxiesMagic.STATE_PROPS = STATE_PROPS;
|
|
7780
7879
|
window.ManifestDataProxiesMagic.getStateProperty = getStateProperty;
|
|
7781
7880
|
window.ManifestDataProxiesMagic.createStatePropertyHandler = createStatePropertyHandler;
|
|
7782
7881
|
|
|
7783
7882
|
|
|
7784
|
-
|
|
7785
7883
|
/* Manifest Data Sources - Magic Method $files Handler */
|
|
7786
7884
|
// Handles $files method for reactive file arrays
|
|
7787
7885
|
|
|
@@ -8250,10 +8348,14 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8250
8348
|
window._manifestXAccessed = true;
|
|
8251
8349
|
}
|
|
8252
8350
|
const pendingLoads = new Map();
|
|
8351
|
+
const preDataProxies = new Map();
|
|
8253
8352
|
const store = Alpine.store('data');
|
|
8254
8353
|
|
|
8255
|
-
//
|
|
8256
|
-
const reloadDataSource =
|
|
8354
|
+
// Reload variant for Appwrite methods: bypasses the memory cache, keeps rows live
|
|
8355
|
+
const reloadDataSource = (name, locale) => {
|
|
8356
|
+
const reload = window.ManifestDataMain?.reloadDataSource;
|
|
8357
|
+
return reload ? reload(name, locale) : loadDataSource(name, locale, { reload: true });
|
|
8358
|
+
};
|
|
8257
8359
|
|
|
8258
8360
|
const ACTIVE_PROPS = Symbol('activeProps');
|
|
8259
8361
|
|
|
@@ -8285,7 +8387,8 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8285
8387
|
const stamped = Array.isArray(data)
|
|
8286
8388
|
? data.map(d => (d && typeof d === 'object' && !('contentType' in d)) ? { contentType: name, ...d } : d)
|
|
8287
8389
|
: data;
|
|
8288
|
-
|
|
8390
|
+
// App-supplied rows are authoritative → fresh
|
|
8391
|
+
window.ManifestDataStore?.updateStore?.(name, stamped, { loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
|
|
8289
8392
|
return true;
|
|
8290
8393
|
};
|
|
8291
8394
|
}
|
|
@@ -8353,7 +8456,21 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8353
8456
|
track();
|
|
8354
8457
|
const createLoadingProxy = window.ManifestDataProxiesCore?.createLoadingProxy;
|
|
8355
8458
|
if (createLoadingProxy) {
|
|
8356
|
-
|
|
8459
|
+
// Cached per source so re-evaluations see one identity; state props
|
|
8460
|
+
// ($loading/$error/$ready/$stale/$fresh) answer before any rows exist
|
|
8461
|
+
if (!preDataProxies.has(prop)) {
|
|
8462
|
+
const loading = createLoadingProxy(prop);
|
|
8463
|
+
const stateProps = window.ManifestDataProxiesMagic?.STATE_PROPS || [];
|
|
8464
|
+
preDataProxies.set(prop, new Proxy(loading, {
|
|
8465
|
+
get(target, key) {
|
|
8466
|
+
if (stateProps.includes(key)) {
|
|
8467
|
+
return window.ManifestDataProxiesMagic.getStateProperty(key, prop);
|
|
8468
|
+
}
|
|
8469
|
+
return target[key];
|
|
8470
|
+
}
|
|
8471
|
+
}));
|
|
8472
|
+
}
|
|
8473
|
+
return preDataProxies.get(prop);
|
|
8357
8474
|
}
|
|
8358
8475
|
return window.ManifestDataProxiesCore?.getChainingFallback?.() ?? '';
|
|
8359
8476
|
}
|
|
@@ -8461,7 +8578,7 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8461
8578
|
const arrayForMethods = valueIsArray ? value : (rawIsArray ? rawValue : arrayToProxy);
|
|
8462
8579
|
|
|
8463
8580
|
if (attachArrayMethods) {
|
|
8464
|
-
const arrayWithMethods = attachArrayMethods(arrayForMethods, prop,
|
|
8581
|
+
const arrayWithMethods = attachArrayMethods(arrayForMethods, prop, reloadDataSource);
|
|
8465
8582
|
// has() trap so Alpine sees $search/$query/etc. before accessing
|
|
8466
8583
|
return new Proxy(arrayWithMethods, {
|
|
8467
8584
|
get(target, key) {
|
|
@@ -8479,7 +8596,7 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8479
8596
|
const manifest = window.ManifestComponentsRegistry?.manifest || null;
|
|
8480
8597
|
const dataSource = manifest?.data?.[prop] || manifest?.appwrite?.[prop];
|
|
8481
8598
|
if (dataSource && window.ManifestDataConfig?.isAppwriteCollection?.(dataSource)) {
|
|
8482
|
-
const methodsHandler = createAppwriteMethodsHandler(prop,
|
|
8599
|
+
const methodsHandler = createAppwriteMethodsHandler(prop, reloadDataSource);
|
|
8483
8600
|
return methodsHandler.bind(null, key);
|
|
8484
8601
|
}
|
|
8485
8602
|
} catch { /* fall through */ }
|
|
@@ -8497,7 +8614,8 @@ function registerXMagicMethod(loadDataSource) {
|
|
|
8497
8614
|
has(target, key) {
|
|
8498
8615
|
// Report that base plugin methods exist
|
|
8499
8616
|
if (key === '$search' || key === '$query' || key === '$route' ||
|
|
8500
|
-
key === '$loading' || key === '$error' || key === '$ready'
|
|
8617
|
+
key === '$loading' || key === '$error' || key === '$ready' ||
|
|
8618
|
+
key === '$stale' || key === '$fresh') {
|
|
8501
8619
|
return key in target;
|
|
8502
8620
|
}
|
|
8503
8621
|
// Report that array methods exist
|
|
@@ -10751,37 +10869,56 @@ async function checkFileMatchesScope(file, scope) {
|
|
|
10751
10869
|
});
|
|
10752
10870
|
}
|
|
10753
10871
|
|
|
10754
|
-
//
|
|
10755
|
-
|
|
10872
|
+
// Rows already in the store (array or object) — a reload keeps them live
|
|
10873
|
+
function hasLiveRows(dataSourceName) {
|
|
10874
|
+
const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
|
|
10875
|
+
const raw = store ? (window.ManifestDataStore?.rawOf?.(store) || store) : null;
|
|
10876
|
+
const current = raw ? raw[dataSourceName] : undefined;
|
|
10877
|
+
return current !== null && current !== undefined;
|
|
10878
|
+
}
|
|
10879
|
+
|
|
10880
|
+
function liveLocale() {
|
|
10881
|
+
return (typeof document !== 'undefined' && document.documentElement?.lang)
|
|
10882
|
+
|| (typeof Alpine !== 'undefined' && Alpine.store('locale')?.current)
|
|
10883
|
+
|| 'en';
|
|
10884
|
+
}
|
|
10885
|
+
|
|
10886
|
+
// Load dataSource data. options.reload: bypass the memory cache and refetch;
|
|
10887
|
+
// existing rows stay live ($loading only), fresh rows merge by $id.
|
|
10888
|
+
async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
|
|
10889
|
+
locale = locale || 'en';
|
|
10756
10890
|
const cacheKey = `${dataSourceName}:${locale}`;
|
|
10757
|
-
const { dataSourceCache, loadingPromises, isInitializing, updateStore, landRows } = window.ManifestDataStore;
|
|
10891
|
+
const { dataSourceCache, loadingPromises, isInitializing, updateStore, landRows, setSourceState, runDeduped } = window.ManifestDataStore;
|
|
10758
10892
|
|
|
10759
|
-
// Memory cache. Serving cached data is fine, but
|
|
10760
|
-
// live store would clobber a concurrent locale
|
|
10761
|
-
// write via the `_locale` stamp
|
|
10762
|
-
if (dataSourceCache.has(cacheKey)) {
|
|
10893
|
+
// Memory cache (this page-load's fetches). Serving cached data is fine, but
|
|
10894
|
+
// writing an old locale to the live store would clobber a concurrent locale
|
|
10895
|
+
// switch — so guard the store write via the `_locale` stamp.
|
|
10896
|
+
if (!options.reload && dataSourceCache.has(cacheKey)) {
|
|
10763
10897
|
const cachedData = dataSourceCache.get(cacheKey);
|
|
10764
|
-
const
|
|
10898
|
+
const live = (window.Alpine && Alpine.store('locale')?.current)
|
|
10765
10899
|
|| document.documentElement.lang || locale;
|
|
10766
|
-
const staleLocaleHit = locale !==
|
|
10900
|
+
const staleLocaleHit = locale !== live && !!(cachedData && cachedData._locale);
|
|
10767
10901
|
if (!isInitializing && !staleLocaleHit) {
|
|
10768
10902
|
// Landing (coalesced): resolves once the cached rows are visible
|
|
10769
|
-
await landRows(dataSourceName, cachedData, { mode: 'replace', loading: false, error: null, ready: true });
|
|
10903
|
+
await landRows(dataSourceName, cachedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true });
|
|
10770
10904
|
}
|
|
10771
10905
|
return cachedData;
|
|
10772
10906
|
}
|
|
10773
10907
|
|
|
10774
|
-
//
|
|
10908
|
+
// In flight for this source+locale: share it (every entry path lands here)
|
|
10775
10909
|
if (loadingPromises.has(cacheKey)) {
|
|
10776
10910
|
return loadingPromises.get(cacheKey);
|
|
10777
10911
|
}
|
|
10778
10912
|
|
|
10779
|
-
//
|
|
10913
|
+
// Loading state: rows already landed stay live; first-ever load clears
|
|
10914
|
+
const keepRows = hasLiveRows(dataSourceName);
|
|
10780
10915
|
if (!isInitializing) {
|
|
10781
|
-
|
|
10916
|
+
if (keepRows) setSourceState(dataSourceName, { loading: true, error: null });
|
|
10917
|
+
else updateStore(dataSourceName, null, { loading: true, error: null, ready: false });
|
|
10782
10918
|
}
|
|
10783
10919
|
|
|
10784
|
-
|
|
10920
|
+
return runDeduped(cacheKey, async () => {
|
|
10921
|
+
let landed = false;
|
|
10785
10922
|
try {
|
|
10786
10923
|
const manifest = await window.ManifestDataConfig.ensureManifest();
|
|
10787
10924
|
if (!manifest) {
|
|
@@ -11043,23 +11180,29 @@ async function loadDataSource(dataSourceName, locale = 'en') {
|
|
|
11043
11180
|
|| document.documentElement.lang || locale;
|
|
11044
11181
|
const staleLocale = localeSensitive && liveLocale !== locale;
|
|
11045
11182
|
|
|
11046
|
-
// Network landing (coalesced per frame); resolves once visible
|
|
11183
|
+
// Network landing (coalesced per frame, merges by $id); resolves once visible
|
|
11047
11184
|
if (!isInitializing && !staleLocale) {
|
|
11048
|
-
|
|
11185
|
+
landed = true;
|
|
11186
|
+
await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true });
|
|
11049
11187
|
}
|
|
11050
11188
|
|
|
11051
11189
|
// Return unsealed version for our proxy system
|
|
11052
11190
|
return enhancedData;
|
|
11053
11191
|
} catch (error) {
|
|
11054
|
-
//
|
|
11192
|
+
// Error state (timestamped to prevent rapid retries); live rows stay
|
|
11055
11193
|
if (!isInitializing) {
|
|
11194
|
+
landed = true;
|
|
11056
11195
|
const errorMessage = error?.message || error?.toString() || `Failed to load dataSource "${dataSourceName}"`;
|
|
11057
|
-
|
|
11058
|
-
loading: false,
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
11062
|
-
|
|
11196
|
+
if (hasLiveRows(dataSourceName)) {
|
|
11197
|
+
setSourceState(dataSourceName, { loading: false, error: errorMessage, errorTime: Date.now() });
|
|
11198
|
+
} else {
|
|
11199
|
+
updateStore(dataSourceName, null, {
|
|
11200
|
+
loading: false,
|
|
11201
|
+
error: errorMessage,
|
|
11202
|
+
ready: false,
|
|
11203
|
+
errorTime: Date.now() // Track when error occurred
|
|
11204
|
+
});
|
|
11205
|
+
}
|
|
11063
11206
|
}
|
|
11064
11207
|
|
|
11065
11208
|
// Only log non-auth errors to reduce noise (401 is expected until user authenticates)
|
|
@@ -11073,12 +11216,15 @@ async function loadDataSource(dataSourceName, locale = 'en') {
|
|
|
11073
11216
|
}
|
|
11074
11217
|
return null;
|
|
11075
11218
|
} finally {
|
|
11076
|
-
|
|
11219
|
+
// Early exits (no source, stale locale) must not strand a live source in $loading
|
|
11220
|
+
if (!landed && keepRows && !isInitializing) setSourceState(dataSourceName, { loading: false });
|
|
11077
11221
|
}
|
|
11078
|
-
})
|
|
11222
|
+
});
|
|
11223
|
+
}
|
|
11079
11224
|
|
|
11080
|
-
|
|
11081
|
-
|
|
11225
|
+
// Reload: refetch past the memory cache; rows stay live and merge by $id
|
|
11226
|
+
function reloadDataSource(dataSourceName, locale) {
|
|
11227
|
+
return loadDataSource(dataSourceName, locale || liveLocale(), { reload: true });
|
|
11082
11228
|
}
|
|
11083
11229
|
|
|
11084
11230
|
// Listen for URL changes to trigger reactivity
|
|
@@ -11203,7 +11349,8 @@ async function initializeDataSourcesPlugin() {
|
|
|
11203
11349
|
// Export loadDataSource for use by team change listener
|
|
11204
11350
|
window.ManifestDataMain = {
|
|
11205
11351
|
loadDataSource,
|
|
11206
|
-
|
|
11352
|
+
reloadDataSource,
|
|
11353
|
+
_loadDataSource: reloadDataSource, // Internal: every user is a reload-after-mutation
|
|
11207
11354
|
filterFilesByScope // Export for use by getFilesForEntry
|
|
11208
11355
|
};
|
|
11209
11356
|
|
|
@@ -11216,18 +11363,16 @@ async function initializeDataSourcesPlugin() {
|
|
|
11216
11363
|
const locale = document.documentElement.lang ||
|
|
11217
11364
|
(typeof Alpine !== 'undefined' && Alpine.store('locale')?.current) || 'en';
|
|
11218
11365
|
|
|
11219
|
-
const { dataSourceCache
|
|
11366
|
+
const { dataSourceCache } = window.ManifestDataStore;
|
|
11220
11367
|
const isAppwriteCollection = window.ManifestDataConfig.isAppwriteCollection;
|
|
11221
11368
|
|
|
11222
11369
|
for (const [name, source] of Object.entries(manifest.data)) {
|
|
11223
11370
|
if (isAppwriteCollection(source)) continue;
|
|
11224
11371
|
if (source && typeof source === 'object' && source.url) continue;
|
|
11225
11372
|
|
|
11226
|
-
|
|
11227
|
-
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
try { await loadDataSource(name, locale); } catch { /* skip failed sources */ }
|
|
11373
|
+
dataSourceCache.delete(`${name}:${locale}`);
|
|
11374
|
+
// Reload keeps rows live and merges (identity survives an edit-save)
|
|
11375
|
+
try { await loadDataSource(name, locale, { reload: true }); } catch { /* skip failed sources */ }
|
|
11231
11376
|
}
|
|
11232
11377
|
});
|
|
11233
11378
|
|
|
@@ -11265,7 +11410,7 @@ async function initializeDataSourcesPlugin() {
|
|
|
11265
11410
|
const data = await loadDataSource(name, locale);
|
|
11266
11411
|
if (data != null) {
|
|
11267
11412
|
// Boot landing: every pre-loaded source lands in ONE flush
|
|
11268
|
-
await landRows(name, data, { mode: 'replace', loading: false, error: null, ready: true, allowDuringInit: true });
|
|
11413
|
+
await landRows(name, data, { mode: 'replace', loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
|
|
11269
11414
|
}
|
|
11270
11415
|
} catch (err) {
|
|
11271
11416
|
console.warn(`[Manifest Data] Failed to pre-load ${name}:`, err);
|
|
@@ -11283,7 +11428,7 @@ async function initializeDataSourcesPlugin() {
|
|
|
11283
11428
|
// allowDuringInit: setIsInitializing(true) is active, so without this
|
|
11284
11429
|
// flag updateStore short-circuits and $x.manifest stays unpopulated.
|
|
11285
11430
|
window.ManifestDataStore.dataSourceCache.set(`manifest:${locale}`, publicManifest);
|
|
11286
|
-
updateStore('manifest', publicManifest, { loading: false, error: null, ready: true, allowDuringInit: true });
|
|
11431
|
+
updateStore('manifest', publicManifest, { loading: false, error: null, ready: true, fresh: true, allowDuringInit: true });
|
|
11287
11432
|
}
|
|
11288
11433
|
Alpine.store('data')._ready = true;
|
|
11289
11434
|
} catch (error) {
|
package/lib/manifest.defer.js
CHANGED
|
@@ -12,7 +12,17 @@ const OWNS_CHILDREN = new Set([
|
|
|
12
12
|
// Kill switch: data-defer="off" on the loader script; prerender snapshots keep eager markup
|
|
13
13
|
const killed = !!document.querySelector('script[data-defer="off"]') || window.__manifestRender === true;
|
|
14
14
|
|
|
15
|
+
// Route deferral (spike, opt-in): data-defer-routes on the loader script or ManifestDeferConfig.routes
|
|
16
|
+
let routesScriptFlag = null;
|
|
17
|
+
function routesOn() {
|
|
18
|
+
const cfg = window.ManifestDeferConfig;
|
|
19
|
+
if (cfg && cfg.routes !== undefined) return cfg.routes === true;
|
|
20
|
+
if (routesScriptFlag === null) routesScriptFlag = !!document.querySelector('script[data-defer-routes]');
|
|
21
|
+
return routesScriptFlag;
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
const pending = new Set();
|
|
25
|
+
const routes = new Set(); // deferred route panes: render on activation only, never prewarmed
|
|
16
26
|
let ready = false;
|
|
17
27
|
let bootDrained = false;
|
|
18
28
|
let idleHandle = null;
|
|
@@ -20,9 +30,17 @@ let idleHandle = null;
|
|
|
20
30
|
// Urgent = mounted after boot, within a gesture window (a pane the user just opened), capped per gesture
|
|
21
31
|
const URGENT_WINDOW_MS = 1500;
|
|
22
32
|
const URGENT_CAP = 8;
|
|
33
|
+
const PREWARM_CAP = (window.ManifestDeferConfig && window.ManifestDeferConfig.prewarmCap) || 48; // warm-but-unopened containers kept live
|
|
34
|
+
const IDLE_BUDGET_MS = 4;
|
|
35
|
+
const SLICE_MAX = 8;
|
|
36
|
+
const warm = new Set(); // prewarmed, never opened by the user
|
|
23
37
|
let lastGestureAt = -Infinity;
|
|
24
38
|
let urgentWindowAt = -Infinity;
|
|
25
39
|
let urgentInWindow = 0;
|
|
40
|
+
let promoteTimer = null;
|
|
41
|
+
let lastPromoteAt = -Infinity;
|
|
42
|
+
let gestureX = 0, gestureY = 0;
|
|
43
|
+
let idleIsTimer = false;
|
|
26
44
|
const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
|
|
27
45
|
|
|
28
46
|
function isUrgent() {
|
|
@@ -65,12 +83,21 @@ function firstPanel(el) {
|
|
|
65
83
|
return scope.querySelector(`[x-tabpanel="${set}"]`);
|
|
66
84
|
}
|
|
67
85
|
|
|
86
|
+
// A route pane is closed when the router says it does not match the URL (or it is already hidden)
|
|
87
|
+
function routeInactive(el) {
|
|
88
|
+
if (el.hasAttribute('hidden')) return true;
|
|
89
|
+
const vis = window.ManifestRoutingVisibility;
|
|
90
|
+
return !!(vis && typeof vis.isRouteActive === 'function' && !vis.isRouteActive(el));
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
// Closed-container rule, or the visibility rule for explicit x-defer
|
|
69
94
|
function ruleFor(el, explicit) {
|
|
95
|
+
const isRoute = el.hasAttribute('x-route');
|
|
96
|
+
if (isRoute && routesOn() && routeInactive(el)) return 'route';
|
|
70
97
|
if (el.hasAttribute('popover')) return isPopoverOpen(el) ? null : 'popover';
|
|
71
98
|
const tag = el.tagName;
|
|
72
99
|
if (tag === 'DIALOG' || tag === 'DETAILS') return el.hasAttribute('open') ? null : 'toggle';
|
|
73
|
-
if (el.hasAttribute('hidden') && !
|
|
100
|
+
if (el.hasAttribute('hidden') && !isRoute) return 'hidden';
|
|
74
101
|
if (el.hasAttribute('x-tabpanel') && window.ensureTabsPluginInitialized) return firstPanel(el) === el ? null : 'panel';
|
|
75
102
|
return explicit ? 'visible' : null;
|
|
76
103
|
}
|
|
@@ -107,14 +134,16 @@ function register(el, rule, opts) {
|
|
|
107
134
|
cleanup: []
|
|
108
135
|
};
|
|
109
136
|
wire(rec);
|
|
110
|
-
if (
|
|
137
|
+
if (rule === 'route') routes.add(rec);
|
|
138
|
+
else if (!rec.lazy) { pending.add(rec); schedule(); }
|
|
111
139
|
return rec;
|
|
112
140
|
}
|
|
113
141
|
|
|
114
|
-
function render(rec) {
|
|
115
|
-
if (rec.rendered) return;
|
|
142
|
+
function render(rec, viaPrewarm) {
|
|
143
|
+
if (rec.rendered) { if (!viaPrewarm && warm.delete(rec)) schedule(); return; }
|
|
116
144
|
rec.rendered = true;
|
|
117
145
|
pending.delete(rec);
|
|
146
|
+
if (viaPrewarm) warm.add(rec); else warm.delete(rec);
|
|
118
147
|
const { el, tpl } = rec;
|
|
119
148
|
const content = rec.discard ? tpl.content.cloneNode(true) : tpl.content;
|
|
120
149
|
Alpine.mutateDom(() => {
|
|
@@ -136,6 +165,38 @@ function teardown(rec) {
|
|
|
136
165
|
});
|
|
137
166
|
}
|
|
138
167
|
|
|
168
|
+
// Evict a warm, still-closed, never-opened container back to its stash (least reachable first)
|
|
169
|
+
function restash(rec) {
|
|
170
|
+
if (!rec.rendered) return;
|
|
171
|
+
rec.rendered = false;
|
|
172
|
+
rec.near = undefined;
|
|
173
|
+
const { el, tpl } = rec;
|
|
174
|
+
Alpine.mutateDom(() => {
|
|
175
|
+
Array.from(el.children).forEach((child) => Alpine.destroyTree(child));
|
|
176
|
+
while (el.firstChild) tpl.content.appendChild(el.firstChild);
|
|
177
|
+
el.appendChild(tpl);
|
|
178
|
+
});
|
|
179
|
+
warm.delete(rec);
|
|
180
|
+
pending.add(rec);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function evict() {
|
|
184
|
+
if (warm.size <= PREWARM_CAP) return;
|
|
185
|
+
for (const rec of warm) rec.near = undefined;
|
|
186
|
+
while (warm.size > PREWARM_CAP) {
|
|
187
|
+
let victim = null, score = -1;
|
|
188
|
+
for (const rec of warm) {
|
|
189
|
+
if (!rec.el.isConnected) { warm.delete(rec); continue; }
|
|
190
|
+
if (rec.rule === 'popover' && isPopoverOpen(rec.el)) continue;
|
|
191
|
+
if (rec.near === undefined) rec.near = nearViewport(rec);
|
|
192
|
+
const sc = (reachable(rec.el) ? 0 : 2) + (rec.near ? 0 : 1);
|
|
193
|
+
if (sc > score) { victim = rec; score = sc; }
|
|
194
|
+
}
|
|
195
|
+
if (!victim) return;
|
|
196
|
+
restash(victim);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
139
200
|
// ---- Open signals ----
|
|
140
201
|
|
|
141
202
|
function wire(rec) {
|
|
@@ -167,6 +228,15 @@ function wire(rec) {
|
|
|
167
228
|
});
|
|
168
229
|
arm();
|
|
169
230
|
if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
|
|
231
|
+
} else if (rule === 'route') {
|
|
232
|
+
// Router fires manifest:route-activate before it unhides the pane; hidden removal is the fallback
|
|
233
|
+
listen('manifest:route-activate', () => render(rec));
|
|
234
|
+
const arm = () => Alpine.onAttributeRemoved(el, 'hidden', () => {
|
|
235
|
+
if (!el.hasAttribute('hidden')) render(rec);
|
|
236
|
+
if (!rec.rendered || rec.discard) arm();
|
|
237
|
+
});
|
|
238
|
+
arm();
|
|
239
|
+
if (rec.discard) observe(['hidden'], () => { if (el.hasAttribute('hidden')) teardown(rec); });
|
|
170
240
|
} else if (rule === 'panel') {
|
|
171
241
|
observe(['style', 'x-show', 'x-tabpanel'], () => onState(shown(el, false)));
|
|
172
242
|
} else if (rule === 'visible') {
|
|
@@ -177,19 +247,86 @@ function wire(rec) {
|
|
|
177
247
|
|
|
178
248
|
// ---- Idle prewarm (after manifest:ready; urgent first, then priority asc, then document order) ----
|
|
179
249
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
250
|
+
// Genuine idle only: no timeout, so a never-idle page never prewarms (its stashed containers cost nothing)
|
|
251
|
+
const idle = (fn) => {
|
|
252
|
+
idleIsTimer = !window.requestIdleCallback;
|
|
253
|
+
return idleIsTimer
|
|
254
|
+
? setTimeout(() => fn({ didTimeout: false, timeRemaining: () => IDLE_BUDGET_MS + 1 }), 200)
|
|
255
|
+
: window.requestIdleCallback(fn);
|
|
256
|
+
};
|
|
257
|
+
function cancelIdle() {
|
|
258
|
+
if (idleHandle === null) return;
|
|
259
|
+
if (idleIsTimer) clearTimeout(idleHandle); else if (window.cancelIdleCallback) window.cancelIdleCallback(idleHandle);
|
|
260
|
+
idleHandle = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// A gesture that revealed a pane (tab switch, panel open) makes its containers urgent
|
|
264
|
+
function promoteNearViewport() {
|
|
265
|
+
if (!ready || !bootDrained) return;
|
|
266
|
+
// The on-screen containers nearest the gesture point (a revealed pane sits under the click)
|
|
267
|
+
const candidates = [];
|
|
268
|
+
for (const rec of pending) {
|
|
269
|
+
if (rec.urgent || !rec.el.isConnected || !reachable(rec.el)) continue;
|
|
270
|
+
const r = anchorRect(rec);
|
|
271
|
+
rec.near = !!r && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
|
|
272
|
+
if (!rec.near) continue;
|
|
273
|
+
const dx = Math.max(r.left - gestureX, 0, gestureX - r.right);
|
|
274
|
+
const dy = Math.max(r.top - gestureY, 0, gestureY - r.bottom);
|
|
275
|
+
candidates.push([dx * dx + dy * dy, rec]);
|
|
276
|
+
}
|
|
277
|
+
if (!candidates.length) return;
|
|
278
|
+
candidates.sort((a, b) => (a[1].priority - b[1].priority) || (a[0] - b[0])); // authored priority first, then nearest
|
|
279
|
+
let n = 0;
|
|
280
|
+
for (const [, rec] of candidates) { if (n++ >= URGENT_CAP) break; rec.urgent = true; }
|
|
281
|
+
cancelIdle();
|
|
282
|
+
schedule();
|
|
283
|
+
}
|
|
284
|
+
function onGesture(e) {
|
|
285
|
+
lastGestureAt = now();
|
|
286
|
+
if (e && typeof e.clientX === 'number' && (e.clientX || e.clientY)) { gestureX = e.clientX; gestureY = e.clientY; }
|
|
287
|
+
if (now() - lastPromoteAt < 300) return;
|
|
288
|
+
clearTimeout(promoteTimer);
|
|
289
|
+
lastPromoteAt = now();
|
|
290
|
+
// Three passes: a revealed pane lays out and mounts its menus over the next few hundred ms
|
|
291
|
+
promoteTimer = setTimeout(() => { promoteNearViewport(); promoteTimer = setTimeout(() => { promoteNearViewport(); promoteTimer = setTimeout(promoteNearViewport, 400); }, 150); }, 50);
|
|
292
|
+
}
|
|
183
293
|
|
|
294
|
+
// Only containers the user can reach: not under a hidden route; visible parents first
|
|
295
|
+
const reachable = (el) => !el.closest('[x-route][hidden]');
|
|
296
|
+
// Judge by the invoker (popovertarget / aria-controls), else the nearest ancestor with a box
|
|
297
|
+
// Rect of the invoker or the nearest boxed ancestor (null when nothing has a box)
|
|
298
|
+
function anchorRect(rec) {
|
|
299
|
+
const el = rec.el;
|
|
300
|
+
if (!rec.invoker && el.id && window.CSS && CSS.escape) {
|
|
301
|
+
const id = CSS.escape(el.id);
|
|
302
|
+
rec.invoker = document.querySelector(`[popovertarget="${id}"], [aria-controls="${id}"]`) || null;
|
|
303
|
+
}
|
|
304
|
+
let p = (rec.invoker && rec.invoker.isConnected) ? rec.invoker : el.parentElement;
|
|
305
|
+
for (let depth = 0; p && depth < 6; p = p.parentElement, depth++) {
|
|
306
|
+
const r = p.getBoundingClientRect();
|
|
307
|
+
if (r.width || r.height) return r;
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
function nearViewport(rec) {
|
|
312
|
+
const r = anchorRect(rec);
|
|
313
|
+
return !!r && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
|
|
314
|
+
}
|
|
315
|
+
function rank(rec) {
|
|
316
|
+
if (rec.near === undefined) rec.near = nearViewport(rec);
|
|
317
|
+
return (rec.urgent ? 0 : 4) + (rec.near ? 0 : 2);
|
|
318
|
+
}
|
|
184
319
|
function next() {
|
|
185
|
-
let best = null;
|
|
320
|
+
let best = null, bestRank = 0;
|
|
186
321
|
for (const rec of pending) {
|
|
187
322
|
if (!rec.el.isConnected) { pending.delete(rec); continue; }
|
|
323
|
+
if (!reachable(rec.el)) continue;
|
|
324
|
+
const r = rank(rec);
|
|
188
325
|
if (!best
|
|
189
|
-
||
|
|
190
|
-
|| (
|
|
326
|
+
|| r < bestRank
|
|
327
|
+
|| (r === bestRank && (rec.priority < best.priority
|
|
191
328
|
|| (rec.priority === best.priority && (best.el.compareDocumentPosition(rec.el) & Node.DOCUMENT_POSITION_PRECEDING))))) {
|
|
192
|
-
best = rec;
|
|
329
|
+
best = rec; bestRank = r;
|
|
193
330
|
}
|
|
194
331
|
}
|
|
195
332
|
return best;
|
|
@@ -199,12 +336,24 @@ function schedule() {
|
|
|
199
336
|
if (pending.size === 0) { if (ready) bootDrained = true; return; }
|
|
200
337
|
if (idleHandle !== null || !ready) return;
|
|
201
338
|
const head = next();
|
|
202
|
-
|
|
339
|
+
if (!head) { if (ready) bootDrained = true; return; }
|
|
340
|
+
if (!head.urgent && warm.size >= PREWARM_CAP) return; // paused at the cap; resumes when a warm one opens or evicts
|
|
341
|
+
idleHandle = idle((deadline) => {
|
|
203
342
|
idleHandle = null;
|
|
204
|
-
|
|
205
|
-
|
|
343
|
+
// Render while the browser reports idle time; the first container of a slice always gets its turn
|
|
344
|
+
let n = 0;
|
|
345
|
+
while (n < SLICE_MAX && (n === 0 || (deadline && deadline.timeRemaining() > IDLE_BUDGET_MS))) {
|
|
346
|
+
if (deadline && deadline.didTimeout) break;
|
|
347
|
+
const rec = next();
|
|
348
|
+
if (!rec) break;
|
|
349
|
+
if (!rec.urgent && warm.size >= PREWARM_CAP) break; // cap = max warm; only urgent renders may exceed it
|
|
350
|
+
for (const other of pending) other.near = undefined;
|
|
351
|
+
render(rec, true);
|
|
352
|
+
n++;
|
|
353
|
+
}
|
|
354
|
+
evict();
|
|
206
355
|
schedule();
|
|
207
|
-
}
|
|
356
|
+
});
|
|
208
357
|
}
|
|
209
358
|
|
|
210
359
|
function markReady() {
|
|
@@ -244,7 +393,7 @@ function intercept(el, skip) {
|
|
|
244
393
|
function initializeDeferPlugin() {
|
|
245
394
|
Alpine.directive('defer', () => { });
|
|
246
395
|
if (!killed) Alpine.interceptInit(Alpine.skipDuringClone(intercept));
|
|
247
|
-
['pointerdown', 'keydown'].forEach((type) => document.addEventListener(type,
|
|
396
|
+
['pointerdown', 'keydown'].forEach((type) => document.addEventListener(type, onGesture, { capture: true, passive: true }));
|
|
248
397
|
armReady();
|
|
249
398
|
}
|
|
250
399
|
|
|
@@ -252,7 +401,12 @@ window.ManifestDefer = {
|
|
|
252
401
|
enabled: !killed,
|
|
253
402
|
defer,
|
|
254
403
|
render: (el) => { if (el && el.__mnfstDefer) render(el.__mnfstDefer); },
|
|
255
|
-
isPending: (el) => !!(el && el.__mnfstDefer && !el.__mnfstDefer.rendered)
|
|
404
|
+
isPending: (el) => !!(el && el.__mnfstDefer && !el.__mnfstDefer.rendered),
|
|
405
|
+
stats: () => ({
|
|
406
|
+
pending: pending.size, warm: warm.size, cap: PREWARM_CAP, ready, bootDrained, armed: idleHandle !== null,
|
|
407
|
+
head: (() => { const h = next(); return h ? (h.el.id || h.el.tagName) : null; })(),
|
|
408
|
+
routes: { enabled: routesOn(), stashed: Array.from(routes).filter((r) => !r.rendered).length, rendered: Array.from(routes).filter((r) => r.rendered).length }
|
|
409
|
+
})
|
|
256
410
|
};
|
|
257
411
|
if (window.Manifest && typeof window.Manifest === 'object') window.Manifest.defer = defer;
|
|
258
412
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"manifest.appwrite.auth.js": "sha384-lWvffK6C2Zs9dOiMh/0OQTnU2ycZq1USOeJDlPUgJsbtrdA9+3zq4VHcIdS40qR0",
|
|
3
3
|
"manifest.appwrite.data.js": "sha384-FusD+MQKkRQexKYl7j6egjDzsPYMWgtNublkgOl595aNqbx4yh8/PfbhLEyI06T1",
|
|
4
4
|
"manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
|
|
5
|
+
"manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
|
|
5
6
|
"manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
|
|
6
7
|
"manifest.chat.js": "sha384-UN5BmhqyafJ8OfLFaOLszrvUW5r001dRheQWqsEBCCLnfBcW1bjXaGOdZvURtrnL",
|
|
7
8
|
"manifest.code.js": "sha384-aoQ4E26vrhNe/+tL7K0h90c8/0ies7e8oIR6Ay92he2X3dnWZ+OPcn2Dna8fmc+c",
|
|
@@ -10,9 +11,9 @@
|
|
|
10
11
|
"manifest.combobox.js": "sha384-SN3XHumoebxcduFQqx5zt65/Cd10y8nuf9Gkco6UnsZ42sadRd+gvVLfWPdkkuPH",
|
|
11
12
|
"manifest.components.js": "sha384-HEyeJtV20k7po8WH0LEj800MlsqgDH0vE5GTAl62SgdEhn1aJ7VfryQcA6yIi18U",
|
|
12
13
|
"manifest.computed.js": "sha384-LDxER8pyYyYwUxIYIyiJA02s+49g8CzpSlOj28On0DFIUgetT3UmvE/9xOsTwT66",
|
|
13
|
-
"manifest.data.js": "sha384-
|
|
14
|
+
"manifest.data.js": "sha384-LhX1a36AITeixPrC4RHz0X+TNMEoDwxi6JVclaIhNAlTYCtkYOHE2KmxJZhfJheD",
|
|
14
15
|
"manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
|
|
15
|
-
"manifest.defer.js": "sha384-
|
|
16
|
+
"manifest.defer.js": "sha384-t/g++osVS3qDXeY8+gfxUtLF9o0SCQQJKNxHGpb8NkIciDvxOAvA6sFIVcM0AKeA",
|
|
16
17
|
"manifest.dropdowns.js": "sha384-4HuHkBt4i8sTPUDLB9rNLli2/lWLY1tjtCLzey37gRwueG+FRLCnVH1q7ljiVvJR",
|
|
17
18
|
"manifest.edit.js": "sha384-ZvgV3ax7yHtSC23zdvRYJPzBsBpGFBLuBJMgfQ1IDumv97Cn2bpHOePxyQikfl5L",
|
|
18
19
|
"manifest.export.js": "sha384-HCy3n6ifrcAGIyzTRgZ5JIRvSX9q9/8ykMKRSbt1QTnhXz6PgYlV0AE2N2Yi2re+",
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"manifest.native.js": "sha384-nIngj4CM5hV4bvQvgs8C7JmSy1UUzobsbhvDE2NH5dXqQVNW/Hiat34wHUxMqSaO",
|
|
23
24
|
"manifest.payments.js": "sha384-QGctMZtxF0Wb2JUJbb9sJ4bFbyQvUdr9HaKs4afyaTXb7zedAAIWeB27XINMe3VZ",
|
|
24
25
|
"manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
|
|
25
|
-
"manifest.router.js": "sha384-
|
|
26
|
+
"manifest.router.js": "sha384-4qmGEkRpgAo9xoQkW6+hHIhowvcyAggKB2J3Wf8tJtXp+L0hufoVAqzfhnqw4cNR",
|
|
26
27
|
"manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
|
|
27
28
|
"manifest.status.js": "sha384-fXqDnKwldB7XRxcfEQYTm/WqLl2oP24xY2Znq8FqnFM9LQo0zrQkXszqAOksArUX",
|
|
28
29
|
"manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
|
|
@@ -33,6 +34,6 @@
|
|
|
33
34
|
"manifest.tooltips.js": "sha384-uXVVQADl6pY2Pj9i/UaSclZN7dzbKhLkmevv7Ad0+wXR8LgqodALNFrc5jZJjlwm",
|
|
34
35
|
"manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
|
|
35
36
|
"manifest.utilities.js": "sha384-YcGKTEviTv9FLkw890CTociA7Rjp6xuhLyfpn3Gk4mT0fexDFqpW7VSuohcpGfsw",
|
|
36
|
-
"manifest.virtual.js": "sha384-
|
|
37
|
-
"manifest.js": "sha384-
|
|
37
|
+
"manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
|
|
38
|
+
"manifest.js": "sha384-4r0X5QSeiQ6bGMrw4Y8CDhTAS38w7CPXyxrid+l+sGWEwJ2XRG0STzrLU6XF+GIX"
|
|
38
39
|
}
|
package/lib/manifest.js
CHANGED
package/lib/manifest.router.js
CHANGED
|
@@ -636,39 +636,21 @@ function isPrerenderedStaticMPA() {
|
|
|
636
636
|
}
|
|
637
637
|
}
|
|
638
638
|
|
|
639
|
-
//
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const routeElements = document.querySelectorAll('[x-route]');
|
|
646
|
-
|
|
647
|
-
// First pass: collect all defined routes (excluding !* and other negative conditions)
|
|
648
|
-
const definedRoutes = [];
|
|
649
|
-
routeElements.forEach(element => {
|
|
650
|
-
const routeCondition = element.getAttribute('x-route');
|
|
651
|
-
if (!routeCondition) return;
|
|
652
|
-
|
|
653
|
-
const conditions = routeCondition.split(',').map(cond => cond.trim());
|
|
654
|
-
conditions.forEach(cond => {
|
|
655
|
-
// Only collect positive conditions and wildcards (not negative ones)
|
|
656
|
-
if (!cond.startsWith('!') && cond !== '!*') {
|
|
657
|
-
definedRoutes.push(cond);
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
});
|
|
639
|
+
// Logical path for the current URL, normalized the way route conditions expect
|
|
640
|
+
function currentNormalizedPath() {
|
|
641
|
+
const currentPath = window.ManifestRoutingNavigation?.getCurrentRoute() ?? window.location.pathname;
|
|
642
|
+
return currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
|
|
643
|
+
}
|
|
661
644
|
|
|
662
|
-
|
|
645
|
+
// Localization codes from manifest.json data sources
|
|
646
|
+
function getLocalizationCodes() {
|
|
663
647
|
const localizationCodes = [];
|
|
664
648
|
try {
|
|
665
|
-
// Check if manifest is available and has data sources
|
|
666
649
|
const manifest = window.ManifestComponentsRegistry?.manifest || window.manifest;
|
|
667
650
|
if (manifest && manifest.data) {
|
|
668
651
|
Object.values(manifest.data).forEach(dataSource => {
|
|
669
652
|
if (typeof dataSource === 'object' && dataSource !== null) {
|
|
670
653
|
Object.keys(dataSource).forEach(key => {
|
|
671
|
-
// Check if this looks like a localization key (common language codes)
|
|
672
654
|
if (key.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
|
|
673
655
|
localizationCodes.push(key);
|
|
674
656
|
}
|
|
@@ -679,80 +661,117 @@ function processRouteVisibility(normalizedPath) {
|
|
|
679
661
|
} catch (e) {
|
|
680
662
|
// Ignore errors if manifest is not available
|
|
681
663
|
}
|
|
664
|
+
return localizationCodes;
|
|
665
|
+
}
|
|
682
666
|
|
|
683
|
-
|
|
684
|
-
|
|
667
|
+
// Positive conditions of every route, including routes stashed inside a deferred route
|
|
668
|
+
function collectDefinedRoutes() {
|
|
669
|
+
const definedRoutes = [];
|
|
670
|
+
const collect = (element) => {
|
|
671
|
+
const routeCondition = element.getAttribute('x-route');
|
|
672
|
+
if (!routeCondition) return;
|
|
673
|
+
routeCondition.split(',').map(cond => cond.trim()).forEach(cond => {
|
|
674
|
+
if (!cond.startsWith('!') && cond !== '!*') definedRoutes.push(cond);
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
document.querySelectorAll('[x-route]').forEach(collect);
|
|
678
|
+
document.querySelectorAll('[x-route] > template[data-mnfst-defer]').forEach(tpl => {
|
|
679
|
+
tpl.content.querySelectorAll('[x-route]').forEach(collect);
|
|
680
|
+
});
|
|
681
|
+
return definedRoutes;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Whether any defined route (or its localized form) covers the path — drives x-route="!*"
|
|
685
|
+
function isRouteDefined(normalizedPath, definedRoutes) {
|
|
686
|
+
let defined = definedRoutes.some(route =>
|
|
685
687
|
window.ManifestRouting.matchesCondition(normalizedPath, route)
|
|
686
688
|
);
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (!isRouteDefined && localizationCodes.length > 0) {
|
|
689
|
+
const localizationCodes = getLocalizationCodes();
|
|
690
|
+
if (!defined && localizationCodes.length > 0) {
|
|
690
691
|
const pathSegments = normalizedPath.split('/').filter(segment => segment);
|
|
691
|
-
if (pathSegments.length > 0) {
|
|
692
|
-
const
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
);
|
|
703
|
-
} else {
|
|
704
|
-
// Check if the remaining path matches any defined route
|
|
705
|
-
isRouteDefined = definedRoutes.some(route =>
|
|
706
|
-
window.ManifestRouting.matchesCondition(remainingPath, route)
|
|
707
|
-
);
|
|
708
|
-
}
|
|
692
|
+
if (pathSegments.length > 0 && localizationCodes.includes(pathSegments[0])) {
|
|
693
|
+
const remainingPath = pathSegments.slice(1).join('/');
|
|
694
|
+
if (remainingPath === '') {
|
|
695
|
+
defined = definedRoutes.some(route =>
|
|
696
|
+
window.ManifestRouting.matchesCondition('/', route) ||
|
|
697
|
+
window.ManifestRouting.matchesCondition('', route)
|
|
698
|
+
);
|
|
699
|
+
} else {
|
|
700
|
+
defined = definedRoutes.some(route =>
|
|
701
|
+
window.ManifestRouting.matchesCondition(remainingPath, route)
|
|
702
|
+
);
|
|
709
703
|
}
|
|
710
704
|
}
|
|
711
705
|
}
|
|
706
|
+
return defined;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Match one route element against a path: true/false, or null when it carries no condition
|
|
710
|
+
function routeMatches(element, normalizedPath, defined) {
|
|
711
|
+
const routeCondition = element.getAttribute('x-route');
|
|
712
|
+
if (!routeCondition) return null;
|
|
713
|
+
|
|
714
|
+
const conditions = routeCondition.split(',').map(cond => cond.trim());
|
|
715
|
+
if (conditions.includes('!*')) {
|
|
716
|
+
if (defined === undefined) defined = isRouteDefined(normalizedPath, collectDefinedRoutes());
|
|
717
|
+
return !defined;
|
|
718
|
+
}
|
|
712
719
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
720
|
+
const positiveConditions = conditions.filter(cond => !cond.startsWith('!'));
|
|
721
|
+
const negativeConditions = conditions
|
|
722
|
+
.filter(cond => cond.startsWith('!'))
|
|
723
|
+
.map(cond => cond.slice(1));
|
|
724
|
+
const hasNegativeMatch = negativeConditions.some(cond =>
|
|
725
|
+
window.ManifestRouting.matchesCondition(normalizedPath, cond)
|
|
726
|
+
);
|
|
727
|
+
const hasPositiveMatch = positiveConditions.length === 0 || positiveConditions.some(cond =>
|
|
728
|
+
window.ManifestRouting.matchesCondition(normalizedPath, cond)
|
|
729
|
+
);
|
|
730
|
+
return hasPositiveMatch && !hasNegativeMatch;
|
|
731
|
+
}
|
|
716
732
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
.map(cond => cond.slice(1));
|
|
723
|
-
|
|
724
|
-
// Special handling for !* (undefined routes)
|
|
725
|
-
if (conditions.includes('!*')) {
|
|
726
|
-
const shouldShow = !isRouteDefined;
|
|
727
|
-
if (shouldShow) {
|
|
728
|
-
element.removeAttribute('hidden');
|
|
729
|
-
element.style.display = '';
|
|
730
|
-
} else {
|
|
731
|
-
element.setAttribute('hidden', '');
|
|
732
|
-
element.style.display = 'none';
|
|
733
|
-
}
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
733
|
+
// Cooperative check (defer plugin): is this route active for the current URL? Static MPA output always is.
|
|
734
|
+
function isRouteActive(element, normalizedPath) {
|
|
735
|
+
if (isPrerenderedStaticMPA()) return true;
|
|
736
|
+
return routeMatches(element, normalizedPath === undefined ? currentNormalizedPath() : normalizedPath) !== false;
|
|
737
|
+
}
|
|
736
738
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
);
|
|
739
|
+
// Activation hook fires before the route becomes visible so deferred content renders first
|
|
740
|
+
function showRoute(element) {
|
|
741
|
+
element.dispatchEvent(new CustomEvent('manifest:route-activate'));
|
|
742
|
+
element.removeAttribute('hidden');
|
|
743
|
+
element.style.display = '';
|
|
744
|
+
}
|
|
744
745
|
|
|
745
|
-
|
|
746
|
+
function hideRoute(element) {
|
|
747
|
+
element.setAttribute('hidden', '');
|
|
748
|
+
element.style.display = 'none';
|
|
749
|
+
}
|
|
746
750
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
+
// Process visibility for all elements with x-route
|
|
752
|
+
function processRouteVisibility(normalizedPath) {
|
|
753
|
+
// Static prerender output already contains only this route's sections; x-cloak + toggling here
|
|
754
|
+
// causes a visible flash (content → hidden via x-cloak → shown when Alpine boots).
|
|
755
|
+
if (isPrerenderedStaticMPA()) return;
|
|
756
|
+
|
|
757
|
+
const defined = isRouteDefined(normalizedPath, collectDefinedRoutes());
|
|
758
|
+
|
|
759
|
+
// Worklist: activating a deferred route can reveal nested routes that need this pass too
|
|
760
|
+
const seen = new Set();
|
|
761
|
+
const queue = Array.from(document.querySelectorAll('[x-route]'));
|
|
762
|
+
while (queue.length) {
|
|
763
|
+
const element = queue.shift();
|
|
764
|
+
if (seen.has(element)) continue;
|
|
765
|
+
seen.add(element);
|
|
766
|
+
const match = routeMatches(element, normalizedPath, defined);
|
|
767
|
+
if (match === null) continue;
|
|
768
|
+
if (match) {
|
|
769
|
+
showRoute(element);
|
|
770
|
+
element.querySelectorAll('[x-route]').forEach(nested => { if (!seen.has(nested)) queue.push(nested); });
|
|
751
771
|
} else {
|
|
752
|
-
element
|
|
753
|
-
element.style.display = 'none';
|
|
772
|
+
hideRoute(element);
|
|
754
773
|
}
|
|
755
|
-
}
|
|
774
|
+
}
|
|
756
775
|
}
|
|
757
776
|
|
|
758
777
|
// Add x-cloak to route elements that don't have it
|
|
@@ -770,9 +789,7 @@ function initializeVisibility() {
|
|
|
770
789
|
addXCloakToRouteElements();
|
|
771
790
|
|
|
772
791
|
// Process initial visibility (use logical path when app is in a subpath)
|
|
773
|
-
|
|
774
|
-
const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
|
|
775
|
-
processRouteVisibility(normalizedPath);
|
|
792
|
+
processRouteVisibility(currentNormalizedPath());
|
|
776
793
|
|
|
777
794
|
// Listen for route changes
|
|
778
795
|
window.addEventListener('manifest:route-change', (event) => {
|
|
@@ -785,10 +802,7 @@ function initializeVisibility() {
|
|
|
785
802
|
if (isPrerenderedStaticMPA()) return;
|
|
786
803
|
// Add x-cloak to any new route elements
|
|
787
804
|
addXCloakToRouteElements();
|
|
788
|
-
|
|
789
|
-
const currentPath = window.ManifestRoutingNavigation?.getCurrentRoute() ?? window.location.pathname;
|
|
790
|
-
const normalizedPath = currentPath === '/' ? '/' : currentPath.replace(/^\/|\/$/g, '');
|
|
791
|
-
processRouteVisibility(normalizedPath);
|
|
805
|
+
processRouteVisibility(currentNormalizedPath());
|
|
792
806
|
});
|
|
793
807
|
}
|
|
794
808
|
|
|
@@ -809,8 +823,10 @@ if (document.readyState === 'loading') {
|
|
|
809
823
|
window.ManifestRoutingVisibility = {
|
|
810
824
|
initialize: initializeVisibility,
|
|
811
825
|
processRouteVisibility,
|
|
826
|
+
isRouteActive,
|
|
812
827
|
isPrerenderedStaticMPA
|
|
813
|
-
};
|
|
828
|
+
};
|
|
829
|
+
|
|
814
830
|
|
|
815
831
|
// Router head
|
|
816
832
|
|
package/lib/manifest.virtual.js
CHANGED
|
@@ -421,7 +421,7 @@ function initializeVirtualPlugin() {
|
|
|
421
421
|
|
|
422
422
|
let resizeTimer = null;
|
|
423
423
|
const ro = new ResizeObserver(() => {
|
|
424
|
-
if (!ready) { render
|
|
424
|
+
if (!ready) { requestAnimationFrame(render); return; } // first paint once sized, off the observer tick
|
|
425
425
|
clearTimeout(resizeTimer);
|
|
426
426
|
resizeTimer = setTimeout(() => { rebuild(); render(); }, 100); // debounced re-pack
|
|
427
427
|
});
|