mnfst 0.5.199-next.1 → 0.5.199-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.
@@ -26,13 +26,14 @@ async function ensureManifest() {
26
26
  return window.__manifestLoaded;
27
27
  }
28
28
 
29
+ if (window.__manifestPromise) {
30
+ const shared = await window.__manifestPromise.catch(() => null);
31
+ if (shared) return shared;
32
+ }
29
33
  try {
30
34
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
31
- const response = await fetch(manifestUrl);
32
- const manifest = await response.json();
33
- // No-loader path: resolve ${VAR} placeholders the dynamic loader would have.
34
- window.ManifestDataConfig?.interpolateManifest?.(manifest);
35
- return manifest;
35
+ window.__manifestPromise = fetch(manifestUrl).then(r => r.json()).then(m => { window.ManifestDataConfig?.interpolateManifest?.(m); return m; });
36
+ return await window.__manifestPromise;
36
37
  } catch (error) {
37
38
  return null;
38
39
  }
@@ -2239,6 +2240,7 @@ function initializeTeamsCore() {
2239
2240
  this.error = null;
2240
2241
 
2241
2242
  try {
2243
+ invalidateRolesCache(teamId);
2242
2244
  const result = await this._appwrite.teams.updatePrefs({
2243
2245
  teamId: teamId,
2244
2246
  prefs: prefs
@@ -2950,6 +2952,7 @@ function initializeTeamsRolesDefaults() {
2950
2952
  roles: updatedRoles,
2951
2953
  deletedTemplateRoles: deletedRoles // Update deleted list in team preferences
2952
2954
  };
2955
+ invalidateRolesCache(teamId);
2953
2956
  await this._appwrite.teams.updatePrefs({
2954
2957
  teamId: teamId,
2955
2958
  prefs: updatedPrefs
@@ -3053,6 +3056,7 @@ function initializeTeamsRolesDefaults() {
3053
3056
  ...currentPrefs,
3054
3057
  roles: updatedRoles
3055
3058
  };
3059
+ invalidateRolesCache(teamId);
3056
3060
  await this._appwrite.teams.updatePrefs({
3057
3061
  teamId: teamId,
3058
3062
  prefs: updatedPrefs
@@ -3204,12 +3208,29 @@ function roleHasAllOwnerPermissions(roleName, memberRoles) {
3204
3208
  return OWNER_PERMISSIONS.every(perm => permissions.includes(perm));
3205
3209
  }
3206
3210
 
3207
- // Get user-generated roles from team preferences
3211
+ // User-generated roles live in team prefs; one read per team is shared by every permission check
3212
+ // for a short window and dropped on any auth change or prefs write
3213
+ const ROLES_CACHE_TTL_MS = 15000;
3214
+ const rolesCache = new Map(); // teamId -> { at, promise }
3215
+ function invalidateRolesCache(teamId) { if (teamId === undefined) rolesCache.clear(); else rolesCache.delete(teamId); }
3216
+ if (typeof window !== 'undefined') {
3217
+ ['manifest:auth:login', 'manifest:auth:logout', 'manifest:auth:anonymous', 'manifest:auth:session-cleared', 'manifest:auth:initialized', 'manifest:auth:teams-loaded']
3218
+ .forEach(type => window.addEventListener(type, () => invalidateRolesCache()));
3219
+ }
3220
+
3208
3221
  async function getUserGeneratedRoles(teamId, appwrite) {
3209
3222
  if (!appwrite || !appwrite.teams) {
3210
3223
  return null;
3211
3224
  }
3225
+ const hit = rolesCache.get(teamId);
3226
+ if (hit && Date.now() - hit.at < ROLES_CACHE_TTL_MS) return hit.promise;
3227
+ const promise = readUserGeneratedRoles(teamId, appwrite);
3228
+ rolesCache.set(teamId, { at: Date.now(), promise });
3229
+ promise.catch(() => rolesCache.delete(teamId)); // a null result (no custom roles) is a valid, cached answer
3230
+ return promise;
3231
+ }
3212
3232
 
3233
+ async function readUserGeneratedRoles(teamId, appwrite) {
3213
3234
  try {
3214
3235
  const prefs = await appwrite.teams.getPrefs({ teamId });
3215
3236
  return prefs?.roles || null;
@@ -3661,6 +3682,7 @@ function initializeTeamsUserRoles() {
3661
3682
  ...currentPrefs,
3662
3683
  roles: updatedRoles
3663
3684
  };
3685
+ invalidateRolesCache(teamId);
3664
3686
  await this._appwrite.teams.updatePrefs({
3665
3687
  teamId: teamId,
3666
3688
  prefs: updatedPrefs
@@ -3779,6 +3801,7 @@ function initializeTeamsUserRoles() {
3779
3801
  ...currentPrefs,
3780
3802
  roles: updatedRoles
3781
3803
  };
3804
+ invalidateRolesCache(teamId);
3782
3805
  await this._appwrite.teams.updatePrefs({
3783
3806
  teamId: teamId,
3784
3807
  prefs: updatedPrefs
@@ -3880,6 +3903,7 @@ function initializeTeamsUserRoles() {
3880
3903
  ...currentPrefs,
3881
3904
  roles: updatedRoles
3882
3905
  };
3906
+ invalidateRolesCache(teamId);
3883
3907
  await this._appwrite.teams.updatePrefs({
3884
3908
  teamId: teamId,
3885
3909
  prefs: updatedPrefs
@@ -4045,6 +4069,8 @@ function initializeTeamsUserRoles() {
4045
4069
  deletedTemplateRoles: deletedRoles
4046
4070
  };
4047
4071
 
4072
+ invalidateRolesCache(teamId);
4073
+
4048
4074
  await this._appwrite.teams.updatePrefs({
4049
4075
  teamId: teamId,
4050
4076
  prefs: updatedPrefs
@@ -4080,6 +4106,7 @@ function initializeTeamsUserRoles() {
4080
4106
  ...currentPrefs,
4081
4107
  roles: updatedRoles
4082
4108
  };
4109
+ invalidateRolesCache(teamId);
4083
4110
  await this._appwrite.teams.updatePrefs({
4084
4111
  teamId: teamId,
4085
4112
  prefs: updatedPrefs
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.199-next.1';
5
+ const MANIFEST_BUILD_VERSION = '0.5.199-next.3';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -35,6 +35,7 @@
35
35
  pending: new Map(), // conversationId | INDEX -> due time
36
36
  timer: null,
37
37
  saved: new Map(), // conversationId -> { messages, savedAt }
38
+ windows: new Map(), // conversationId -> { messages, savedAt } last known window (disk read or written snapshot) for synchronous re-opens
38
39
  generation: 0,
39
40
  watching: false,
40
41
  resetQueued: false
@@ -132,6 +133,7 @@
132
133
  state.indexPromise = null;
133
134
  state.indexDirty = false;
134
135
  state.saved.clear();
136
+ state.windows.clear();
135
137
  for (const h of state.handles.values()) { try { h.reset(); } catch (_) { } }
136
138
  cancel();
137
139
  }
@@ -158,25 +160,36 @@
158
160
  return state.indexPromise;
159
161
  }
160
162
 
161
- // Recency bump; beyond the cap the oldest conversations' records are deleted
163
+ // Move to the front of the index; beyond the cap the oldest conversations' records are deleted
164
+ function promote(recent, conversationId) {
165
+ const i = recent.indexOf(conversationId);
166
+ if (i === 0) return;
167
+ if (i > -1) recent.splice(i, 1);
168
+ recent.unshift(conversationId);
169
+ const evicted = recent.splice(state.config.conversations);
170
+ const r = records();
171
+ for (const id of evicted) { state.pending.delete(id); state.saved.delete(id); state.windows.delete(id); }
172
+ if (evicted.length) r.delete(evicted.map(id => r.key('chat', id))).catch(noop);
173
+ state.indexDirty = true;
174
+ state.pending.set(INDEX, Date.now() + WRITE_DEBOUNCE_MS);
175
+ schedule();
176
+ }
177
+
178
+ // Recency bump for a conversation already in the index; a new one earns its slot on its first non-empty write
162
179
  function opened(conversationId) {
163
180
  if (conversationId == null) return;
164
181
  const gen = state.generation;
165
182
  ensureIndex().then(recent => {
166
183
  if (gen !== state.generation || !enabled()) return;
167
- const i = recent.indexOf(conversationId);
168
- if (i > -1) recent.splice(i, 1);
169
- recent.unshift(conversationId);
170
- const evicted = recent.splice(state.config.conversations);
171
- const r = records();
172
- for (const id of evicted) { state.pending.delete(id); state.saved.delete(id); }
173
- if (evicted.length) r.delete(evicted.map(id => r.key('chat', id))).catch(noop);
174
- state.indexDirty = true;
175
- state.pending.set(INDEX, Date.now() + WRITE_DEBOUNCE_MS);
176
- schedule();
184
+ if (recent.includes(conversationId)) promote(recent, conversationId);
177
185
  }).catch(noop);
178
186
  }
179
187
 
188
+ // Last known window for a conversation this generation (synchronous; re-opens and handle swaps)
189
+ function peek(conversationId) {
190
+ return (enabled() && conversationId != null && state.windows.get(conversationId)) || null;
191
+ }
192
+
180
193
  // ---- hydrate (read path) ----------------------------------------------------
181
194
  // Record + index in one transaction on the first open; later opens read the record only
182
195
  function hydrate(conversationId) {
@@ -208,7 +221,9 @@
208
221
  return null;
209
222
  }
210
223
  state.saved.set(conversationId, { messages: rec.rows.length, savedAt: rec.savedAt });
211
- return { messages: rec.rows, savedAt: rec.savedAt };
224
+ const window_ = { messages: rec.rows, savedAt: rec.savedAt };
225
+ if (!state.windows.has(conversationId)) state.windows.set(conversationId, window_);
226
+ return window_;
212
227
  }
213
228
 
214
229
  // ---- snapshot (write path) --------------------------------------------------
@@ -267,14 +282,21 @@
267
282
  const gen = state.generation;
268
283
  const r = records();
269
284
  const savedAt = Date.now();
270
- const list = [];
285
+ const candidates = new Map();
271
286
  for (const id of due) {
272
287
  if (id === INDEX) continue;
273
288
  const h = state.handles.get(id);
274
- if (!h || !recent.includes(id)) continue;
289
+ if (!h) continue;
275
290
  let rows = null;
276
291
  try { const msgs = h.snapshot(); rows = msgs && msgs.length ? snapshotOf(msgs) : null; } catch (_) { rows = null; }
277
292
  if (!rows) continue;
293
+ if (!recent.includes(id)) promote(recent, id); // first non-empty write earns the slot
294
+ candidates.set(id, rows);
295
+ }
296
+ const list = [];
297
+ for (const [id, rows] of candidates) {
298
+ if (!recent.includes(id)) continue; // evicted by a later candidate in this same flush
299
+ state.windows.set(id, { messages: rows, savedAt });
278
300
  list.push({ key: r.key('chat', id), kind: 'chat', conversation: id, rows, savedAt });
279
301
  }
280
302
  if (state.indexDirty) { list.push({ key: r.key('chat'), kind: 'chat-index', recent: recent.slice(), savedAt }); state.indexDirty = false; }
@@ -317,7 +339,7 @@
317
339
 
318
340
  window.ManifestChatPersist = {
319
341
  WRITE_DEBOUNCE_MS,
320
- configure, bootstrap, enabled, hydrate, opened, touch, attach, anyStale, persistence, flushPending, reset,
342
+ configure, bootstrap, enabled, hydrate, opened, peek, touch, attach, anyStale, persistence, flushPending, reset,
321
343
  config: () => state.config,
322
344
  state
323
345
  };
@@ -429,10 +451,15 @@
429
451
  function commitParticipants() { state.participants = [..._participants.values()]; rev().n++; }
430
452
  function commitTyping() { state.typing = [..._typing.values()]; rev().n++; }
431
453
 
432
- // server echo reconciles body/media/status onto the local copy
454
+ // server echo reconciles body/media/status onto the local copy; meta merges so a
455
+ // re-delivery from another source (no translation, no channel) never drops fields
433
456
  function merge(target, incoming) {
434
457
  Object.assign(target.body, incoming.body);
435
- for (const k of Object.keys(incoming)) if (k !== 'body' && k !== '_seq') target[k] = incoming[k];
458
+ for (const k of Object.keys(incoming)) {
459
+ if (k === 'body' || k === '_seq') continue;
460
+ if (k === 'meta' && target.meta && incoming.meta && typeof incoming.meta === 'object') { Object.assign(target.meta, incoming.meta); continue; }
461
+ target[k] = incoming[k];
462
+ }
436
463
  }
437
464
 
438
465
  // own echo beating the ack: claim the pending row by meta.clientId
@@ -562,12 +589,14 @@
562
589
  commit(); commitParticipants(); commitTyping();
563
590
  }
564
591
 
565
- if (P() && !isAggregate) detach = P().attach(conversationId, { snapshot, reset, stale: () => state.stale, count: () => _msgs.length });
592
+ if (P() && !isAggregate && opts.persistWindow !== false) detach = P().attach(conversationId, { snapshot, reset, stale: () => state.stale, count: () => _msgs.length });
566
593
 
567
594
  async function open() {
568
595
  state.status = 'loading';
569
596
  if (detach) {
570
- P().hydrate(conversationId).then(h => { if (h) hydrateWindow(h.messages); }).catch(() => { });
597
+ const known = P().peek(conversationId); // re-open or handle swap: window already in memory, no read to lose the race
598
+ if (known) hydrateWindow(known.messages);
599
+ else P().hydrate(conversationId).then(h => { if (h) hydrateWindow(h.messages); }).catch(() => { });
571
600
  P().opened(conversationId);
572
601
  }
573
602
  try {
@@ -61,6 +61,7 @@ window.ManifestComponentsRegistry = {
61
61
  // manifest on window yet). This must be async — a synchronous XHR on
62
62
  // the main thread is deprecated and was flagged by PageSpeed.
63
63
  let manifest = window.__manifestLoaded || this.manifest;
64
+ if (!manifest && window.__manifestPromise) manifest = await window.__manifestPromise.catch(() => null);
64
65
  if (!manifest) {
65
66
  try {
66
67
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
@@ -72,6 +73,7 @@ window.ManifestComponentsRegistry = {
72
73
  manifest = await res.json();
73
74
  // No-loader path: resolve ${VAR} placeholders the dynamic loader would have.
74
75
  window.ManifestDataConfig?.interpolateManifest?.(manifest);
76
+ window.__manifestPromise = Promise.resolve(manifest); // share with plugins that init after us
75
77
  } else {
76
78
  console.warn('[Manifest] Failed to load manifest.json (HTTP', res.status + ')');
77
79
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.199-next.1';
5
+ const MANIFEST_BUILD_VERSION = '0.5.199-next.3';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -21,12 +21,15 @@ async function ensureManifest() {
21
21
  return window.__manifestLoaded;
22
22
  }
23
23
 
24
+ // One request per boot, shared with the loader and the other plugins (window.__manifestPromise)
25
+ if (window.__manifestPromise) {
26
+ const shared = await window.__manifestPromise.catch(() => null);
27
+ if (looksComplete(shared)) return shared;
28
+ }
24
29
  try {
25
30
  const manifestUrl = (document.querySelector('link[rel="manifest"]')?.getAttribute('href')) || '/manifest.json';
26
- const response = await fetch(manifestUrl);
27
- const manifest = await response.json();
28
- interpolateManifest(manifest);
29
- return manifest;
31
+ window.__manifestPromise = fetch(manifestUrl).then(r => r.json()).then(m => { interpolateManifest(m); return m; });
32
+ return await window.__manifestPromise;
30
33
  } catch (error) {
31
34
  console.error('[Manifest Data] Failed to load manifest:', error);
32
35
  return null;
@@ -1536,6 +1539,11 @@ window.ManifestDataStore = {
1536
1539
  sources: new Map(), // source -> config (+ rows/savedAt bookkeeping)
1537
1540
  filters: new Map(), // source -> row filter
1538
1541
  scopeExpr: null,
1542
+ scopePending: false, // a scope expression that has not resolved yet (auth still booting): no keys, no wipes, no resets
1543
+ authExpected: false, // manifest.appwrite.auth present: the scope may still resolve once identity/teams load
1544
+ teamsExpected: false,
1545
+ authSettled: false,
1546
+ settleTimer: null,
1539
1547
  scopeFn: null,
1540
1548
  scope: '',
1541
1549
  generation: 0,
@@ -1634,7 +1642,9 @@ window.ManifestDataStore = {
1634
1642
  state.dbName = `manifest:${origin}${projectId ? `:${projectId}` : ''}`;
1635
1643
  state.enabled = (state.sources.size > 0 || state.external) && typeof indexedDB !== 'undefined' && !!indexedDB;
1636
1644
  if (!state.enabled) return false;
1645
+ watchAuthSettle(manifest);
1637
1646
  state.scope = evaluateScope();
1647
+ state.scopePending = pendingScope(state.scope);
1638
1648
  watchScope();
1639
1649
  return state.sources.size > 0;
1640
1650
  }
@@ -1648,6 +1658,7 @@ window.ManifestDataStore = {
1648
1658
  if (!state.dbName) state.dbName = `manifest:${(typeof location !== 'undefined' && location.origin) || 'null'}`;
1649
1659
  if (!state.frameworkVersion) state.frameworkVersion = buildVersion;
1650
1660
  state.scope = evaluateScope();
1661
+ state.scopePending = pendingScope(state.scope);
1651
1662
  watchScope();
1652
1663
  return true;
1653
1664
  }
@@ -1692,6 +1703,36 @@ window.ManifestDataStore = {
1692
1703
  });
1693
1704
  }
1694
1705
 
1706
+ // With an expression configured and auth still booting, an empty value means "not known yet"
1707
+ function pendingScope(value) { return !!state.scopeExpr && value === '' && state.authExpected && !state.authSettled; }
1708
+
1709
+ // Identity and teams are known (or will never load): an empty scope is now a real single-tenant scope
1710
+ function settleAuth() {
1711
+ if (state.authSettled) return;
1712
+ state.authSettled = true;
1713
+ if (state.settleTimer) { clearTimeout(state.settleTimer); state.settleTimer = null; }
1714
+ if (state.enabled && state.scopePending) changeScope(evaluateScope());
1715
+ }
1716
+
1717
+ function watchAuthSettle(manifest) {
1718
+ state.authExpected = !!(manifest && manifest.appwrite && manifest.appwrite.auth);
1719
+ state.teamsExpected = !!(state.authExpected && manifest.appwrite.auth.teams);
1720
+ state.authSettled = false;
1721
+ if (!state.authExpected || typeof window === 'undefined') { state.authSettled = true; return; }
1722
+ const auth = (typeof Alpine !== 'undefined' && Alpine.store) ? Alpine.store('auth') : null;
1723
+ if (auth && auth._initialized && (!state.teamsExpected || !auth.isAuthenticated || (auth.teams && auth.teams.length))) { state.authSettled = true; return; }
1724
+ if (!state.settleWatching) {
1725
+ state.settleWatching = true;
1726
+ window.addEventListener('manifest:auth:teams-loaded', settleAuth);
1727
+ window.addEventListener('manifest:auth:initialized', (e) => { if (!state.teamsExpected || !(e.detail && e.detail.isAuthenticated)) settleAuth(); });
1728
+ window.addEventListener('manifest:auth:logout', settleAuth);
1729
+ window.addEventListener('manifest:auth:session-cleared', settleAuth);
1730
+ window.addEventListener('manifest:auth:anonymous', () => { if (!state.teamsExpected) settleAuth(); });
1731
+ }
1732
+ if (state.settleTimer) clearTimeout(state.settleTimer);
1733
+ state.settleTimer = setTimeout(settleAuth, 10000); // never pending forever
1734
+ }
1735
+
1695
1736
  function evaluateScope() {
1696
1737
  if (!state.scopeExpr) return '';
1697
1738
  try {
@@ -1734,19 +1775,27 @@ window.ManifestDataStore = {
1734
1775
  // cleared (older loads discarded), new scope hydrates
1735
1776
  function changeScope(next) {
1736
1777
  const prev = state.scope;
1778
+ const wasPending = state.scopePending;
1737
1779
  state.scope = next;
1780
+ state.scopePending = pendingScope(next);
1738
1781
  state.generation++;
1739
1782
  cancelWrites();
1740
1783
  state.hydrated.clear();
1741
1784
  state.fetchKicked.clear();
1742
- const ds = dataStore();
1743
- for (const [source, cfg] of state.sources) {
1744
- cfg.rows = null;
1745
- cfg.savedAt = null;
1746
- ds?.resetSource?.(source);
1785
+ // pending → resolved is boot, not a switch: rows loaded meanwhile stay (hydrate skips fresh sources), nothing is wiped
1786
+ if (!wasPending) {
1787
+ const ds = dataStore();
1788
+ for (const [source, cfg] of state.sources) {
1789
+ cfg.rows = null;
1790
+ cfg.savedAt = null;
1791
+ ds?.resetSource?.(source);
1792
+ }
1793
+ if (prev !== next) deleteScope(prev);
1794
+ }
1795
+ if (!state.scopePending) {
1796
+ hydrateBoot();
1797
+ if (wasPending) onLanded([...state.sources.keys()]); // rows that landed while pending get written under the real scope
1747
1798
  }
1748
- if (prev !== next) deleteScope(prev);
1749
- hydrateBoot();
1750
1799
  if (typeof window !== 'undefined') {
1751
1800
  try { window.dispatchEvent(new CustomEvent('manifest:persist:scope', { detail: { scope: next, previous: prev } })); } catch { /* no-op */ }
1752
1801
  }
@@ -1948,7 +1997,7 @@ window.ManifestDataStore = {
1948
1997
 
1949
1998
  // Landing hook (store flush): each landing restarts that source's 500ms debounce
1950
1999
  function onLanded(sources) {
1951
- if (!state.enabled || state.disabled) return;
2000
+ if (!state.enabled || state.disabled || state.scopePending) return; // nothing is keyed under an unresolved scope
1952
2001
  const at = Date.now() + WRITE_DEBOUNCE_MS;
1953
2002
  for (const source of sources) if (state.sources.has(source)) state.pending.set(source, at);
1954
2003
  if (state.writeTimer !== null) { clearTimeout(state.writeTimer); state.writeTimer = null; }
@@ -1971,7 +2020,7 @@ window.ManifestDataStore = {
1971
2020
  async function hydrate(sources) {
1972
2021
  const ds = dataStore();
1973
2022
  const pending = sources.filter(source => state.sources.has(source) && !state.hydrated.has(source));
1974
- if (!ds || !pending.length || !state.enabled || state.disabled) return;
2023
+ if (!ds || !pending.length || !state.enabled || state.disabled || state.scopePending) return;
1975
2024
  for (const source of pending) state.hydrated.add(source);
1976
2025
  const generation = state.generation;
1977
2026
  const scope = state.scope;
@@ -2002,7 +2051,7 @@ window.ManifestDataStore = {
2002
2051
 
2003
2052
  // Boot tier: all keys in one read; the caller caps the wait (cold boot never blocks)
2004
2053
  function hydrateBoot(options = {}) {
2005
- if (!state.enabled || state.disabled) return Promise.resolve();
2054
+ if (!state.enabled || state.disabled || state.scopePending) return Promise.resolve();
2006
2055
  const run = hydrate(bootSources()).catch(() => { /* disabled */ });
2007
2056
  const maxWaitMs = options.maxWaitMs;
2008
2057
  if (!Number.isFinite(maxWaitMs)) return run;
@@ -1,17 +1,17 @@
1
1
  {
2
- "manifest.appwrite.auth.js": "sha384-lWvffK6C2Zs9dOiMh/0OQTnU2ycZq1USOeJDlPUgJsbtrdA9+3zq4VHcIdS40qR0",
3
- "manifest.appwrite.data.js": "sha384-C2EDm2EsuWW9PTtvXtU1nTBrDAUaIjWc3fY5YuuHsRV0MjRlZ0u9J0UYuQW4g41o",
2
+ "manifest.appwrite.auth.js": "sha384-HMJmRXnvdDBWyuteXL+DTxWM2llSCdjl4EDamEI7iHNAVlqeOCE4GDTm8ZHipPRj",
3
+ "manifest.appwrite.data.js": "sha384-dLQJibkJHbQh7N2j73pCWHSUl2j/O3M0W/Zuge0d7VYjUBXv0PlW55Wylhk6YEsp",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-qQrjhWkIDi4oU5uQrpy8DdlMjY+gfiivBuX6mssNiFdkawgkVBvKneGgp+nd3PYJ",
7
- "manifest.chat.js": "sha384-1c6+WsGivAlFSeTegFLmL0hjs48hIjlpLP7bWW/cjK+glQlWIYq76KfJiBBHRRxj",
7
+ "manifest.chat.js": "sha384-mD8exNWzRhkGW07xBxLyDagykP5knEx/i0/trG47rM2n1XJJ0t3SlJSskwcFmbm6",
8
8
  "manifest.code.js": "sha384-aoQ4E26vrhNe/+tL7K0h90c8/0ies7e8oIR6Ay92he2X3dnWZ+OPcn2Dna8fmc+c",
9
9
  "manifest.color.js": "sha384-6Rv3LxyTcZNjrhtayQfqRdCx0uSZ4BiEbgEI98I62eTvp8Aw7LBIoNJ0Je1oktwL",
10
10
  "manifest.colorpicker.js": "sha384-1+ZV5Hhjf7WnGP7b71Cqa5h3BKW8UgGT5V14i8Q+aF5zH8Xxwowt/ibSUVsiIHSm",
11
11
  "manifest.combobox.js": "sha384-SN3XHumoebxcduFQqx5zt65/Cd10y8nuf9Gkco6UnsZ42sadRd+gvVLfWPdkkuPH",
12
- "manifest.components.js": "sha384-HEyeJtV20k7po8WH0LEj800MlsqgDH0vE5GTAl62SgdEhn1aJ7VfryQcA6yIi18U",
12
+ "manifest.components.js": "sha384-DLGSqCbnGkS87AWJ8GpKoAFP4nCosgC5SFSg6C10hH/nWu/MxI+0ntHlnFEfs++u",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-5hH1ym307YXcSqTGfUKoRPnj1lZI6bjnkRr/okJhWLy+QyUN7pcrOe6TbNxKOPCU",
14
+ "manifest.data.js": "sha384-KV4OtKbidiW4Am0EZBdMApC4JPetFLnscK23r4L+qUoCzCL7CLjmzRPP1+vRXcCI",
15
15
  "manifest.datepicker.js": "sha384-8vTJYkLqqvgXgm1w1becuaRR/jUEpiLBjBJ6RsRbUr+ZkypvspD6EsssuDrX4SQq",
16
16
  "manifest.defer.js": "sha384-t/g++osVS3qDXeY8+gfxUtLF9o0SCQQJKNxHGpb8NkIciDvxOAvA6sFIVcM0AKeA",
17
17
  "manifest.dropdowns.js": "sha384-4HuHkBt4i8sTPUDLB9rNLli2/lWLY1tjtCLzey37gRwueG+FRLCnVH1q7ljiVvJR",
@@ -21,13 +21,13 @@
21
21
  "manifest.localization.js": "sha384-p8MRlfpjQuMar7rerGRjfdRKqIPoU/QOwVmnwN+PjhrlEZNpetYLZhqO2i5BZn9s",
22
22
  "manifest.markdown.js": "sha384-HFE2VwBRX5vhDGQ3c8XgZ10Y5ENkXxHzgX9DjmIEffqLoUDbnJYNk3TFWT9k3e+f",
23
23
  "manifest.native.js": "sha384-nIngj4CM5hV4bvQvgs8C7JmSy1UUzobsbhvDE2NH5dXqQVNW/Hiat34wHUxMqSaO",
24
- "manifest.payments.js": "sha384-QGctMZtxF0Wb2JUJbb9sJ4bFbyQvUdr9HaKs4afyaTXb7zedAAIWeB27XINMe3VZ",
24
+ "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
25
25
  "manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
26
26
  "manifest.router.js": "sha384-4qmGEkRpgAo9xoQkW6+hHIhowvcyAggKB2J3Wf8tJtXp+L0hufoVAqzfhnqw4cNR",
27
27
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
28
- "manifest.status.js": "sha384-fXqDnKwldB7XRxcfEQYTm/WqLl2oP24xY2Znq8FqnFM9LQo0zrQkXszqAOksArUX",
28
+ "manifest.status.js": "sha384-nwvUSbT+gpRMqhYx6MeBYswUMH72rY92mjgxv0vrSG7Ht7l83XYpEB6fCTbXjjfK",
29
29
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
30
- "manifest.sw.js": "sha384-99glzEjzhOxUy6yUID+Exx7mHVs78XKdoPRL7nWvdXpdX9uMgFHWSIWBinuRYfml",
30
+ "manifest.sw.js": "sha384-s7ZLcM3CZ8Yyt+KsC9Mc1bSgvL7JJsNfWRHBEldBOEWvrRDohTRPDWSVqXprHf3k",
31
31
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
32
32
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
33
33
  "manifest.text.edit.js": "sha384-1LsuOJyMCgab7W9w6DJEu5URzbadYxtq/cHtB8sN3hBFa/BHPnjdnTRbiKyM0FcC",
@@ -36,5 +36,5 @@
36
36
  "manifest.url.parameters.js": "sha384-J2aSVBOPsoqsbafHndrWLHEDpKb4AKJAoQnVmKA4gZh5P/8HHjFWHO/lkusmG84l",
37
37
  "manifest.utilities.js": "sha384-RvUXxxMLv5q6Psng6jjfK5O1xw05mV6BdLjV92zQW7Zuy3XWctl3QnF5XYNuPyYq",
38
38
  "manifest.virtual.js": "sha384-YwwmAsZNddajnAkLhBWtrK8edHMJOhD7cJkQi3qmm5o1Thg0wbtDU0dsfrCHKTIQ",
39
- "manifest.js": "sha384-jicPS2+j2BtRtkdXC8YqGOQFi/V/E9MMjJFQ8mbDult9IG2cyzwq191W+zIl137b"
39
+ "manifest.js": "sha384-uZNE076xOWVEeO51qX8XpEMh+OQZEkGd3AI0yuy4skn320IkuX1j9F1THVuOgzgf"
40
40
  }
package/lib/manifest.js CHANGED
@@ -710,6 +710,7 @@
710
710
  const devServer = !!window.__mnfstRun;
711
711
  const devOrigin = isDevHost(loc.hostname);
712
712
  let manifest = window.__manifestLoaded || null;
713
+ if (!manifest && window.__manifestPromise) manifest = await window.__manifestPromise.catch(() => null);
713
714
  if (!manifest) {
714
715
  const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
715
716
  manifest = await fetch(url).then(r => r.ok ? r.json() : null).catch(() => null);
@@ -829,13 +830,26 @@
829
830
  walk(obj);
830
831
  };
831
832
 
833
+ // One manifest.json request per boot: published on window the moment it starts so
834
+ // plugins that init before __manifestLoaded is set (data, auth, components) await it
835
+ // instead of fetching their own. Resolves interpolated, or null.
836
+ const shareManifestFetch = () => {
837
+ if (window.__manifestLoaded) return Promise.resolve(window.__manifestLoaded);
838
+ if (!window.__manifestPromise) {
839
+ window.__manifestPromise = fetch(manifestUrl).then(r => r.ok ? r.json() : null)
840
+ .then(m => { if (m && !m.__interpolated) { interpolateManifestEnv(m); Object.defineProperty(m, '__interpolated', { value: true, enumerable: false }); } return m; })
841
+ .catch(() => null);
842
+ }
843
+ return window.__manifestPromise;
844
+ };
845
+
832
846
  const loadPlugins = async () => {
833
847
  let manifest = null;
834
848
  let pluginsToLoad = config.plugins;
835
849
  let manifestPromise = null;
836
850
 
837
851
  if (config.deriveFromManifest) {
838
- manifest = await fetch(manifestUrl).then(r => r.ok ? r.json() : null).catch(() => null);
852
+ manifest = await shareManifestFetch();
839
853
  const corePlugins = getDefaultPluginsFromManifest(manifest);
840
854
  const appwritePlugins = detectAppwritePlugins(manifest);
841
855
  const paymentsPlugins = detectPaymentsPlugins(manifest);
@@ -846,7 +860,7 @@
846
860
  pluginsToLoad = resolveDependencies([...ALWAYS_ON, ...pluginsToLoad.filter(p => !ALWAYS_ON.includes(p))]);
847
861
  const needsManifest = config.plugins.some(p => MANIFEST_DEPENDENT_PLUGINS.includes(p));
848
862
  if (needsManifest) {
849
- manifestPromise = fetch(manifestUrl).then(r => r.ok ? r.json() : null).catch(() => null);
863
+ manifestPromise = shareManifestFetch();
850
864
  }
851
865
  // Inside a Capacitor container, ensure the native umbrella loads even on
852
866
  // the explicit data-plugins path (matches the derive-path auto-inject).
@@ -868,9 +882,7 @@
868
882
  manifest = await manifestPromise;
869
883
  }
870
884
  if (manifest && typeof window !== 'undefined') {
871
- // Resolve ${VAR} before any plugin reads the cached manifest —
872
- // appwrite-auth etc. read window.__manifestLoaded directly.
873
- interpolateManifestEnv(manifest);
885
+ // Already interpolated by shareManifestFetch; appwrite-auth etc. read window.__manifestLoaded directly.
874
886
  window.__manifestLoaded = manifest;
875
887
  if (window.ManifestComponentsRegistry) {
876
888
  window.ManifestComponentsRegistry.manifest = manifest;
@@ -21,10 +21,11 @@ function resolvedOrNull(value, fieldName) {
21
21
  async function ensureManifest() {
22
22
  if (window.ManifestComponentsRegistry?.manifest) return window.ManifestComponentsRegistry.manifest;
23
23
  if (window.__manifestLoaded) return window.__manifestLoaded;
24
+ if (window.__manifestPromise) { const shared = await window.__manifestPromise.catch(() => null); if (shared) return shared; }
24
25
  try {
25
26
  const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
26
- const res = await fetch(url);
27
- return await res.json();
27
+ window.__manifestPromise = fetch(url).then(r => r.json());
28
+ return await window.__manifestPromise;
28
29
  } catch (_) {
29
30
  return null;
30
31
  }
@@ -64,13 +64,14 @@
64
64
  // cache a normalized copy that omits keys they don't consume.
65
65
  async function ensureStatusManifest() {
66
66
  const cached = window.ManifestComponentsRegistry?.manifest || window.__manifestLoaded;
67
- if (cached && cached.status) return cached;
67
+ if (cached) return cached; // a loaded manifest without `status` means no status config, not a stale copy
68
+ if (window.__manifestPromise) { const shared = await window.__manifestPromise.catch(() => null); if (shared) return shared; }
68
69
  try {
69
70
  const url = document.querySelector('link[rel="manifest"]')?.getAttribute('href') || '/manifest.json';
70
- const res = await fetch(url);
71
- return await res.json();
71
+ window.__manifestPromise = fetch(url).then(r => r.json());
72
+ return await window.__manifestPromise;
72
73
  } catch (_) {
73
- return cached || null;
74
+ return null;
74
75
  }
75
76
  }
76
77
 
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.199-next.1';
19
+ const BUILD_VERSION = '0.5.199-next.3';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  const SWR_CAP = 200;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.199-next.1",
3
+ "version": "0.5.199-next.3",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",