mnfst 0.5.222 → 0.5.224

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.
@@ -218,6 +218,32 @@ function initializeAuthStore() {
218
218
 
219
219
  // Cross-tab synchronization using localStorage events
220
220
  const STORAGE_KEY = 'manifest:auth:state';
221
+ // Cap on holding scoped data for a guest/callback sign-in that may fail
222
+ const SIGN_IN_PENDING_MS = 5000;
223
+ ['manifest:auth:login', 'manifest:auth:anonymous', 'manifest:auth:logout', 'manifest:auth:session-cleared'].forEach(type =>
224
+ window.addEventListener(type, () => { const store = Alpine.store('auth'); if (store) { store._signInPendingUntil = 0; store._authEpoch++; markVerified(store); } }));
225
+
226
+ // Unverified session: recheck on reconnect, on return to the tab, and on a bounded backoff
227
+ const RECHECK_DELAYS_MS = [2000, 5000, 15000, 60000];
228
+ const recheck = () => { Alpine.store('auth')?._recheckSession?.(); };
229
+ window.addEventListener('online', recheck);
230
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') recheck(); });
231
+
232
+ function markVerified(store) {
233
+ store._sessionUnverified = false;
234
+ store._recheckAttempt = 0;
235
+ if (store._recheckTimer) { clearTimeout(store._recheckTimer); store._recheckTimer = null; }
236
+ }
237
+
238
+ function scheduleRecheck(store) {
239
+ if (!store._sessionUnverified || store._recheckTimer) return;
240
+ const delay = RECHECK_DELAYS_MS[Math.min(store._recheckAttempt++, RECHECK_DELAYS_MS.length - 1)];
241
+ store._recheckTimer = setTimeout(() => { store._recheckTimer = null; store._recheckSession(); }, delay);
242
+ }
243
+
244
+ // Appwrite's "no session" answer; anything else (offline, 5xx) leaves the session unknown
245
+ const NO_SESSION_TYPES = ['general_unauthorized_scope', 'user_unauthorized', 'user_session_not_found', 'user_jwt_invalid', 'user_not_found', 'user_blocked'];
246
+ const isNoSession = error => error?.code === 401 || NO_SESSION_TYPES.includes(error?.type);
221
247
 
222
248
  // Session fields safe to mirror across tabs. Excludes `secret` and provider
223
249
  // tokens — this copy is only for UI cross-tab sync, not the auth of record.
@@ -243,7 +269,10 @@ function initializeAuthStore() {
243
269
  const state = JSON.parse(e.newValue);
244
270
  const store = Alpine.store('auth');
245
271
  if (store) {
272
+ const prevId = store.user?.$id || null;
246
273
  // Update store state from other tab
274
+ store._authEpoch++;
275
+ if (state.isAuthenticated) markVerified(store);
247
276
  store.isAuthenticated = state.isAuthenticated;
248
277
  store.isAnonymous = state.isAnonymous;
249
278
  store.user = state.user;
@@ -253,6 +282,15 @@ function initializeAuthStore() {
253
282
  store.otpSent = state.otpSent || false;
254
283
  store.otpExpired = state.otpExpired || false;
255
284
  store.error = state.error;
285
+ // Identity ended or changed in another tab: scoped data must drop this tab's rows
286
+ const nextId = state.isAuthenticated ? (state.user?.$id || null) : null;
287
+ if (prevId && nextId !== prevId) {
288
+ if (!nextId) {
289
+ store.teams = [];
290
+ store.currentTeam = null;
291
+ }
292
+ window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
293
+ }
256
294
  }
257
295
  } catch (error) {
258
296
  // Failed to sync state from other tab
@@ -262,6 +300,8 @@ function initializeAuthStore() {
262
300
 
263
301
  // Helper to sync state to localStorage (for cross-tab communication)
264
302
  function syncStateToStorage(store) {
303
+ // An unverified signed-out state must not sign other tabs out
304
+ if (store._sessionUnverified && !store.isAuthenticated) return;
265
305
  try {
266
306
  const state = {
267
307
  isAuthenticated: store.isAuthenticated,
@@ -340,6 +380,11 @@ function initializeAuthStore() {
340
380
  _appwrite: null,
341
381
  _guestAuto: false,
342
382
  _guestManual: false,
383
+ _signInPendingUntil: 0, // signed-out init with guest-auto or an auth callback still to sign in
384
+ _sessionUnverified: false, // init could not reach Appwrite: signed out locally, never broadcast
385
+ _authEpoch: 0, // bumped by every identity change; an in-flight recheck from an older epoch is dropped
386
+ _recheckTimer: null,
387
+ _recheckAttempt: 0,
343
388
  guestManualEnabled: false,
344
389
  _oauthProvider: null, // Store OAuth provider name (google, github, etc.) when login is initiated
345
390
  _syncStateToStorage: syncStateToStorage,
@@ -545,57 +590,18 @@ function initializeAuthStore() {
545
590
 
546
591
  // Try to restore existing session
547
592
  try {
548
- this.user = await appwrite.account.get();
549
- const sessionsResponse = await appwrite.account.listSessions();
550
- const allSessions = sessionsResponse.sessions || [];
551
- const currentSession = allSessions.find(s => s.current === true) || allSessions[0];
552
-
553
- if (currentSession) {
554
- this.session = currentSession;
555
- this.isAuthenticated = true;
556
- this.isAnonymous = currentSession.provider === 'anonymous';
557
-
558
- // Restore OAuth provider from storage (persists across redirects/refresh)
559
- if (!this.isAnonymous && currentSession.provider !== 'magic-url') {
560
- try {
561
- // Try localStorage first (persists across redirects), fallback to sessionStorage
562
- let storedProvider = localStorage.getItem('manifest:oauth:provider');
563
- if (!storedProvider) {
564
- storedProvider = sessionStorage.getItem('manifest:oauth:provider');
565
- }
566
- if (storedProvider) {
567
- this._oauthProvider = storedProvider;
568
- }
569
- } catch (e) {
570
- // Storage error
571
- }
572
- }
573
-
574
- // If guest is disabled but we have anonymous session, clear it
575
- if (this.isAnonymous && !this._guestAuto && !this._guestManual) {
576
- try {
577
- await appwrite.account.deleteSession(this.session.$id);
578
- this.isAuthenticated = false;
579
- this.isAnonymous = false;
580
- this.user = null;
581
- this.session = null;
582
- } catch (deleteError) {
583
- // Failed to delete guest session
584
- }
585
- }
586
- } else {
587
- this.isAuthenticated = true; // User exists, session might be managed by cookies
588
- this.isAnonymous = false;
589
- }
593
+ await this._restoreSession(appwrite);
590
594
 
591
595
  // Team loading is deferred (not awaited) — runs in the background after
592
596
  // manifest:auth:initialized so a session gate isn't held up by it.
593
597
  } catch (error) {
594
- // No existing session - this is expected
595
- this.isAuthenticated = false;
596
- this.isAnonymous = false;
597
- this.user = null;
598
- this.session = null;
598
+ this._clearIdentity();
599
+ // Signed out for now; other tabs keep their session
600
+ if (!isNoSession(error)) {
601
+ this._sessionUnverified = true;
602
+ scheduleRecheck(this);
603
+ console.warn('[Manifest Appwrite Auth] Session check failed (signed out until reachable):', error?.message || error);
604
+ }
599
605
  }
600
606
 
601
607
  // Sync state to localStorage
@@ -611,6 +617,17 @@ function initializeAuthStore() {
611
617
  this._initialized = true;
612
618
  this._initializing = false;
613
619
 
620
+ // Guest-auto / callback sign-in follows: scoped data holds instead of flashing signed-out
621
+ if (!this.isAuthenticated) {
622
+ let callback = false;
623
+ try {
624
+ // Team invites need an existing session (acceptInvite refuses signed-out), so they don't sign in
625
+ const info = window.ManifestAppwriteAuthCallbacks?.detect?.();
626
+ callback = !!(info?.hasCallback && !info.isTeamInvite);
627
+ } catch (e) { /* no-op */ }
628
+ if (this._guestAuto || callback) this._signInPendingUntil = Date.now() + SIGN_IN_PENDING_MS;
629
+ }
630
+
614
631
  // Fire as soon as identity is known, before teams load, so a session
615
632
  // gate / splash clears in a few hundred ms.
616
633
  window.dispatchEvent(new CustomEvent('manifest:auth:initialized', {
@@ -633,6 +650,95 @@ function initializeAuthStore() {
633
650
  }
634
651
  },
635
652
 
653
+ // Adopt the session Appwrite reports (throws when there is none or it is unreachable)
654
+ async _restoreSession(appwrite, isCurrent = () => true) {
655
+ const user = await appwrite.account.get();
656
+ const sessionsResponse = await appwrite.account.listSessions();
657
+ if (!isCurrent()) return false;
658
+ this.user = user;
659
+ const allSessions = sessionsResponse.sessions || [];
660
+ const currentSession = allSessions.find(s => s.current === true) || allSessions[0];
661
+
662
+ if (currentSession) {
663
+ this.session = currentSession;
664
+ this.isAuthenticated = true;
665
+ this.isAnonymous = currentSession.provider === 'anonymous';
666
+
667
+ // Restore OAuth provider from storage (persists across redirects/refresh)
668
+ if (!this.isAnonymous && currentSession.provider !== 'magic-url') {
669
+ try {
670
+ // Try localStorage first (persists across redirects), fallback to sessionStorage
671
+ let storedProvider = localStorage.getItem('manifest:oauth:provider');
672
+ if (!storedProvider) {
673
+ storedProvider = sessionStorage.getItem('manifest:oauth:provider');
674
+ }
675
+ if (storedProvider) {
676
+ this._oauthProvider = storedProvider;
677
+ }
678
+ } catch (e) {
679
+ // Storage error
680
+ }
681
+ }
682
+
683
+ // If guest is disabled but we have anonymous session, clear it
684
+ if (this.isAnonymous && !this._guestAuto && !this._guestManual) {
685
+ try {
686
+ await appwrite.account.deleteSession(this.session.$id);
687
+ this.isAuthenticated = false;
688
+ this.isAnonymous = false;
689
+ this.user = null;
690
+ this.session = null;
691
+ } catch (deleteError) {
692
+ // Failed to delete guest session
693
+ }
694
+ }
695
+ } else {
696
+ this.isAuthenticated = true; // User exists, session might be managed by cookies
697
+ this.isAnonymous = false;
698
+ }
699
+ return true;
700
+ },
701
+
702
+ // Offline-booted tab: re-run the session check once Appwrite is reachable
703
+ async _recheckSession() {
704
+ if (!this._sessionUnverified || this._rechecking || !this._appwrite) return;
705
+ if (this._recheckTimer) { clearTimeout(this._recheckTimer); this._recheckTimer = null; }
706
+ this._rechecking = true;
707
+ const epoch = this._authEpoch;
708
+ const current = () => this._authEpoch === epoch && this._sessionUnverified && !this.isAuthenticated;
709
+ try {
710
+ if (!await this._restoreSession(this._appwrite, current)) return;
711
+ } catch (error) {
712
+ if (!current()) return;
713
+ if (!isNoSession(error)) { scheduleRecheck(this); return; }
714
+ this._clearIdentity();
715
+ markVerified(this);
716
+ syncStateToStorage(this);
717
+ if (this._guestAuto && this._createAnonymousSession) await this._createAnonymousSession();
718
+ return;
719
+ } finally {
720
+ this._rechecking = false;
721
+ }
722
+ markVerified(this);
723
+ const cfg = await config.getAppwriteConfig();
724
+ if (this._authEpoch !== epoch) return;
725
+ if (this.isAuthenticated && cfg?.teams && (!this.isAnonymous || cfg.guestTeams)) {
726
+ try { await this._loadTeamsAndSeed(cfg); } catch (e) { console.warn('[Manifest Appwrite Auth] Team load failed:', e?.message || e); }
727
+ if (this._authEpoch !== epoch) return;
728
+ }
729
+ syncStateToStorage(this);
730
+ if (this.isAuthenticated) {
731
+ window.dispatchEvent(new CustomEvent(this.isAnonymous ? 'manifest:auth:anonymous' : 'manifest:auth:login', { detail: { user: this.user } }));
732
+ }
733
+ },
734
+
735
+ _clearIdentity() {
736
+ this.isAuthenticated = false;
737
+ this.isAnonymous = false;
738
+ this.user = null;
739
+ this.session = null;
740
+ },
741
+
636
742
  // Clear team state when the identity changes to a different user (e.g. guest
637
743
  // replaced on OTP sign-in). Otherwise stale currentTeam/teams cause 404s.
638
744
  _resetTeamsState() {
@@ -804,6 +910,8 @@ function initializeAuthStore() {
804
910
  return { success: true };
805
911
  }
806
912
 
913
+ this._authEpoch++;
914
+ markVerified(this);
807
915
  this.inProgress = true;
808
916
 
809
917
  try {
@@ -847,15 +955,12 @@ function initializeAuthStore() {
847
955
 
848
956
  // Restore to guest state after logout (guest-auto only, and not when
849
957
  // already a guest — don't mint a new guest on top).
958
+ let restored = false;
850
959
  if (!this.isAnonymous && this._guestAuto && this._createAnonymousSession) {
851
960
  await this._createAnonymousSession();
852
- } else {
853
- // Clear auth state completely
854
- this.isAuthenticated = false;
855
- this.isAnonymous = false;
856
- this.user = null;
857
- this.session = null;
961
+ restored = this.isAuthenticated && this.isAnonymous;
858
962
  }
963
+ if (!restored) this._clearIdentity();
859
964
 
860
965
  syncStateToStorage(this);
861
966
  window.dispatchEvent(new CustomEvent('manifest:auth:logout'));
@@ -893,6 +998,10 @@ function initializeAuthStore() {
893
998
  // Clear teams on logout error too
894
999
  this.teams = [];
895
1000
  this.currentTeam = null;
1001
+ if (!this.isAuthenticated) this._clearIdentity();
1002
+ // Expired session (401) is still a logout: scoped data must drop the old rows
1003
+ syncStateToStorage(this);
1004
+ window.dispatchEvent(new CustomEvent('manifest:auth:logout'));
896
1005
  return { success: false, error: error.message };
897
1006
  } finally {
898
1007
  this.inProgress = false;
@@ -905,6 +1014,8 @@ function initializeAuthStore() {
905
1014
  return { success: false, error: 'Appwrite not configured' };
906
1015
  }
907
1016
 
1017
+ this._authEpoch++;
1018
+ markVerified(this);
908
1019
  this.inProgress = true;
909
1020
 
910
1021
  try {
@@ -944,6 +1055,10 @@ function initializeAuthStore() {
944
1055
  this.session = null;
945
1056
  this.magicLinkSent = false;
946
1057
  this.magicLinkExpired = false;
1058
+ this.teams = [];
1059
+ this.currentTeam = null;
1060
+ syncStateToStorage(this);
1061
+ window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
947
1062
  return { success: false, error: error.message };
948
1063
  } finally {
949
1064
  this.inProgress = false;
@@ -958,10 +1073,12 @@ function initializeAuthStore() {
958
1073
 
959
1074
  try {
960
1075
  this.user = await this._appwrite.account.get();
1076
+ if (this.isAuthenticated) markVerified(this);
961
1077
  syncStateToStorage(this);
962
1078
  return this.user;
963
1079
  } catch (error) {
964
- // Session may have expired
1080
+ if (!isNoSession(error)) throw error;
1081
+ markVerified(this);
965
1082
  this.isAuthenticated = false;
966
1083
  this.isAnonymous = false;
967
1084
  this.user = null;
@@ -1038,8 +1155,8 @@ async function initializeAppwriteAuthPlugin() {
1038
1155
  }
1039
1156
  }
1040
1157
 
1041
- // If no session and guest-auto is enabled, create guest session
1042
- if (!store.isAuthenticated && store._guestAuto && store._createAnonymousSession) {
1158
+ // If no session and guest-auto is enabled, create guest session (not while the session check is unreachable)
1159
+ if (!store.isAuthenticated && !store._sessionUnverified && store._guestAuto && store._createAnonymousSession) {
1043
1160
  store._createAnonymousSession();
1044
1161
  }
1045
1162
  }, { once: true });
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.222';
5
+ const MANIFEST_BUILD_VERSION = '0.5.224';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -671,7 +671,8 @@ window.ManifestDataAppwrite = {
671
671
  const subscriptions = new Map(); // Map<dataSourceName, unsubscribeFunction>
672
672
 
673
673
  // Subscribe to storage bucket file changes
674
- async function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent) {
674
+ // isCurrent: false once the requesting load is superseded (logout) — skip, never touch a newer entry
675
+ async function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent, isCurrent) {
675
676
  // Unsubscribe from existing subscription if any
676
677
  if (subscriptions.has(dataSourceName)) {
677
678
  const unsubscribe = subscriptions.get(dataSourceName);
@@ -682,6 +683,7 @@ async function subscribeToStorageBucket(dataSourceName, bucketId, scope, onEvent
682
683
  }
683
684
 
684
685
  const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
686
+ if (isCurrent && !isCurrent()) return null;
685
687
  if (!services?.realtime) {
686
688
  console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
687
689
  return null;
@@ -751,7 +753,7 @@ function unsubscribeAll() {
751
753
  }
752
754
 
753
755
  // Subscribe to database table row changes
754
- async function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent) {
756
+ async function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEvent, isCurrent) {
755
757
  // Unsubscribe from existing subscription if any
756
758
  if (subscriptions.has(dataSourceName)) {
757
759
  const unsubscribe = subscriptions.get(dataSourceName);
@@ -762,6 +764,7 @@ async function subscribeToTable(dataSourceName, databaseId, tableId, scope, onEv
762
764
  }
763
765
 
764
766
  const services = await window.ManifestDataAppwrite.getAppwriteDataServices();
767
+ if (isCurrent && !isCurrent()) return null;
765
768
  if (!services?.realtime) {
766
769
  console.warn('[Manifest Data] Realtime service not available for', dataSourceName);
767
770
  return null;
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.222';
5
+ const MANIFEST_BUILD_VERSION = '0.5.224';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -590,8 +590,14 @@ function writeSource(dataSourceName, data, options = {}) {
590
590
  }
591
591
 
592
592
  // Synchronous replace: local writes ($register, init preload, state-only updates)
593
+ // A write captured under an older generation (scope reset since, e.g. logout) never lands
594
+ function isStaleWrite(dataSourceName, options) {
595
+ return !!options && options.generation !== undefined && options.generation !== sourceGeneration(dataSourceName);
596
+ }
597
+
593
598
  function updateStore(dataSourceName, data, options = {}) {
594
599
  if (isInitializing && !options.allowDuringInit) return;
600
+ if (isStaleWrite(dataSourceName, options)) return;
595
601
  const state = writeSource(dataSourceName, data, { ...options, mode: 'replace' });
596
602
  if (!state) return;
597
603
  touchSource(dataSourceName);
@@ -601,10 +607,12 @@ function updateStore(dataSourceName, data, options = {}) {
601
607
  // Network landing (page load, paged append, realtime batch): buffered, applied
602
608
  // with every other landing of the same frame in ONE flush. Resolves once visible.
603
609
  function landRows(dataSourceName, rows, options = {}) {
610
+ if (isStaleWrite(dataSourceName, options)) return Promise.resolve();
604
611
  return queueLanding({ source: dataSourceName, rows, options: { mode: 'replace', ...options } });
605
612
  }
606
613
 
607
614
  function landRemove(dataSourceName, ids, options = {}) {
615
+ if (isStaleWrite(dataSourceName, options)) return Promise.resolve();
608
616
  return queueLanding({ source: dataSourceName, remove: Array.isArray(ids) ? ids : [ids], options });
609
617
  }
610
618
 
@@ -1132,6 +1140,11 @@ function setupTeamChangeListener() {
1132
1140
  }
1133
1141
  };
1134
1142
 
1143
+ // Logout already reset scoped sources (data main); don't reload them again as a team change
1144
+ const syncTeam = () => { lastTeamId = Alpine.store('auth')?.currentTeam?.$id || null; };
1145
+ window.addEventListener('manifest:auth:logout', syncTeam);
1146
+ window.addEventListener('manifest:auth:session-cleared', syncTeam);
1147
+
1135
1148
  if (document.readyState === 'loading') {
1136
1149
  document.addEventListener('DOMContentLoaded', startPolling);
1137
1150
  } else {
@@ -1188,10 +1201,15 @@ function checkAndDispatchRenderReady() {
1188
1201
  }, RENDER_READY_QUIET_MS);
1189
1202
  }
1190
1203
 
1204
+ // Latest locale change owns _localeChanging; an older one finishing must not clear it
1205
+ let localeChangeSeq = 0;
1206
+
1191
1207
  // Listen for locale changes to reload data
1192
1208
  function setupLocaleChangeListener() {
1193
1209
  window.addEventListener('localechange', async (event) => {
1194
1210
  const newLocale = event.detail.locale;
1211
+ const seq = ++localeChangeSeq;
1212
+ const latest = () => seq === localeChangeSeq;
1195
1213
 
1196
1214
  // Set loading state to prevent flicker
1197
1215
  const store = Alpine.store('data');
@@ -1293,7 +1311,7 @@ function setupLocaleChangeListener() {
1293
1311
  delete store[dataSourceName];
1294
1312
  delete store[`_${dataSourceName}_state`];
1295
1313
  });
1296
- store._localeChanging = false;
1314
+ if (latest()) store._localeChanging = false;
1297
1315
  touchSources(store, localizedDataSources);
1298
1316
  }
1299
1317
 
@@ -1308,7 +1326,7 @@ function setupLocaleChangeListener() {
1308
1326
 
1309
1327
  // All localized sources have reloaded — check if everything is settled.
1310
1328
  // This fires manifest:render-ready after a locale change completes end-to-end.
1311
- checkAndDispatchRenderReady();
1329
+ if (latest()) checkAndDispatchRenderReady();
1312
1330
 
1313
1331
  } catch (error) {
1314
1332
  console.error('[Manifest Data] Error handling locale change:', error);
@@ -1323,13 +1341,13 @@ function setupLocaleChangeListener() {
1323
1341
  if (!key.startsWith('_') && typeof raw[key] !== 'function') delete store[key];
1324
1342
  }
1325
1343
  store._initialized = true;
1326
- store._localeChanging = false;
1344
+ if (latest()) store._localeChanging = false;
1327
1345
  bumpAllVersions();
1328
1346
  }
1329
1347
  } finally {
1330
1348
  // Early exits (no manifest.data) must not strand the render-ready gate
1331
1349
  const store = Alpine.store('data');
1332
- if (store?._localeChanging) {
1350
+ if (latest() && store?._localeChanging) {
1333
1351
  store._localeChanging = false;
1334
1352
  checkAndDispatchRenderReady();
1335
1353
  }
@@ -1506,6 +1524,7 @@ window.ManifestDataStore = {
1506
1524
  sourceFreshness,
1507
1525
  // Persistence (§12.2)
1508
1526
  sourceGeneration,
1527
+ isStaleWrite,
1509
1528
  resetSource,
1510
1529
  touchSource,
1511
1530
  bumpAllVersions,
@@ -1576,6 +1595,7 @@ window.ManifestDataStore = {
1576
1595
  pending: new Map(), // source -> time its debounced write is due
1577
1596
  writeTimer: null,
1578
1597
  hydrated: new Set(), // sources hydrated (or attempted) this generation
1598
+ hydratedIdentity: null, // identity auth-dependent hydration last ran under
1579
1599
  fetchKicked: new Set(),
1580
1600
  watching: false,
1581
1601
  deployment: null,
@@ -1589,6 +1609,26 @@ window.ManifestDataStore = {
1589
1609
  const liveLocale = () => (typeof document !== 'undefined' && document.documentElement?.lang)
1590
1610
  || (typeof Alpine !== 'undefined' && Alpine.store?.('locale')?.current) || 'en';
1591
1611
 
1612
+ // Auth-dependent sources (scope / $auth. queries) carry the identity they were read under
1613
+ function isAuthDependent(source) {
1614
+ if (!source || typeof source !== 'object') return false;
1615
+ if (source.scope) return true;
1616
+ try { return JSON.stringify(source.queries || '').includes('$auth.'); } catch { return false; }
1617
+ }
1618
+
1619
+ // Current identity: '' signed out (or no auth plugin), null while auth is still booting
1620
+ function authIdentity() {
1621
+ const auth = (typeof Alpine !== 'undefined' && Alpine.store) ? Alpine.store('auth') : null;
1622
+ if (!auth) return '';
1623
+ if (auth._initialized !== true) return null;
1624
+ return auth.isAuthenticated === true ? String(auth.user?.$id || auth.user?.id || '') : '';
1625
+ }
1626
+
1627
+ function authUnverified() {
1628
+ const auth = (typeof Alpine !== 'undefined' && Alpine.store) ? Alpine.store('auth') : null;
1629
+ return auth?._sessionUnverified === true;
1630
+ }
1631
+
1592
1632
  function warnOnce(key, message, error) {
1593
1633
  if (state.warned.has(key)) return;
1594
1634
  state.warned.add(key);
@@ -1657,7 +1697,7 @@ window.ManifestDataStore = {
1657
1697
  for (const [name, source] of Object.entries(manifest?.data || {})) {
1658
1698
  if (!source || typeof source !== 'object') continue;
1659
1699
  const cfg = normalizeConfig(source.persist);
1660
- if (cfg) state.sources.set(name, cfg);
1700
+ if (cfg) { cfg.authDependent = isAuthDependent(source); state.sources.set(name, cfg); }
1661
1701
  }
1662
1702
  state.scopeExpr = typeof manifest?.persistence?.scope === 'string' && manifest.persistence.scope.trim()
1663
1703
  ? manifest.persistence.scope.trim() : null;
@@ -1787,6 +1827,7 @@ window.ManifestDataStore = {
1787
1827
  if (!state.enabled) return;
1788
1828
  if (WIPE_EVENTS.has(type)) deleteScope(state.scope);
1789
1829
  refreshScope();
1830
+ if (!state.scopePending) hydrate(bootSources()).catch(() => { /* disabled */ });
1790
1831
  });
1791
1832
  }
1792
1833
  // Reactive re-evaluation (team switches that fire no event); the switch itself runs outside the effect
@@ -1963,6 +2004,8 @@ window.ManifestDataStore = {
1963
2004
  if (!cfg || !ds) return null;
1964
2005
  const raw = ds.getRawData(source);
1965
2006
  if (raw === null || raw === undefined) return null;
2007
+ const identity = cfg.authDependent ? authIdentity() : undefined;
2008
+ if (identity === null) return null;
1966
2009
  try {
1967
2010
  const snapshot = snapshotOf(source, raw, cfg);
1968
2011
  if (snapshot === null) return null;
@@ -1974,7 +2017,8 @@ window.ManifestDataStore = {
1974
2017
  savedAt: Date.now(),
1975
2018
  frameworkVersion: state.frameworkVersion,
1976
2019
  deployment: state.deployment,
1977
- locale: liveLocale()
2020
+ locale: liveLocale(),
2021
+ ...(cfg.authDependent ? { identity } : {})
1978
2022
  };
1979
2023
  } catch (error) {
1980
2024
  warnOnce(`snapshot:${source}`, `persistence skipped "${source}" (rows are not serialisable)`, error);
@@ -2040,6 +2084,9 @@ window.ManifestDataStore = {
2040
2084
  if (typeof record.savedAt !== 'number' || Date.now() - record.savedAt > cfg.ttl) return { ok: false, drop: true };
2041
2085
  if (majorMinor(record.frameworkVersion) !== majorMinor(state.frameworkVersion)) return { ok: false, drop: true };
2042
2086
  if (record.locale && record.locale !== liveLocale()) return { ok: false };
2087
+ // Another identity's (or an unstamped) snapshot never hydrates: the scope alone may be '' for everyone
2088
+ // An unverified (offline) signed-out boot keeps it for the session check to come back
2089
+ if (cfg.authDependent && record.identity !== authIdentity()) return authUnverified() ? { ok: false } : { ok: false, drop: true };
2043
2090
  return { ok: true };
2044
2091
  }
2045
2092
 
@@ -2047,7 +2094,15 @@ window.ManifestDataStore = {
2047
2094
  // fresh landing (or after a scope change) is discarded
2048
2095
  async function hydrate(sources) {
2049
2096
  const ds = dataStore();
2050
- const pending = sources.filter(source => state.sources.has(source) && !state.hydrated.has(source));
2097
+ // Auth-dependent sources wait for a known identity (re-run on auth events), and retry when it changes
2098
+ const identity = authIdentity();
2099
+ const known = identity !== null;
2100
+ if (known && identity !== state.hydratedIdentity) {
2101
+ for (const [source, cfg] of state.sources) if (cfg.authDependent) state.hydrated.delete(source);
2102
+ state.hydratedIdentity = identity;
2103
+ }
2104
+ const pending = sources.filter(source => state.sources.has(source) && !state.hydrated.has(source)
2105
+ && (known || !state.sources.get(source).authDependent));
2051
2106
  if (!ds || !pending.length || !state.enabled || state.disabled || state.scopePending) return;
2052
2107
  for (const source of pending) state.hydrated.add(source);
2053
2108
  const generation = state.generation;
@@ -2853,6 +2908,7 @@ function updateEntryInStore(dataSourceName, entryId, updates, options = {}) {
2853
2908
  if (typeof Alpine === 'undefined' || !Alpine.store) {
2854
2909
  return false;
2855
2910
  }
2911
+ if (window.ManifestDataStore?.isStaleWrite?.(dataSourceName, options)) return false;
2856
2912
 
2857
2913
  const { store, arr, index } = findRawIndex(dataSourceName, entryId);
2858
2914
  if (!store || !arr || index === -1) {
@@ -2884,6 +2940,7 @@ function addEntryToStore(dataSourceName, entry, options = {}) {
2884
2940
  if (typeof Alpine === 'undefined' || !Alpine.store) {
2885
2941
  return false;
2886
2942
  }
2943
+ if (window.ManifestDataStore?.isStaleWrite?.(dataSourceName, options)) return false;
2887
2944
 
2888
2945
  const ds = window.ManifestDataStore;
2889
2946
  const target = ds?.ensureSourceArray?.(dataSourceName);
@@ -2915,6 +2972,7 @@ function removeEntryFromStore(dataSourceName, entryId, options = {}) {
2915
2972
  if (typeof Alpine === 'undefined' || !Alpine.store) {
2916
2973
  return false;
2917
2974
  }
2975
+ if (window.ManifestDataStore?.isStaleWrite?.(dataSourceName, options)) return false;
2918
2976
 
2919
2977
  const { store, arr, index } = findRawIndex(dataSourceName, entryId);
2920
2978
  if (!store || !arr || index === -1) {
@@ -2991,6 +3049,8 @@ async function executeMutation(mutationConfig) {
2991
3049
  } = mutationConfig;
2992
3050
 
2993
3051
  const mutationId = generateMutationId();
3052
+ // Writes after the API call carry this; a logout/scope reset meanwhile drops them
3053
+ const writeOptions = { ...options, generation: window.ManifestDataStore?.sourceGeneration?.(dataSourceName) };
2994
3054
  let originalData = null;
2995
3055
  let optimisticData = null;
2996
3056
  let resolvedEntryId = entryId;
@@ -3083,12 +3143,12 @@ async function executeMutation(mutationConfig) {
3083
3143
  clearSourceCaches(dataSourceName);
3084
3144
  } else {
3085
3145
  // Temporary entry not found, just add the real one
3086
- addEntryToStore(dataSourceName, result, options);
3146
+ addEntryToStore(dataSourceName, result, writeOptions);
3087
3147
  }
3088
3148
  }
3089
3149
  } else if (type === 'update') {
3090
3150
  // Update with server response (may have additional fields)
3091
- updateEntryInStore(dataSourceName, entryId, result, options);
3151
+ updateEntryInStore(dataSourceName, entryId, result, writeOptions);
3092
3152
  }
3093
3153
  // For delete, entry is already removed optimistically, nothing to sync
3094
3154
  }
@@ -3123,11 +3183,11 @@ async function executeMutation(mutationConfig) {
3123
3183
 
3124
3184
  const rollbackFn = () => {
3125
3185
  if (type === 'create' && optimisticData) {
3126
- removeEntryFromStore(dataSourceName, optimisticData.$id);
3186
+ removeEntryFromStore(dataSourceName, optimisticData.$id, writeOptions);
3127
3187
  } else if (type === 'update' && originalData) {
3128
- updateEntryInStore(dataSourceName, entryId, originalData);
3188
+ updateEntryInStore(dataSourceName, entryId, originalData, writeOptions);
3129
3189
  } else if (type === 'delete' && originalData) {
3130
- addEntryToStore(dataSourceName, originalData);
3190
+ addEntryToStore(dataSourceName, originalData, writeOptions);
3131
3191
  }
3132
3192
  };
3133
3193
 
@@ -3151,10 +3211,11 @@ async function executeMutation(mutationConfig) {
3151
3211
 
3152
3212
  // Sync entry from server (background reconciliation)
3153
3213
  async function syncEntryFromServer(dataSourceName, entryId, syncFunction) {
3214
+ const generation = window.ManifestDataStore?.sourceGeneration?.(dataSourceName);
3154
3215
  try {
3155
3216
  const serverData = await syncFunction();
3156
3217
  if (serverData && serverData.$id) {
3157
- updateEntryInStore(dataSourceName, entryId, serverData);
3218
+ updateEntryInStore(dataSourceName, entryId, serverData, { generation });
3158
3219
  return serverData;
3159
3220
  }
3160
3221
  } catch (error) {
@@ -7112,6 +7173,7 @@ async function linkFileToEntry(tableName, entryId, fileId, fileIdsColumn = 'file
7112
7173
 
7113
7174
  if (executeMutation && updateEntryInStore) {
7114
7175
  // Optimistic update: immediately update store
7176
+ const generation = window.ManifestDataStore?.sourceGeneration?.(tableName);
7115
7177
  updateEntryInStore(tableName, entryId, { [fileIdsColumn]: fileIds });
7116
7178
 
7117
7179
  // Background sync: update server
@@ -7135,7 +7197,7 @@ async function linkFileToEntry(tableName, entryId, fileId, fileIdsColumn = 'file
7135
7197
  if (originalEntry) {
7136
7198
  const originalFileIds = originalEntry[fileIdsColumn] || [];
7137
7199
  const rolledBackFileIds = originalFileIds.filter(id => id !== fileId);
7138
- updateEntryInStore(tableName, entryId, { [fileIdsColumn]: rolledBackFileIds });
7200
+ updateEntryInStore(tableName, entryId, { [fileIdsColumn]: rolledBackFileIds }, { generation });
7139
7201
  }
7140
7202
  }
7141
7203
  throw updateError;
@@ -7301,6 +7363,7 @@ async function unlinkFileFromEntry(tableName, entryId, fileId, fileIdsColumn = '
7301
7363
 
7302
7364
  if (updateEntryInStore) {
7303
7365
  // Optimistic update: immediately update store
7366
+ const generation = window.ManifestDataStore?.sourceGeneration?.(tableName);
7304
7367
  updateEntryInStore(tableName, entryId, { [fileIdsColumn]: fileIds });
7305
7368
 
7306
7369
  // Background sync: update server
@@ -7322,7 +7385,7 @@ async function unlinkFileFromEntry(tableName, entryId, fileId, fileIdsColumn = '
7322
7385
  if (!originalFileIds.includes(fileId)) {
7323
7386
  originalFileIds.push(fileId);
7324
7387
  }
7325
- updateEntryInStore(tableName, entryId, { [fileIdsColumn]: originalFileIds });
7388
+ updateEntryInStore(tableName, entryId, { [fileIdsColumn]: originalFileIds }, { generation });
7326
7389
  }
7327
7390
  }
7328
7391
  throw updateError;
@@ -7922,7 +7985,8 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7922
7985
  }
7923
7986
  // Dedupe key: source + serialized queries (never across different queries)
7924
7987
  const key = `${dataSourceName}:$query:${JSON.stringify(appwriteQueries.map(q => String(q)))}`;
7925
- const { runDeduped, landRows } = window.ManifestDataStore || {};
7988
+ const { runDeduped, landRows, sourceGeneration } = window.ManifestDataStore || {};
7989
+ const generation = sourceGeneration?.(dataSourceName);
7926
7990
  const run = async () => {
7927
7991
  const result = await window.ManifestDataAppwrite.loadTableRows(
7928
7992
  appwriteConfig.databaseId,
@@ -7931,7 +7995,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7931
7995
  );
7932
7996
  // Network landing: query result replaces the source (coalesced, identity-preserving)
7933
7997
  if (result !== undefined && landRows) {
7934
- await landRows(dataSourceName, result, { mode: 'replace', fresh: true });
7998
+ await landRows(dataSourceName, result, { mode: 'replace', fresh: true, generation });
7935
7999
  }
7936
8000
  return result;
7937
8001
  };
@@ -8100,6 +8164,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8100
8164
  // Use optimistic update via mutation system
8101
8165
  const addEntryToStore = window.ManifestDataMutations?.addEntryToStore;
8102
8166
  const removeEntryFromStore = window.ManifestDataMutations?.removeEntryFromStore;
8167
+ const writeOptions = { generation: window.ManifestDataStore?.sourceGeneration?.(dataSourceName) };
8103
8168
  const manifest = await window.ManifestDataConfig.ensureManifest();
8104
8169
  const dataSource = manifest?.data?.[dataSourceName];
8105
8170
  const scope = window.ManifestDataConfig.getScope(dataSource);
@@ -8147,7 +8212,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8147
8212
  _optimistic: true // Mark as optimistic
8148
8213
  };
8149
8214
  // Optimistic update: immediately add file to store
8150
- addEntryToStore(dataSourceName, optimisticFile);
8215
+ addEntryToStore(dataSourceName, optimisticFile, writeOptions);
8151
8216
  }
8152
8217
 
8153
8218
  let result;
@@ -8171,7 +8236,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8171
8236
  }
8172
8237
  // Rollback optimistic update on error
8173
8238
  if (optimisticFile && removeEntryFromStore) {
8174
- removeEntryFromStore(dataSourceName, optimisticFile.$id);
8239
+ removeEntryFromStore(dataSourceName, optimisticFile.$id, writeOptions);
8175
8240
  }
8176
8241
  throw error;
8177
8242
  }
@@ -8179,12 +8244,12 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8179
8244
  // Replace optimistic file with real one from server
8180
8245
  if (optimisticFile && addEntryToStore && removeEntryFromStore) {
8181
8246
  // Remove optimistic entry
8182
- removeEntryFromStore(dataSourceName, optimisticFile.$id);
8247
+ removeEntryFromStore(dataSourceName, optimisticFile.$id, writeOptions);
8183
8248
  // Add real entry
8184
- addEntryToStore(dataSourceName, result);
8249
+ addEntryToStore(dataSourceName, result, writeOptions);
8185
8250
  } else if (addEntryToStore) {
8186
8251
  // If no optimistic update was done, add now
8187
- addEntryToStore(dataSourceName, result);
8252
+ addEntryToStore(dataSourceName, result, writeOptions);
8188
8253
  }
8189
8254
 
8190
8255
  // Link the uploaded file to a table entry, resolved from the
@@ -8627,6 +8692,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8627
8692
  ? Promise.all(fileId.map(id => window.ManifestDataAppwrite.deleteFile(bucketId, id.$id || id)))
8628
8693
  : window.ManifestDataAppwrite.deleteFile(bucketId, actualFileId);
8629
8694
 
8695
+ const generation = window.ManifestDataStore?.sourceGeneration?.(dataSourceName);
8630
8696
  const reloadPromise = window.ManifestDataAppwrite.listBucketFiles(bucketId, []);
8631
8697
 
8632
8698
  // Do delete and reload in parallel
@@ -8634,7 +8700,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8634
8700
 
8635
8701
  // Network landing: reloaded bucket listing replaces the source (coalesced)
8636
8702
  if (reloadedData && window.ManifestDataStore?.landRows) {
8637
- await window.ManifestDataStore.landRows(dataSourceName, reloadedData, { mode: 'replace' });
8703
+ await window.ManifestDataStore.landRows(dataSourceName, reloadedData, { mode: 'replace', generation });
8638
8704
  }
8639
8705
 
8640
8706
  // Reload affected table data sources so fileIds arrays and counters
@@ -11361,11 +11427,93 @@ function scheduleAuthRetry(dataSourceName, locale) {
11361
11427
  AUTH_RETRY_EVENTS.forEach(type => window.addEventListener(type, retry));
11362
11428
  }
11363
11429
 
11430
+ // Teams known for this identity: login/anonymous dispatch after teams load, teams-loaded after the background load
11431
+ let teamsSettledFor = null;
11432
+ // A sign-in deadline already answered by an auth event no longer holds sources
11433
+ let answeredDeadline = 0;
11434
+ if (typeof window !== 'undefined') {
11435
+ ['manifest:auth:teams-loaded', 'manifest:auth:login', 'manifest:auth:anonymous'].forEach(type =>
11436
+ window.addEventListener(type, () => { teamsSettledFor = authUserId(currentAuth()); }));
11437
+ // Kept only when logout restored the same (guest) identity whose teams already loaded
11438
+ ['manifest:auth:logout', 'manifest:auth:session-cleared', 'manifest:auth:initialized'].forEach(type =>
11439
+ window.addEventListener(type, () => { if (type === 'manifest:auth:initialized' || teamsSettledFor !== authUserId(currentAuth())) teamsSettledFor = null; }));
11440
+ ['manifest:auth:login', 'manifest:auth:anonymous', 'manifest:auth:logout', 'manifest:auth:session-cleared'].forEach(type =>
11441
+ window.addEventListener(type, () => { answeredDeadline = currentAuth()?._signInPendingUntil || 0; }));
11442
+ }
11443
+
11444
+ function currentAuth() {
11445
+ return typeof Alpine !== 'undefined' ? Alpine.store('auth') : null;
11446
+ }
11447
+
11448
+ function authUserId(auth) {
11449
+ return auth?.user?.$id || auth?.user?.id || null;
11450
+ }
11451
+
11452
+ // Guest-auto or an auth callback is signing in after a signed-out init (bounded by the auth store)
11453
+ // Prerender never waits on (or bakes) a guest's rows
11454
+ function signInWaitMs(auth) {
11455
+ if (!auth || auth.isAuthenticated === true || window.__manifestRender) return 0;
11456
+ const until = auth._signInPendingUntil || 0;
11457
+ if (until === answeredDeadline) return 0;
11458
+ return Math.max(0, until - Date.now());
11459
+ }
11460
+
11461
+ // Drop only this load's subscription; a newer one under the same name stays
11462
+ function dropSubscription(dataSourceName, unsubscribe) {
11463
+ if (typeof unsubscribe !== 'function') return;
11464
+ unsubscribe();
11465
+ const subs = window.ManifestDataRealtime?.subscriptions;
11466
+ if (subs?.get?.(dataSourceName) === unsubscribe) subs.delete(dataSourceName);
11467
+ }
11468
+
11364
11469
  // Auth settled with no identity (or no auth plugin): a scoped read can't resolve until sign-in
11365
11470
  function authSettledSignedOut() {
11366
- const auth = typeof Alpine !== 'undefined' ? Alpine.store('auth') : null;
11471
+ const auth = currentAuth();
11367
11472
  if (!auth) return true;
11368
- return auth._initialized === true && auth.isAuthenticated !== true;
11473
+ return auth._initialized === true && auth.isAuthenticated !== true && signInWaitMs(auth) === 0;
11474
+ }
11475
+
11476
+ function teamsSettled(auth, manifest) {
11477
+ const id = authUserId(auth);
11478
+ if (id && teamsSettledFor === id) return true;
11479
+ const teams = manifest?.appwrite?.auth?.teams;
11480
+ if (!teams) return true;
11481
+ const guests = typeof teams.guests === 'string' ? /^(true|1|yes|on)$/i.test(teams.guests.trim()) : !!teams.guests;
11482
+ return auth.isAnonymous === true && !guests;
11483
+ }
11484
+
11485
+ // Auth fully settled (identity + teams) yet the scoped read still can't resolve: it never will
11486
+ function authSettledUnresolvable(manifest) {
11487
+ if (authSettledSignedOut()) return true;
11488
+ const auth = currentAuth();
11489
+ return auth._initialized === true && auth.isAuthenticated === true && teamsSettled(auth, manifest);
11490
+ }
11491
+
11492
+ function isAuthDependent(config) {
11493
+ if (!config || typeof config !== 'object') return false;
11494
+ if (window.ManifestDataConfig?.getScope?.(config)) return true;
11495
+ try { return JSON.stringify(config.queries || '').includes('$auth.'); } catch { return false; }
11496
+ }
11497
+
11498
+ // Logout / session cleared: drop the previous identity's rows now, then settle for the new one
11499
+ function setupAuthResetListener() {
11500
+ const reset = async () => {
11501
+ const manifest = await window.ManifestDataConfig?.ensureManifest?.();
11502
+ const ds = window.ManifestDataStore;
11503
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
11504
+ if (!manifest || !ds?.resetSource || !store) return;
11505
+ const raw = ds.rawOf ? ds.rawOf(store) : store;
11506
+ const configs = { ...(manifest.appwrite || {}), ...(manifest.data || {}) };
11507
+ const locale = liveLocale();
11508
+ for (const [name, config] of Object.entries(configs)) {
11509
+ if (!isAuthDependent(config) || raw[`_${name}_state`] === undefined) continue;
11510
+ window.ManifestDataRealtime?.unsubscribeFromDataSource?.(name);
11511
+ ds.resetSource(name);
11512
+ loadDataSource(name, locale, { reload: true });
11513
+ }
11514
+ };
11515
+ window.addEventListener('manifest:auth:logout', reset);
11516
+ window.addEventListener('manifest:auth:session-cleared', reset);
11369
11517
  }
11370
11518
 
11371
11519
  // Client-side scope filter: Appwrite returns all accessible files, so narrow to
@@ -11468,7 +11616,7 @@ async function filterFilesByScope(files, scope) {
11468
11616
  }
11469
11617
 
11470
11618
  // Handle real-time storage events
11471
- async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, eventType, payload) {
11619
+ async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, eventType, payload, generation) {
11472
11620
 
11473
11621
  // Deduplicate events
11474
11622
  const eventKey = getEventKey(eventType, payload);
@@ -11495,7 +11643,7 @@ async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, event
11495
11643
  if (file && file.$id) {
11496
11644
  const fileMatchesScope = await checkFileMatchesScope(file, scope);
11497
11645
  if (fileMatchesScope) {
11498
- landRows(dataSourceName, [file], { mode: 'append' });
11646
+ landRows(dataSourceName, [file], { mode: 'append', generation });
11499
11647
 
11500
11648
  // Emit custom event for new file creation so UI can refresh project files
11501
11649
  window.dispatchEvent(new CustomEvent('manifest:file-created', {
@@ -11510,16 +11658,16 @@ async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, event
11510
11658
  if (file && file.$id) {
11511
11659
  const fileMatchesScope = await checkFileMatchesScope(file, scope);
11512
11660
  if (fileMatchesScope) {
11513
- landRows(dataSourceName, [file], { mode: 'append' });
11661
+ landRows(dataSourceName, [file], { mode: 'append', generation });
11514
11662
  } else {
11515
11663
  // File no longer matches scope, remove it
11516
- landRemove(dataSourceName, [file.$id]);
11664
+ landRemove(dataSourceName, [file.$id], { generation });
11517
11665
  }
11518
11666
  }
11519
11667
  } else if (eventType === 'delete') {
11520
11668
  const fileId = payload?.$id || payload?.file?.$id || payload?.fileId || payload;
11521
11669
  if (fileId) {
11522
- landRemove(dataSourceName, [fileId.$id || fileId]);
11670
+ landRemove(dataSourceName, [fileId.$id || fileId], { generation });
11523
11671
  }
11524
11672
  }
11525
11673
  }
@@ -11570,7 +11718,7 @@ function markEventProcessed(eventKey) {
11570
11718
  }
11571
11719
 
11572
11720
  // Handle real-time events for database tables
11573
- async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, scope, scopeColumns, eventType, payload) {
11721
+ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, scope, scopeColumns, eventType, payload, generation) {
11574
11722
 
11575
11723
  // Deduplicate events
11576
11724
  const eventKey = getEventKey(eventType, payload);
@@ -11599,7 +11747,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
11599
11747
  if (row && row.$id) {
11600
11748
  const rowMatchesScope = await checkRowMatchesScope(row, scope, scopeColumns);
11601
11749
  if (rowMatchesScope) {
11602
- landRows(dataSourceName, [row], { mode: 'append' });
11750
+ landRows(dataSourceName, [row], { mode: 'append', generation });
11603
11751
  }
11604
11752
  } else {
11605
11753
  console.warn('[Manifest Data] Invalid row payload in create event:', payload);
@@ -11611,12 +11759,12 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
11611
11759
  const rowMatchesScope = await checkRowMatchesScope(row, scope, scopeColumns);
11612
11760
  if (!rowMatchesScope) {
11613
11761
  // Row no longer matches scope, remove it
11614
- if (existingRow) landRemove(dataSourceName, [row.$id]);
11762
+ if (existingRow) landRemove(dataSourceName, [row.$id], { generation });
11615
11763
  return;
11616
11764
  }
11617
11765
  if (!existingRow) {
11618
11766
  // Row not in list, but matches scope now - add it
11619
- landRows(dataSourceName, [row], { mode: 'append' });
11767
+ landRows(dataSourceName, [row], { mode: 'append', generation });
11620
11768
  return;
11621
11769
  }
11622
11770
 
@@ -11676,7 +11824,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
11676
11824
  const fileIdsChanged = isProjectWithFiles &&
11677
11825
  JSON.stringify(existingFileIds) !== JSON.stringify(incomingFileIds);
11678
11826
 
11679
- landRows(dataSourceName, [row], { mode: 'append' });
11827
+ landRows(dataSourceName, [row], { mode: 'append', generation });
11680
11828
 
11681
11829
  // Emit custom event for project file updates so UI can refresh
11682
11830
  if (fileIdsChanged && dataSourceName === 'projects') {
@@ -11689,7 +11837,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
11689
11837
  } else if (eventType === 'delete') {
11690
11838
  const rowId = payload?.$id || payload?.row?.$id || payload?.rowId || payload;
11691
11839
  if (rowId) {
11692
- landRemove(dataSourceName, [rowId.$id || rowId]);
11840
+ landRemove(dataSourceName, [rowId.$id || rowId], { generation });
11693
11841
  }
11694
11842
  }
11695
11843
 
@@ -11871,9 +12019,20 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
11871
12019
  const generation = sourceGeneration ? sourceGeneration(dataSourceName) : 0;
11872
12020
  const superseded = () => sourceGeneration && sourceGeneration(dataSourceName) !== generation;
11873
12021
 
11874
- // Signed out: settle empty (not $loading forever); sign-in reloads via the auth retry
11875
- const settleSignedOut = () => {
11876
- if (isInitializing || superseded() || !authSettledSignedOut()) return false;
12022
+ // Unresolvable scope (signed out, or teamless): settle empty, not $loading forever; auth events reload.
12023
+ // A pending sign-in holds the source unsettled (one render-ready), bounded by its deadline.
12024
+ const settleUnresolved = (manifest) => {
12025
+ if (isInitializing || superseded()) return false;
12026
+ const wait = signInWaitMs(currentAuth());
12027
+ if (wait > 0) {
12028
+ setTimeout(() => {
12029
+ const state = Alpine.store('data')?.[`_${dataSourceName}_state`];
12030
+ if (state?.ready || superseded() || !authSettledSignedOut()) return;
12031
+ updateStore(dataSourceName, [], { loading: false, error: null, ready: true });
12032
+ }, wait + 10);
12033
+ return false;
12034
+ }
12035
+ if (!authSettledUnresolvable(manifest)) return false;
11877
12036
  updateStore(dataSourceName, [], { loading: false, error: null, ready: true });
11878
12037
  return true;
11879
12038
  };
@@ -11957,7 +12116,7 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
11957
12116
  // Not ready (auth/scope unresolved): skip this read, retry on an auth event
11958
12117
  if (queries === null) {
11959
12118
  scheduleAuthRetry(dataSourceName, locale);
11960
- if (settleSignedOut()) landed = true;
12119
+ if (settleUnresolved(manifest)) landed = true;
11961
12120
  return null;
11962
12121
  }
11963
12122
 
@@ -11970,19 +12129,21 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
11970
12129
  tableId,
11971
12130
  queries
11972
12131
  );
12132
+ if (superseded()) { landed = true; return null; }
11973
12133
 
11974
- // Subscribe to real-time updates for this table
12134
+ // Subscribe to real-time updates for this table (events from a superseded identity drop)
11975
12135
  if (window.ManifestDataRealtime && window.ManifestDataRealtime.subscribeToTable) {
11976
- await window.ManifestDataRealtime.subscribeToTable(
12136
+ const unsubscribe = await window.ManifestDataRealtime.subscribeToTable(
11977
12137
  dataSourceName,
11978
12138
  appwriteConfig.databaseId,
11979
12139
  tableId,
11980
12140
  scope,
11981
12141
  async (eventType, payload) => {
11982
- // Handle real-time events
11983
- await handleTableRealtimeEvent(dataSourceName, appwriteConfig.databaseId, tableId, scope, scopeColumns, eventType, payload);
11984
- }
12142
+ await handleTableRealtimeEvent(dataSourceName, appwriteConfig.databaseId, tableId, scope, scopeColumns, eventType, payload, generation);
12143
+ },
12144
+ () => !superseded()
11985
12145
  );
12146
+ if (superseded()) { dropSubscription(dataSourceName, unsubscribe); landed = true; return null; }
11986
12147
  }
11987
12148
  } else if (bucketId) {
11988
12149
  // Load from Appwrite storage bucket
@@ -11996,7 +12157,7 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
11996
12157
  // Not ready (a $auth. arg in queriesConfig unresolved): skip, retry on an auth event
11997
12158
  if (queries === null) {
11998
12159
  scheduleAuthRetry(dataSourceName, locale);
11999
- if (settleSignedOut()) landed = true;
12160
+ if (settleUnresolved(manifest)) landed = true;
12000
12161
  return null;
12001
12162
  }
12002
12163
 
@@ -12017,17 +12178,21 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12017
12178
 
12018
12179
  data = files;
12019
12180
 
12020
- // Subscribe to real-time updates for this bucket
12181
+ if (superseded()) { landed = true; return null; }
12182
+
12183
+ // Subscribe to real-time updates for this bucket (events from a superseded identity drop)
12021
12184
  if (window.ManifestDataRealtime && window.ManifestDataRealtime.subscribeToStorageBucket) {
12022
- await window.ManifestDataRealtime.subscribeToStorageBucket(
12185
+ const unsubscribe = await window.ManifestDataRealtime.subscribeToStorageBucket(
12023
12186
  dataSourceName,
12024
12187
  bucketId,
12025
12188
  scope,
12026
12189
  async (eventType, payload) => {
12027
- // Handle real-time events
12028
- await handleStorageRealtimeEvent(dataSourceName, bucketId, scope, eventType, payload);
12029
- }
12190
+ if (superseded()) return;
12191
+ await handleStorageRealtimeEvent(dataSourceName, bucketId, scope, eventType, payload, generation);
12192
+ },
12193
+ () => !superseded()
12030
12194
  );
12195
+ if (superseded()) { dropSubscription(dataSourceName, unsubscribe); landed = true; return null; }
12031
12196
  }
12032
12197
  } else {
12033
12198
  console.warn(`[Manifest Data] Appwrite data source "${dataSourceName}" missing tableId or bucketId`);
@@ -12161,7 +12326,7 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12161
12326
  // Network landing (coalesced per frame, merges by $id); resolves once visible
12162
12327
  if (!isInitializing && !staleLocale) {
12163
12328
  landed = true;
12164
- await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true });
12329
+ await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true, generation });
12165
12330
  }
12166
12331
 
12167
12332
  // Return unsealed version for our proxy system
@@ -12307,6 +12472,8 @@ async function initializeDataSourcesPlugin() {
12307
12472
  // Setup URL change listeners
12308
12473
  setupUrlChangeListeners();
12309
12474
 
12475
+ setupAuthResetListener();
12476
+
12310
12477
  // Register $x magic method (only if Alpine is available)
12311
12478
 
12312
12479
  if (typeof Alpine !== 'undefined' && window.ManifestDataProxies) {
@@ -1,6 +1,6 @@
1
1
  {
2
- "manifest.appwrite.auth.js": "sha384-X30RFvbwT9qEIaMsMUumnW54ZzcnY5aY9QQeXPG8/0sI6VAbAw3+FZ6iuZb6b4cu",
3
- "manifest.appwrite.data.js": "sha384-dSnLE73aoJkrLXQ3EYSNgmMLJ8WZn6dGcZOKx6bY007Qnl/vY2JK75q/leiZXe7s",
2
+ "manifest.appwrite.auth.js": "sha384-tcpyR3WJvAL3TenZRsevGCGCHUrGgbU5wHuOYG5Emvny/wukRiaPEagrXiH5v7R6",
3
+ "manifest.appwrite.data.js": "sha384-gnniX4ojmePGmJsC3KRZqP+jdIGe852Sr/XSkO+HJWljiNMeZVph5LYgYdcwoRNf",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
@@ -11,7 +11,7 @@
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-4bc3HgPvF9DHMiOG08w+nWXI7Hho01RuAPAUL9xyLxOiS2igeZpulCVcvr5aT9v6",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-ixpU7pvJhlPBvT7in0HonHQLdNOabbpqik6IVsMNNzAY3UhCVAf8BRno2niTkZK1",
14
+ "manifest.data.js": "sha384-5kg35XDQb/nTD4oED1HxAkyAhCpfieoUhaY1oCO/vLTc6oPXiPATzyW6/95KSPLw",
15
15
  "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-WcCf8YQ1bKIXCLSZ24a0bJwepbYntYc8TzQM50hklo5VMIH8AtzDnwl6WU4prRv1",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
@@ -28,7 +28,7 @@
28
28
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
29
  "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
30
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-P5M9kT06bquAyIK8aLQK6ujNn5rnaJ2Lw8f/ZsDx1yLVPWYeIOwa6Y06eEWmc5ry",
31
+ "manifest.sw.js": "sha384-AQB+zglu9/quyKkdAo2oIiWzfgCH7UqiOmFwrS2hqfzYsaSpYeDpmjb5d2tl/RTB",
32
32
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
33
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
34
  "manifest.text.edit.js": "sha384-RzOvLQE8GbmI6gntAwiotDewYT/08OHcd7sJ5hLon8e8u1Xbq1D7VfEGuaRVd2CE",
@@ -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.222';
19
+ const BUILD_VERSION = '0.5.224';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  // Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
@@ -28,6 +28,8 @@ const PRECACHE_CONCURRENCY = 4;
28
28
  const STATIC_EXT = /\.(css|js|mjs|json|csv|tsv|ya?ml|md|txt|xml|svg|png|jpe?g|gif|webp|avif|ico|bmp|woff2?|ttf|otf|eot|mp3|mp4|webm|ogg|wav|flac|m4a|wasm|webmanifest|map)$/i;
29
29
  const SKIP_PATH = /^\/(sw\.js$|precache\.json$|v1\/|_appwrite\/|_ai\/|__mnfst|__edit)/;
30
30
  const CDN_HOST = /^(cdn\.manifestx\.dev|cdn\.jsdelivr\.net|unpkg\.com|esm\.run)$/;
31
+ const PRIMARY_CDN = 'https://cdn.manifestx.dev/npm/';
32
+ const MIRROR_CDN = 'https://cdn.jsdelivr.net/npm/';
31
33
  const ICONIFY_HOST = /^api\.(iconify\.design|simplesvg\.com|unisvg\.com)$/;
32
34
  const EXACT_PIN = /@\d+\.\d+\.\d+[^/]*(\/|$)/;
33
35
  const ANY_PIN = /@[^/]+(\/|$)/;
@@ -127,10 +129,20 @@ function cacheable(response) {
127
129
 
128
130
  // Cross-origin script tags fetch no-cors (opaque, uncheckable); refetch with
129
131
  // CORS so only real 200s are cached. A CORS failure falls back to pass-through.
132
+ // Primary-CDN requests that reject or come back non-ok retry once against the
133
+ // jsDelivr mirror (same /npm/ path), so a blocked or wedged cdn host can't
134
+ // dead-end the boot assets; a good mirror response caches under the original URL.
130
135
  function cacheFetch(request) {
131
136
  const url = new URL(request.url);
132
- if (url.origin === self.location.origin || request.mode === 'cors') return fetch(request);
133
- return fetch(new Request(request.url, { mode: 'cors', credentials: 'omit', redirect: 'follow' }));
137
+ const direct = url.origin === self.location.origin || request.mode === 'cors'
138
+ ? fetch(request)
139
+ : fetch(new Request(request.url, { mode: 'cors', credentials: 'omit', redirect: 'follow' }));
140
+ if (request.url.indexOf(PRIMARY_CDN) !== 0) return direct;
141
+ const mirror = () => fetch(new Request(MIRROR_CDN + request.url.slice(PRIMARY_CDN.length), { mode: 'cors', credentials: 'omit', redirect: 'follow' }));
142
+ return direct.then(
143
+ (res) => (res.ok ? res : mirror().then((alt) => (alt.ok ? alt : res)).catch(() => res)),
144
+ () => mirror(),
145
+ );
134
146
  }
135
147
 
136
148
  async function trimSwr(cache) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.222",
3
+ "version": "0.5.224",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",