mnfst 0.5.198-next.1 → 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.
@@ -14,8 +14,31 @@ const killed = !!document.querySelector('script[data-defer="off"]') || window.__
14
14
 
15
15
  const pending = new Set();
16
16
  let ready = false;
17
+ let bootDrained = false;
17
18
  let idleHandle = null;
18
19
 
20
+ // Urgent = mounted after boot, within a gesture window (a pane the user just opened), capped per gesture
21
+ const URGENT_WINDOW_MS = 1500;
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
27
+ let lastGestureAt = -Infinity;
28
+ let urgentWindowAt = -Infinity;
29
+ let urgentInWindow = 0;
30
+ let promoteTimer = null;
31
+ let lastPromoteAt = -Infinity;
32
+ let gestureX = 0, gestureY = 0;
33
+ let idleIsTimer = false;
34
+ const now = () => (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
35
+
36
+ function isUrgent() {
37
+ if (!ready || !bootDrained || now() - lastGestureAt > URGENT_WINDOW_MS) return false;
38
+ if (urgentWindowAt !== lastGestureAt) { urgentWindowAt = lastGestureAt; urgentInWindow = 0; }
39
+ return urgentInWindow++ < URGENT_CAP;
40
+ }
41
+
19
42
  // ---- Detection ----
20
43
 
21
44
  const isPopoverOpen = (el) => { try { return el.matches(':popover-open'); } catch (_) { return false; } };
@@ -87,7 +110,7 @@ function register(el, rule, opts) {
87
110
  lazy: !!(opts && opts.lazy),
88
111
  discard: !!(opts && opts.discard),
89
112
  priority: opts ? opts.priority : 0,
90
- urgent: ready, // mounted after ready → the user just made it appear
113
+ urgent: isUrgent(),
91
114
  rendered: false,
92
115
  cleanup: []
93
116
  };
@@ -96,10 +119,11 @@ function register(el, rule, opts) {
96
119
  return rec;
97
120
  }
98
121
 
99
- function render(rec) {
100
- if (rec.rendered) return;
122
+ function render(rec, viaPrewarm) {
123
+ if (rec.rendered) { if (!viaPrewarm && warm.delete(rec)) schedule(); return; }
101
124
  rec.rendered = true;
102
125
  pending.delete(rec);
126
+ if (viaPrewarm) warm.add(rec); else warm.delete(rec);
103
127
  const { el, tpl } = rec;
104
128
  const content = rec.discard ? tpl.content.cloneNode(true) : tpl.content;
105
129
  Alpine.mutateDom(() => {
@@ -121,6 +145,38 @@ function teardown(rec) {
121
145
  });
122
146
  }
123
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
+
124
180
  // ---- Open signals ----
125
181
 
126
182
  function wire(rec) {
@@ -162,39 +218,117 @@ function wire(rec) {
162
218
 
163
219
  // ---- Idle prewarm (after manifest:ready; urgent first, then priority asc, then document order) ----
164
220
 
165
- const idle = (fn, timeout) => window.requestIdleCallback
166
- ? window.requestIdleCallback(fn, { timeout })
167
- : 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
+ }
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
+ }
168
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
+ }
169
288
  function next() {
170
- let best = null;
289
+ let best = null, bestRank = 0;
171
290
  for (const rec of pending) {
172
291
  if (!rec.el.isConnected) { pending.delete(rec); continue; }
292
+ if (!reachable(rec.el)) continue;
293
+ const r = rank(rec);
173
294
  if (!best
174
- || (rec.urgent && !best.urgent)
175
- || (rec.urgent === best.urgent && (rec.priority < best.priority
295
+ || r < bestRank
296
+ || (r === bestRank && (rec.priority < best.priority
176
297
  || (rec.priority === best.priority && (best.el.compareDocumentPosition(rec.el) & Node.DOCUMENT_POSITION_PRECEDING))))) {
177
- best = rec;
298
+ best = rec; bestRank = r;
178
299
  }
179
300
  }
180
301
  return best;
181
302
  }
182
303
 
183
304
  function schedule() {
184
- if (idleHandle !== null || !ready || pending.size === 0) return;
305
+ if (pending.size === 0) { if (ready) bootDrained = true; return; }
306
+ if (idleHandle !== null || !ready) return;
185
307
  const head = next();
186
- 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) => {
187
311
  idleHandle = null;
188
- const rec = next();
189
- 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();
190
323
  schedule();
191
- }, head && head.urgent ? 100 : 1000);
324
+ }, head.urgent ? 100 : 500);
192
325
  }
193
326
 
194
327
  function markReady() {
195
328
  if (ready) return;
196
329
  ready = true;
197
330
  schedule();
331
+ setTimeout(() => { bootDrained = true; }, 5000); // lazy components may keep the boot queue busy
198
332
  }
199
333
 
200
334
  function armReady() {
@@ -227,6 +361,7 @@ function intercept(el, skip) {
227
361
  function initializeDeferPlugin() {
228
362
  Alpine.directive('defer', () => { });
229
363
  if (!killed) Alpine.interceptInit(Alpine.skipDuringClone(intercept));
364
+ ['pointerdown', 'keydown'].forEach((type) => document.addEventListener(type, onGesture, { capture: true, passive: true }));
230
365
  armReady();
231
366
  }
232
367
 
@@ -234,7 +369,8 @@ window.ManifestDefer = {
234
369
  enabled: !killed,
235
370
  defer,
236
371
  render: (el) => { if (el && el.__mnfstDefer) render(el.__mnfstDefer); },
237
- 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; })() })
238
374
  };
239
375
  if (window.Manifest && typeof window.Manifest === 'object') window.Manifest.defer = defer;
240
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-wjQ3a8ClODUJi4xlBRI03G7qWOF+DN+fJQXyxcp1jekNwrMqbgshR0LSCgkJJUiH",
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.1",
3
+ "version": "0.5.198-next.3",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",