mnfst 0.5.198-next.2 → 0.5.198-next.3

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.
@@ -20,9 +20,17 @@ let idleHandle = null;
20
20
  // Urgent = mounted after boot, within a gesture window (a pane the user just opened), capped per gesture
21
21
  const URGENT_WINDOW_MS = 1500;
22
22
  const URGENT_CAP = 8;
23
+ const PREWARM_CAP = (window.ManifestDeferConfig && window.ManifestDeferConfig.prewarmCap) || 48; // warm-but-unopened containers kept live
24
+ const IDLE_BUDGET_MS = 4;
25
+ const SLICE_MAX = 8;
26
+ const warm = new Set(); // prewarmed, never opened by the user
23
27
  let lastGestureAt = -Infinity;
24
28
  let urgentWindowAt = -Infinity;
25
29
  let urgentInWindow = 0;
30
+ let promoteTimer = null;
31
+ let lastPromoteAt = -Infinity;
32
+ let gestureX = 0, gestureY = 0;
33
+ let idleIsTimer = false;
26
34
  const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
27
35
 
28
36
  function isUrgent() {
@@ -111,10 +119,11 @@ function register(el, rule, opts) {
111
119
  return rec;
112
120
  }
113
121
 
114
- function render(rec) {
115
- if (rec.rendered) return;
122
+ function render(rec, viaPrewarm) {
123
+ if (rec.rendered) { if (!viaPrewarm && warm.delete(rec)) schedule(); return; }
116
124
  rec.rendered = true;
117
125
  pending.delete(rec);
126
+ if (viaPrewarm) warm.add(rec); else warm.delete(rec);
118
127
  const { el, tpl } = rec;
119
128
  const content = rec.discard ? tpl.content.cloneNode(true) : tpl.content;
120
129
  Alpine.mutateDom(() => {
@@ -136,6 +145,38 @@ function teardown(rec) {
136
145
  });
137
146
  }
138
147
 
148
+ // Evict a warm, still-closed, never-opened container back to its stash (least reachable first)
149
+ function restash(rec) {
150
+ if (!rec.rendered) return;
151
+ rec.rendered = false;
152
+ rec.near = undefined;
153
+ const { el, tpl } = rec;
154
+ Alpine.mutateDom(() => {
155
+ Array.from(el.children).forEach((child) => Alpine.destroyTree(child));
156
+ while (el.firstChild) tpl.content.appendChild(el.firstChild);
157
+ el.appendChild(tpl);
158
+ });
159
+ warm.delete(rec);
160
+ pending.add(rec);
161
+ }
162
+
163
+ function evict() {
164
+ if (warm.size <= PREWARM_CAP) return;
165
+ for (const rec of warm) rec.near = undefined;
166
+ while (warm.size > PREWARM_CAP) {
167
+ let victim = null, score = -1;
168
+ for (const rec of warm) {
169
+ if (!rec.el.isConnected) { warm.delete(rec); continue; }
170
+ if (rec.rule === 'popover' && isPopoverOpen(rec.el)) continue;
171
+ if (rec.near === undefined) rec.near = nearViewport(rec);
172
+ const sc = (reachable(rec.el) ? 0 : 2) + (rec.near ? 0 : 1);
173
+ if (sc > score) { victim = rec; score = sc; }
174
+ }
175
+ if (!victim) return;
176
+ restash(victim);
177
+ }
178
+ }
179
+
139
180
  // ---- Open signals ----
140
181
 
141
182
  function wire(rec) {
@@ -177,19 +218,84 @@ function wire(rec) {
177
218
 
178
219
  // ---- Idle prewarm (after manifest:ready; urgent first, then priority asc, then document order) ----
179
220
 
180
- const idle = (fn, timeout) => window.requestIdleCallback
181
- ? window.requestIdleCallback(fn, { timeout })
182
- : setTimeout(fn, 50);
221
+ const idle = (fn, timeout) => {
222
+ idleIsTimer = !window.requestIdleCallback;
223
+ return idleIsTimer
224
+ ? setTimeout(() => fn({ didTimeout: true, timeRemaining: () => 0 }), 50)
225
+ : window.requestIdleCallback(fn, { timeout });
226
+ };
227
+ function cancelIdle() {
228
+ if (idleHandle === null) return;
229
+ if (idleIsTimer) clearTimeout(idleHandle); else if (window.cancelIdleCallback) window.cancelIdleCallback(idleHandle);
230
+ idleHandle = null;
231
+ }
183
232
 
233
+ // A gesture that revealed a pane (tab switch, panel open) makes its containers urgent
234
+ function promoteNearViewport() {
235
+ promoteTimer = null;
236
+ if (!ready || !bootDrained) return;
237
+ // The on-screen containers nearest the gesture point (a revealed pane sits under the click)
238
+ const candidates = [];
239
+ for (const rec of pending) {
240
+ if (rec.urgent || !rec.el.isConnected || !reachable(rec.el)) continue;
241
+ const r = anchorRect(rec);
242
+ rec.near = !!r && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
243
+ if (!rec.near) continue;
244
+ const dx = Math.max(r.left - gestureX, 0, gestureX - r.right);
245
+ const dy = Math.max(r.top - gestureY, 0, gestureY - r.bottom);
246
+ candidates.push([dx * dx + dy * dy, rec]);
247
+ }
248
+ if (!candidates.length) return;
249
+ candidates.sort((a, b) => (a[1].priority - b[1].priority) || (a[0] - b[0])); // authored priority first, then nearest
250
+ let n = 0;
251
+ for (const [, rec] of candidates) { if (n++ >= URGENT_CAP) break; rec.urgent = true; }
252
+ cancelIdle();
253
+ schedule();
254
+ }
255
+ function onGesture(e) {
256
+ lastGestureAt = now();
257
+ if (e && typeof e.clientX === 'number' && (e.clientX || e.clientY)) { gestureX = e.clientX; gestureY = e.clientY; }
258
+ if (promoteTimer !== null || now() - lastPromoteAt < 500) return;
259
+ lastPromoteAt = now();
260
+ promoteTimer = setTimeout(promoteNearViewport, 150);
261
+ }
262
+
263
+ // Only containers the user can reach: not under a hidden route; visible parents first
264
+ const reachable = (el) => !el.closest('[x-route][hidden]');
265
+ // Judge by the invoker (popovertarget / aria-controls), else the nearest ancestor with a box
266
+ // Rect of the invoker or the nearest boxed ancestor (null when nothing has a box)
267
+ function anchorRect(rec) {
268
+ const el = rec.el;
269
+ if (!rec.invoker && el.id && window.CSS && CSS.escape) {
270
+ const id = CSS.escape(el.id);
271
+ rec.invoker = document.querySelector(`[popovertarget="${id}"], [aria-controls="${id}"]`) || null;
272
+ }
273
+ let p = (rec.invoker && rec.invoker.isConnected) ? rec.invoker : el.parentElement;
274
+ for (let depth = 0; p && depth < 6; p = p.parentElement, depth++) {
275
+ const r = p.getBoundingClientRect();
276
+ if (r.width || r.height) return r;
277
+ }
278
+ return null;
279
+ }
280
+ function nearViewport(rec) {
281
+ const r = anchorRect(rec);
282
+ return !!r && r.bottom > 0 && r.top < window.innerHeight && r.right > 0 && r.left < window.innerWidth;
283
+ }
284
+ function rank(rec) {
285
+ if (rec.near === undefined) rec.near = nearViewport(rec);
286
+ return (rec.urgent ? 0 : 4) + (rec.near ? 0 : 2);
287
+ }
184
288
  function next() {
185
- let best = null;
289
+ let best = null, bestRank = 0;
186
290
  for (const rec of pending) {
187
291
  if (!rec.el.isConnected) { pending.delete(rec); continue; }
292
+ if (!reachable(rec.el)) continue;
293
+ const r = rank(rec);
188
294
  if (!best
189
- || (rec.urgent && !best.urgent)
190
- || (rec.urgent === best.urgent && (rec.priority < best.priority
295
+ || r < bestRank
296
+ || (r === bestRank && (rec.priority < best.priority
191
297
  || (rec.priority === best.priority && (best.el.compareDocumentPosition(rec.el) & Node.DOCUMENT_POSITION_PRECEDING))))) {
192
- best = rec;
298
+ best = rec; bestRank = r;
193
299
  }
194
300
  }
195
301
  return best;
@@ -199,12 +305,23 @@ function schedule() {
199
305
  if (pending.size === 0) { if (ready) bootDrained = true; return; }
200
306
  if (idleHandle !== null || !ready) return;
201
307
  const head = next();
202
- idleHandle = idle(() => {
308
+ if (!head) { if (ready) bootDrained = true; return; }
309
+ if (!head.urgent && warm.size >= PREWARM_CAP) return; // paused at the cap; resumes when a warm one opens or evicts
310
+ idleHandle = idle((deadline) => {
203
311
  idleHandle = null;
204
- const rec = next();
205
- if (rec) render(rec);
312
+ // Forced fire (never-idle page): one container. Genuine idle: batch within the budget.
313
+ let n = 0;
314
+ do {
315
+ const rec = next();
316
+ if (!rec) break;
317
+ if (!rec.urgent && warm.size >= PREWARM_CAP) break; // cap = max warm; only urgent renders may exceed it
318
+ for (const other of pending) other.near = undefined;
319
+ render(rec, true);
320
+ n++;
321
+ } while (deadline && !deadline.didTimeout && deadline.timeRemaining() > IDLE_BUDGET_MS && n < SLICE_MAX);
322
+ evict();
206
323
  schedule();
207
- }, head && head.urgent ? 100 : 1000);
324
+ }, head.urgent ? 100 : 500);
208
325
  }
209
326
 
210
327
  function markReady() {
@@ -244,7 +361,7 @@ function intercept(el, skip) {
244
361
  function initializeDeferPlugin() {
245
362
  Alpine.directive('defer', () => { });
246
363
  if (!killed) Alpine.interceptInit(Alpine.skipDuringClone(intercept));
247
- ['pointerdown', 'keydown'].forEach((type) => document.addEventListener(type, () => { lastGestureAt = now(); }, { capture: true, passive: true }));
364
+ ['pointerdown', 'keydown'].forEach((type) => document.addEventListener(type, onGesture, { capture: true, passive: true }));
248
365
  armReady();
249
366
  }
250
367
 
@@ -252,7 +369,8 @@ window.ManifestDefer = {
252
369
  enabled: !killed,
253
370
  defer,
254
371
  render: (el) => { if (el && el.__mnfstDefer) render(el.__mnfstDefer); },
255
- isPending: (el) => !!(el && el.__mnfstDefer && !el.__mnfstDefer.rendered)
372
+ isPending: (el) => !!(el && el.__mnfstDefer && !el.__mnfstDefer.rendered),
373
+ stats: () => ({ pending: pending.size, warm: warm.size, cap: PREWARM_CAP, ready, bootDrained, armed: idleHandle !== null, head: (() => { const h = next(); return h ? (h.el.id || h.el.tagName) : null; })() })
256
374
  };
257
375
  if (window.Manifest && typeof window.Manifest === 'object') window.Manifest.defer = defer;
258
376
 
@@ -12,7 +12,7 @@
12
12
  "manifest.computed.js": "sha384-LDxER8pyYyYwUxIYIyiJA02s+49g8CzpSlOj28On0DFIUgetT3UmvE/9xOsTwT66",
13
13
  "manifest.data.js": "sha384-urUiANuQfrc+AfwTB7+nMneF2C1+EfV+S5+eg4PQnZCJ0Aid50KwSARufiVrGAXd",
14
14
  "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
15
- "manifest.defer.js": "sha384-IxVROtFvzgYYP/zfSTULfKW9HfUBBu02pjXPfJ/myyKoAU/lj59qFVNq0WAHNLZI",
15
+ "manifest.defer.js": "sha384-szMa04R4gWsZl3nICHz3Mtku8vgROEHvrAIMap+wuqITKvMVQHJOa0kVzHHjMcwN",
16
16
  "manifest.dropdowns.js": "sha384-4HuHkBt4i8sTPUDLB9rNLli2/lWLY1tjtCLzey37gRwueG+FRLCnVH1q7ljiVvJR",
17
17
  "manifest.edit.js": "sha384-ZvgV3ax7yHtSC23zdvRYJPzBsBpGFBLuBJMgfQ1IDumv97Cn2bpHOePxyQikfl5L",
18
18
  "manifest.export.js": "sha384-HCy3n6ifrcAGIyzTRgZ5JIRvSX9q9/8ykMKRSbt1QTnhXz6PgYlV0AE2N2Yi2re+",
@@ -33,6 +33,6 @@
33
33
  "manifest.tooltips.js": "sha384-uXVVQADl6pY2Pj9i/UaSclZN7dzbKhLkmevv7Ad0+wXR8LgqodALNFrc5jZJjlwm",
34
34
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
35
35
  "manifest.utilities.js": "sha384-YcGKTEviTv9FLkw890CTociA7Rjp6xuhLyfpn3Gk4mT0fexDFqpW7VSuohcpGfsw",
36
- "manifest.virtual.js": "sha384-OPHiW1zQxunoL+yJrCxFU3AzTfTlZQfUf6X3XSRrSyNDPlr3BvsYe6vZa0DlOjoY",
36
+ "manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
37
37
  "manifest.js": "sha384-IaRa1ZZNj8C/mcB9e6qNiTqK7oriQr0Vmt4Q7FcGWOqfUgPxojYixbs+g8XR9lNz"
38
38
  }
@@ -421,7 +421,7 @@ function initializeVirtualPlugin() {
421
421
 
422
422
  let resizeTimer = null;
423
423
  const ro = new ResizeObserver(() => {
424
- if (!ready) { render(); return; } // first paint once sized
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.198-next.2",
3
+ "version": "0.5.198-next.3",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",