mnfst 0.5.196 → 0.5.198-next.0

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.
@@ -265,6 +265,20 @@ let initializationComplete = false;
265
265
  let _renderReadyTimer = null;
266
266
  const RENDER_READY_QUIET_MS = 150; // quiet time before firing render-ready
267
267
 
268
+ // Landing queue: network writes coalesce into one store write per frame
269
+ const pendingLandings = [];
270
+ let landingResolvers = [];
271
+ let landingFrame = null;
272
+ let landingTimer = null;
273
+ const LANDING_FALLBACK_MS = 50;
274
+
275
+ // Local writes made while a flush is pending win over it (local-last)
276
+ const pendingLocal = new Map(); // source -> Map<$id, { patch, removed }>
277
+
278
+ // Lazy cross-source `all`
279
+ let allCache = null;
280
+ let allDirty = true;
281
+
268
282
  // Deep seal so Alpine won't proxy (double-proxying causes recursion)
269
283
  function deepSeal(obj) {
270
284
  if (obj === null || typeof obj !== 'object') {
@@ -295,99 +309,302 @@ function deepSeal(obj) {
295
309
  return obj;
296
310
  }
297
311
 
298
- // Update store with new data
299
- function updateStore(dataSourceName, data, options = {}) {
300
- if (isInitializing && !options.allowDuringInit) return;
312
+ const rawOf = (obj) => (typeof Alpine !== 'undefined' && Alpine.raw && obj ? Alpine.raw(obj) : obj);
301
313
 
302
- // Store raw data in our non-reactive Map for backup access
303
- rawDataStore.set(dataSourceName, data);
314
+ const rowKey = (row) => (row && typeof row === 'object' && (typeof row.$id === 'string' || typeof row.$id === 'number'))
315
+ ? row.$id
316
+ : null;
304
317
 
305
- // Store data in Alpine's reactive store (like backup did)
306
- // Alpine will handle reactivity, and our proxies will work on top
307
- const store = Alpine.store('data');
318
+ function ensureStoreShape(store) {
319
+ if (!store._v) store._v = {};
320
+ }
308
321
 
309
- // Filter out null/undefined items and items matching this data source
310
- const all = (store.all || []).filter(item =>
311
- item !== null &&
312
- item !== undefined &&
313
- item.contentType !== dataSourceName
314
- );
322
+ // Version bump: per-source (`$x` reads subscribe here), `all`, and the legacy
323
+ // global counter kept for external readers (status/datepicker/charts)
324
+ function touchSources(store, sources) {
325
+ ensureStoreShape(store);
326
+ const v = store._v;
327
+ for (const source of sources) v[source] = (v[source] || 0) + 1;
328
+ v.all = (v.all || 0) + 1;
329
+ allDirty = true;
330
+ store._dataVersion = (store._dataVersion || 0) + 1;
331
+ }
315
332
 
316
- // Only add data to 'all' array if it's not null/undefined
317
- if (data !== null && data !== undefined) {
318
- if (Array.isArray(data)) {
319
- all.push(...data);
320
- } else {
321
- all.push(data);
322
- }
333
+ function touchSource(dataSourceName) {
334
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
335
+ if (store) touchSources(store, [dataSourceName]);
336
+ }
337
+
338
+ // Post-settle hammer: re-run every `$x` reader (Alpine scheduler swallow)
339
+ function bumpAllVersions() {
340
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
341
+ if (!store) return;
342
+ ensureStoreShape(store);
343
+ const raw = rawOf(store);
344
+ const sources = new Set(Object.keys(raw._v || {}));
345
+ for (const key of Object.keys(raw)) {
346
+ if (!key.startsWith('_') && key !== 'all' && typeof raw[key] !== 'function') sources.add(key);
323
347
  }
348
+ sources.delete('all');
349
+ touchSources(store, sources);
350
+ }
324
351
 
325
- // Get current state for this data source (or defaults)
326
- const currentState = store[`_${dataSourceName}_state`] || {
327
- loading: false,
328
- error: null,
329
- ready: false
330
- };
352
+ function sameScalarOrList(a, b) {
353
+ if (a === b) return true;
354
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
355
+ for (let i = 0; i < a.length; i++) {
356
+ const x = a[i];
357
+ if (x !== b[i] || (x !== null && typeof x === 'object')) return false;
358
+ }
359
+ return true;
360
+ }
361
+
362
+ // Merge fields onto an existing tracked row (property-grain); `$files` stays bound
363
+ function mergeRowFields(target, fields, dataSourceName) {
364
+ if (!target || !fields || typeof fields !== 'object') return;
365
+ const raw = rawOf(target);
366
+ for (const key of Object.keys(fields)) {
367
+ if (key === '$files') continue;
368
+ const value = fields[key];
369
+ if (sameScalarOrList(raw[key], value)) continue;
370
+ target[key] = createReactiveReferences(value, dataSourceName);
371
+ }
372
+ }
373
+
374
+ function attachMethods(array, dataSourceName) {
375
+ const loadDataSource = window.ManifestDataMain?.loadDataSource;
376
+ if (Array.isArray(array) && loadDataSource && window.ManifestDataProxies?.attachArrayMethods) {
377
+ window.ManifestDataProxies.attachArrayMethods(array, dataSourceName, loadDataSource);
378
+ }
379
+ }
380
+
381
+ // Reactive source array, created on demand; raw map tracks the same identity
382
+ function ensureSourceArray(dataSourceName) {
383
+ const store = Alpine.store('data');
384
+ if (!store) return null;
385
+ const current = rawOf(store)[dataSourceName];
386
+ if (Array.isArray(current)) return store[dataSourceName];
387
+ if (current !== null && current !== undefined) return null;
388
+ store[dataSourceName] = [];
389
+ const created = rawOf(store)[dataSourceName];
390
+ rawDataStore.set(dataSourceName, created);
391
+ attachMethods(created, dataSourceName);
392
+ return store[dataSourceName];
393
+ }
394
+
395
+ // Identity-preserving upsert by $id: existing rows are merged in place, new
396
+ // rows created; append keeps the array, replace swaps it only when membership
397
+ // or order changed
398
+ function upsertRows(store, dataSourceName, rows, mode) {
399
+ const raw = rawOf(store);
400
+ const curRaw = Array.isArray(raw[dataSourceName]) ? raw[dataSourceName] : null;
401
+ if (!curRaw) {
402
+ store[dataSourceName] = rows.map(row => createReactiveReferences(row, dataSourceName));
403
+ return;
404
+ }
405
+ const cur = store[dataSourceName];
406
+ const index = new Map();
407
+ curRaw.forEach((row, i) => { const id = rowKey(row); if (id !== null) index.set(id, i); });
408
+ const fresh = new Map();
409
+ const next = [];
410
+ const appended = [];
411
+ for (const row of rows) {
412
+ const id = rowKey(row);
413
+ const i = id !== null ? index.get(id) : undefined;
414
+ if (i !== undefined) {
415
+ mergeRowFields(cur[i], row, dataSourceName);
416
+ if (mode === 'replace') next.push(curRaw[i]);
417
+ continue;
418
+ }
419
+ if (id !== null && fresh.has(id)) {
420
+ mergeRowFields(fresh.get(id), row, dataSourceName);
421
+ continue;
422
+ }
423
+ const created = createReactiveReferences(row, dataSourceName);
424
+ if (id !== null) fresh.set(id, created);
425
+ (mode === 'replace' ? next : appended).push(created);
426
+ }
427
+ if (mode === 'replace') {
428
+ if (next.length !== curRaw.length || next.some((row, i) => row !== curRaw[i])) store[dataSourceName] = next;
429
+ } else if (appended.length) {
430
+ cur.push(...appended);
431
+ }
432
+ }
433
+
434
+ function removeRows(store, dataSourceName, ids) {
435
+ const curRaw = rawOf(store)[dataSourceName];
436
+ if (!Array.isArray(curRaw) || !ids?.length) return false;
437
+ const set = new Set(ids);
438
+ const cur = store[dataSourceName];
439
+ let removed = false;
440
+ for (let i = curRaw.length - 1; i >= 0; i--) {
441
+ const id = rowKey(curRaw[i]);
442
+ if (id !== null && set.has(id)) { cur.splice(i, 1); removed = true; }
443
+ }
444
+ return removed;
445
+ }
446
+
447
+ // Synchronous write of data + state for one source (no version bump); returns the new state
448
+ function writeSource(dataSourceName, data, options = {}) {
449
+ const store = Alpine.store('data');
450
+ if (!store) return null;
451
+ ensureStoreShape(store);
452
+ const mode = options.mode === 'append' ? 'append' : 'replace';
331
453
 
332
- // Update state based on options
454
+ if (data === null || data === undefined) {
455
+ if (mode === 'replace') {
456
+ store[dataSourceName] = data;
457
+ rawDataStore.set(dataSourceName, data);
458
+ }
459
+ } else if (Array.isArray(data)) {
460
+ upsertRows(store, dataSourceName, data, mode);
461
+ const arr = rawOf(store)[dataSourceName];
462
+ rawDataStore.set(dataSourceName, arr);
463
+ attachMethods(arr, dataSourceName);
464
+ } else {
465
+ store[dataSourceName] = data;
466
+ rawDataStore.set(dataSourceName, data);
467
+ }
468
+
469
+ const currentState = store[`_${dataSourceName}_state`] || { loading: false, error: null, ready: false };
333
470
  const newState = {
334
471
  loading: options.loading !== undefined ? options.loading : currentState.loading,
335
472
  error: options.error !== undefined ? options.error : currentState.error,
336
473
  ready: options.ready !== undefined ? options.ready : (data !== null && data !== undefined),
337
474
  errorTime: options.error !== undefined && options.error !== null ? Date.now() : (currentState.errorTime || null)
338
475
  };
476
+ store[`_${dataSourceName}_state`] = newState;
477
+ store._initialized = true;
478
+ store._ready = true;
339
479
 
340
- // Use Alpine's reactive store update to trigger reactivity
341
- // Create a new object reference to ensure Alpine detects the change
342
- // Also create new references for nested arrays (like fileIds) so Alpine can track nested property changes
343
- const reactiveData = Array.isArray(data)
344
- ? (createReactiveReferences ? createReactiveReferences(data, dataSourceName) : data)
345
- : data;
346
-
347
- // Bump version so any effect that read the store re-runs (fixes bindings stuck on loading fallback)
348
- const dataVersion = (store._dataVersion || 0) + 1;
349
- const updatedStore = {
350
- ...store,
351
- [dataSourceName]: reactiveData, // Store actual data in Alpine store (like backup)
352
- [`_${dataSourceName}_state`]: newState, // Store state for this data source
353
- all,
354
- _initialized: true,
355
- _ready: true, // Mark as ready when first data source is loaded
356
- _dataVersion: dataVersion
357
- };
480
+ const proxies = window.ManifestDataProxies;
481
+ proxies?.clearAccessCache?.(dataSourceName);
482
+ proxies?.clearArrayProxyCacheForDataSource?.(dataSourceName);
483
+ proxies?.clearRouteProxyCacheForDataSource?.(dataSourceName);
484
+ proxies?.clearNestedProxyCacheForDataSource?.(dataSourceName);
485
+ return newState;
486
+ }
358
487
 
359
- Alpine.store('data', updatedStore);
488
+ // Synchronous replace: local writes ($register, init preload, state-only updates)
489
+ function updateStore(dataSourceName, data, options = {}) {
490
+ if (isInitializing && !options.allowDuringInit) return;
491
+ const state = writeSource(dataSourceName, data, { ...options, mode: 'replace' });
492
+ if (!state) return;
493
+ touchSource(dataSourceName);
494
+ if (!state.loading) checkAndDispatchRenderReady();
495
+ }
360
496
 
361
- // When a source finishes loading (success or error), check if everything is settled.
362
- // This is the primary trigger for manifest:render-ready.
363
- if (!newState.loading) {
364
- checkAndDispatchRenderReady();
365
- }
497
+ // Network landing (page load, paged append, realtime batch): buffered, applied
498
+ // with every other landing of the same frame in ONE flush. Resolves once visible.
499
+ function landRows(dataSourceName, rows, options = {}) {
500
+ return queueLanding({ source: dataSourceName, rows, options: { mode: 'replace', ...options } });
501
+ }
502
+
503
+ function landRemove(dataSourceName, ids, options = {}) {
504
+ return queueLanding({ source: dataSourceName, remove: Array.isArray(ids) ? ids : [ids], options });
505
+ }
506
+
507
+ function queueLanding(op) {
508
+ return new Promise(resolve => {
509
+ pendingLandings.push(op);
510
+ landingResolvers.push(resolve);
511
+ scheduleLandingFlush();
512
+ });
513
+ }
366
514
 
367
- // Re-attach methods on the new array reference
368
- if (Array.isArray(reactiveData) && window.ManifestDataProxies?.attachArrayMethods) {
369
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
370
- if (loadDataSource) {
371
- window.ManifestDataProxies.attachArrayMethods(reactiveData, dataSourceName, loadDataSource);
515
+ function scheduleLandingFlush() {
516
+ if (landingFrame !== null || landingTimer !== null) return;
517
+ const hidden = typeof document !== 'undefined' && document.hidden;
518
+ if (!hidden && typeof requestAnimationFrame === 'function') {
519
+ landingFrame = requestAnimationFrame(flushLandings);
520
+ landingTimer = setTimeout(flushLandings, LANDING_FALLBACK_MS);
521
+ } else {
522
+ landingTimer = setTimeout(flushLandings, 0);
523
+ }
524
+ }
525
+
526
+ // Local write while a flush is pending: replayed on top of that flush
527
+ function noteLocalWrite(dataSourceName, id, note) {
528
+ if (!pendingLandings.length || id === null || id === undefined) return;
529
+ let byId = pendingLocal.get(dataSourceName);
530
+ if (!byId) pendingLocal.set(dataSourceName, byId = new Map());
531
+ if (note.removed) { byId.set(id, { removed: true }); return; }
532
+ const prev = byId.get(id);
533
+ const base = prev && !prev.removed ? prev.patch : {};
534
+ byId.set(id, { removed: false, patch: { ...base, ...note.patch } });
535
+ }
536
+
537
+ function flushLandings() {
538
+ if (landingFrame !== null) { cancelAnimationFrame(landingFrame); landingFrame = null; }
539
+ if (landingTimer !== null) { clearTimeout(landingTimer); landingTimer = null; }
540
+ const ops = pendingLandings.splice(0);
541
+ const resolvers = landingResolvers.splice(0);
542
+ const local = new Map(pendingLocal);
543
+ pendingLocal.clear();
544
+ if (!ops.length) return;
545
+
546
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
547
+ const touched = new Set();
548
+ let settled = false;
549
+ if (store) {
550
+ for (const op of ops) {
551
+ try {
552
+ if (op.remove) {
553
+ if (removeRows(store, op.source, op.remove)) touched.add(op.source);
554
+ continue;
555
+ }
556
+ const state = writeSource(op.source, op.rows, op.options);
557
+ if (!state) continue;
558
+ touched.add(op.source);
559
+ // Only load completions (explicit loading state) feed render-ready; realtime upserts never do
560
+ if (op.options.loading !== undefined && !state.loading) settled = true;
561
+ } catch (error) {
562
+ console.error(`[Manifest Data] Landing failed for "${op.source}":`, error);
563
+ }
564
+ }
565
+ for (const [source, byId] of local) {
566
+ for (const [id, note] of byId) {
567
+ if (note.removed) {
568
+ if (removeRows(store, source, [id])) touched.add(source);
569
+ continue;
570
+ }
571
+ const curRaw = rawOf(store)[source];
572
+ const i = Array.isArray(curRaw) ? curRaw.findIndex(row => rowKey(row) === id) : -1;
573
+ if (i !== -1) { mergeRowFields(store[source][i], note.patch, source); touched.add(source); }
574
+ }
372
575
  }
576
+ if (touched.size) touchSources(store, touched);
373
577
  }
578
+ resolvers.forEach(resolve => resolve());
579
+ if (settled) checkAndDispatchRenderReady();
580
+ }
374
581
 
375
- // Clear proxy cache for this data source to force re-reading from store
376
- if (window.ManifestDataProxies?.clearAccessCache) {
377
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
378
- }
379
- // Clear array proxy cache to ensure Alpine gets fresh proxy
380
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
381
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
382
- }
383
- // Clear route proxy cache to ensure fresh route proxies
384
- if (window.ManifestDataProxies?.clearRouteProxyCacheForDataSource) {
385
- window.ManifestDataProxies.clearRouteProxyCacheForDataSource(dataSourceName);
386
- }
387
- // Clear nested proxy cache so next $x.dataSourceName access builds from new store data
388
- if (window.ManifestDataProxies?.clearNestedProxyCacheForDataSource) {
389
- window.ManifestDataProxies.clearNestedProxyCacheForDataSource(dataSourceName);
582
+ // Cross-source `all`: built on first read after a change, versioned by _v.all
583
+ function getAll() {
584
+ const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
585
+ if (!store) return [];
586
+ ensureStoreShape(store);
587
+ void store._v.all;
588
+ if (allDirty || !allCache) {
589
+ const raw = rawOf(store);
590
+ const wrap = Alpine.reactive ? (item => Alpine.reactive(item)) : (item => item);
591
+ const out = [];
592
+ for (const key of Object.keys(raw)) {
593
+ if (key.startsWith('_') || key === 'all') continue;
594
+ const value = raw[key];
595
+ if (Array.isArray(value)) {
596
+ for (const item of value) {
597
+ if (item !== null && item !== undefined) out.push(typeof item === 'object' ? wrap(item) : item);
598
+ }
599
+ } else if (value && typeof value === 'object') {
600
+ out.push(wrap(value));
601
+ }
602
+ }
603
+ allCache = out;
604
+ allDirty = false;
605
+ attachMethods(out, 'all');
390
606
  }
607
+ return allCache;
391
608
  }
392
609
 
393
610
  // Get raw data from our non-reactive store
@@ -649,10 +866,10 @@ function createReactiveReferences(data, dataSourceName = null) {
649
866
  // Initialize store
650
867
  function initializeStore() {
651
868
  const initialStore = {
652
- all: [], // Global content array for cross-dataSource access
653
869
  _initialized: false,
654
870
  _ready: false, // Flag to indicate when data is ready for Alpine evaluation
655
- _dataVersion: 0, // Bumped in updateStore so bindings re-run when data loads
871
+ _v: {}, // Per-source versions: `$x.<source>` reads subscribe to _v[source]; _v.all for `$x.all`
872
+ _dataVersion: 0, // Legacy global counter, bumped once per flush for external readers
656
873
  _currentUrl: window.location.pathname,
657
874
  // Operation-specific loading states (for UI reactivity)
658
875
  // Format: { dataSourceName: { entryId: true } }
@@ -675,6 +892,10 @@ function initializeStore() {
675
892
  return isUploadingFile(dataSourceName, entryId, fileId);
676
893
  }
677
894
  };
895
+ // Lazy cross-source array (non-enumerable so store spreads/key scans skip it)
896
+ Object.defineProperty(initialStore, 'all', { enumerable: false, configurable: true, get: getAll });
897
+ allCache = null;
898
+ allDirty = true;
678
899
  Alpine.store('data', initialStore);
679
900
  }
680
901
 
@@ -754,11 +975,10 @@ function setupTeamChangeListener() {
754
975
  // Remove team-scoped data from store
755
976
  const store = Alpine.store('data');
756
977
  if (store) {
757
- const newStore = { ...store };
758
978
  teamScopedDataSources.forEach(dataSourceName => {
759
- delete newStore[dataSourceName];
979
+ delete store[dataSourceName];
760
980
  });
761
- Alpine.store('data', newStore);
981
+ touchSources(store, teamScopedDataSources);
762
982
  }
763
983
 
764
984
  // Clear proxy cache for these data sources
@@ -781,16 +1001,6 @@ function setupTeamChangeListener() {
781
1001
  }
782
1002
  })).then(() => {
783
1003
  });
784
- } else {
785
- // Fallback: Delete from store to force reload on next access
786
- const store = Alpine.store('data');
787
- if (store) {
788
- const newStore = { ...store };
789
- teamScopedDataSources.forEach(dataSourceName => {
790
- delete newStore[dataSourceName];
791
- });
792
- Alpine.store('data', newStore);
793
- }
794
1004
  }
795
1005
  }
796
1006
  } catch (error) {
@@ -863,10 +1073,7 @@ function checkAndDispatchRenderReady() {
863
1073
  // never re-run. One post-settle bump on a fresh task re-runs
864
1074
  // anything dropped.
865
1075
  setTimeout(() => {
866
- try {
867
- const s = Alpine.store('data');
868
- if (s) Alpine.store('data', { ...s, _dataVersion: (s._dataVersion || 0) + 1 });
869
- } catch (_) { /* no-op */ }
1076
+ try { bumpAllVersions(); } catch (_) { /* no-op */ }
870
1077
  }, 50);
871
1078
  } catch {
872
1079
  // Silently fail — the render script has its own timeout fallback
@@ -881,12 +1088,7 @@ function setupLocaleChangeListener() {
881
1088
 
882
1089
  // Set loading state to prevent flicker
883
1090
  const store = Alpine.store('data');
884
- if (store) {
885
- Alpine.store('data', {
886
- ...store,
887
- _localeChanging: true
888
- });
889
- }
1091
+ if (store) store._localeChanging = true;
890
1092
 
891
1093
  try {
892
1094
  // Get manifest to identify localized data sources
@@ -979,23 +1181,13 @@ function setupLocaleChangeListener() {
979
1181
 
980
1182
  // Remove localized data from store so bindings see missing data and re-run
981
1183
  const store = Alpine.store('data');
982
- if (store && store.all) {
983
- const filteredAll = store.all.filter(item =>
984
- !localizedDataSources.includes(item.contentType)
985
- );
986
-
987
- const newStore = { ...store, all: filteredAll };
1184
+ if (store) {
988
1185
  localizedDataSources.forEach(dataSourceName => {
989
- delete newStore[dataSourceName];
990
- delete newStore[`_${dataSourceName}_state`];
991
- });
992
-
993
- const dataVersion = (store._dataVersion || 0) + 1;
994
- Alpine.store('data', {
995
- ...newStore,
996
- _localeChanging: false,
997
- _dataVersion: dataVersion
1186
+ delete store[dataSourceName];
1187
+ delete store[`_${dataSourceName}_state`];
998
1188
  });
1189
+ store._localeChanging = false;
1190
+ touchSources(store, localizedDataSources);
999
1191
  }
1000
1192
 
1001
1193
  // Proactively reload localized sources with the new locale so the UI updates.
@@ -1016,11 +1208,17 @@ function setupLocaleChangeListener() {
1016
1208
  // Fallback to full reload if something goes wrong
1017
1209
  dataSourceCache.clear();
1018
1210
  loadingPromises.clear();
1019
- Alpine.store('data', {
1020
- all: [],
1021
- _initialized: true,
1022
- _localeChanging: false
1023
- });
1211
+ rawDataStore.clear();
1212
+ const store = Alpine.store('data');
1213
+ if (store) {
1214
+ const raw = rawOf(store);
1215
+ for (const key of Object.keys(raw)) {
1216
+ if (!key.startsWith('_') && typeof raw[key] !== 'function') delete store[key];
1217
+ }
1218
+ store._initialized = true;
1219
+ store._localeChanging = false;
1220
+ bumpAllVersions();
1221
+ }
1024
1222
  }
1025
1223
  });
1026
1224
  }
@@ -1043,12 +1241,6 @@ function setCreatingEntry(dataSourceName, entryId) {
1043
1241
  ...store._creatingEntry[dataSourceName],
1044
1242
  [entryId]: true
1045
1243
  };
1046
-
1047
- // Update store to trigger reactivity
1048
- Alpine.store('data', {
1049
- ...store,
1050
- _creatingEntry: { ...store._creatingEntry }
1051
- });
1052
1244
  }
1053
1245
 
1054
1246
  function clearCreatingEntry(dataSourceName, entryId) {
@@ -1058,12 +1250,6 @@ function clearCreatingEntry(dataSourceName, entryId) {
1058
1250
  // Create new object without this entryId
1059
1251
  const { [entryId]: removed, ...rest } = store._creatingEntry[dataSourceName];
1060
1252
  store._creatingEntry[dataSourceName] = rest;
1061
-
1062
- // Update store to trigger reactivity
1063
- Alpine.store('data', {
1064
- ...store,
1065
- _creatingEntry: { ...store._creatingEntry }
1066
- });
1067
1253
  }
1068
1254
 
1069
1255
  function setUpdatingEntry(dataSourceName, entryId) {
@@ -1082,12 +1268,6 @@ function setUpdatingEntry(dataSourceName, entryId) {
1082
1268
  ...store._updatingEntry[dataSourceName],
1083
1269
  [entryId]: true
1084
1270
  };
1085
-
1086
- // Update store to trigger reactivity
1087
- Alpine.store('data', {
1088
- ...store,
1089
- _updatingEntry: { ...store._updatingEntry }
1090
- });
1091
1271
  }
1092
1272
 
1093
1273
  function clearUpdatingEntry(dataSourceName, entryId) {
@@ -1097,12 +1277,6 @@ function clearUpdatingEntry(dataSourceName, entryId) {
1097
1277
  // Create new object without this entryId
1098
1278
  const { [entryId]: removed, ...rest } = store._updatingEntry[dataSourceName];
1099
1279
  store._updatingEntry[dataSourceName] = rest;
1100
-
1101
- // Update store to trigger reactivity
1102
- Alpine.store('data', {
1103
- ...store,
1104
- _updatingEntry: { ...store._updatingEntry }
1105
- });
1106
1280
  }
1107
1281
 
1108
1282
  function setDeletingEntry(dataSourceName, entryId) {
@@ -1121,12 +1295,6 @@ function setDeletingEntry(dataSourceName, entryId) {
1121
1295
  ...store._deletingEntry[dataSourceName],
1122
1296
  [entryId]: true
1123
1297
  };
1124
-
1125
- // Update store to trigger reactivity
1126
- Alpine.store('data', {
1127
- ...store,
1128
- _deletingEntry: { ...store._deletingEntry }
1129
- });
1130
1298
  }
1131
1299
 
1132
1300
  function clearDeletingEntry(dataSourceName, entryId) {
@@ -1136,12 +1304,6 @@ function clearDeletingEntry(dataSourceName, entryId) {
1136
1304
  // Create new object without this entryId
1137
1305
  const { [entryId]: removed, ...rest } = store._deletingEntry[dataSourceName];
1138
1306
  store._deletingEntry[dataSourceName] = rest;
1139
-
1140
- // Update store to trigger reactivity
1141
- Alpine.store('data', {
1142
- ...store,
1143
- _deletingEntry: { ...store._deletingEntry }
1144
- });
1145
1307
  }
1146
1308
 
1147
1309
  function setUploadingFile(dataSourceName, entryId, fileId) {
@@ -1163,12 +1325,6 @@ function setUploadingFile(dataSourceName, entryId, fileId) {
1163
1325
  ...store._uploadingFile[dataSourceName][entryId],
1164
1326
  [fileId]: true
1165
1327
  };
1166
-
1167
- // Update store to trigger reactivity
1168
- Alpine.store('data', {
1169
- ...store,
1170
- _uploadingFile: { ...store._uploadingFile }
1171
- });
1172
1328
  }
1173
1329
 
1174
1330
  function clearUploadingFile(dataSourceName, entryId, fileId) {
@@ -1184,12 +1340,6 @@ function clearUploadingFile(dataSourceName, entryId, fileId) {
1184
1340
  const { [entryId]: removedEntry, ...restEntries } = store._uploadingFile[dataSourceName];
1185
1341
  store._uploadingFile[dataSourceName] = restEntries;
1186
1342
  }
1187
-
1188
- // Update store to trigger reactivity
1189
- Alpine.store('data', {
1190
- ...store,
1191
- _uploadingFile: { ...store._uploadingFile }
1192
- });
1193
1343
  }
1194
1344
 
1195
1345
  // Helper methods to check operation-specific loading states
@@ -1231,6 +1381,17 @@ window.ManifestDataStore = {
1231
1381
  setIsInitializing: (value) => { isInitializing = value; },
1232
1382
  setInitializationComplete: (value) => { initializationComplete = value; },
1233
1383
  updateStore,
1384
+ // Landing model (PERF-PRIMITIVES-DESIGN.md §5)
1385
+ landRows,
1386
+ landRemove,
1387
+ flushLandings,
1388
+ noteLocalWrite,
1389
+ touchSource,
1390
+ bumpAllVersions,
1391
+ mergeRowFields,
1392
+ ensureSourceArray,
1393
+ rawOf,
1394
+ getAll,
1234
1395
  getRawData,
1235
1396
  createReactiveReferences,
1236
1397
  initializeStore,
@@ -1864,78 +2025,40 @@ function generateMutationId() {
1864
2025
  return `mutation_${Date.now()}_${++mutationIdCounter}`;
1865
2026
  }
1866
2027
 
2028
+ // Local writes are synchronous (read-your-writes) and identity-preserving;
2029
+ // network landings go through ManifestDataStore.landRows instead.
2030
+ function clearSourceCaches(dataSourceName) {
2031
+ const proxies = window.ManifestDataProxies;
2032
+ proxies?.clearAccessCache?.(dataSourceName);
2033
+ proxies?.clearArrayProxyCacheForDataSource?.(dataSourceName);
2034
+ proxies?.clearRouteProxyCacheForDataSource?.(dataSourceName);
2035
+ }
2036
+
2037
+ function findRawIndex(dataSourceName, entryId) {
2038
+ const store = Alpine.store('data');
2039
+ const raw = window.ManifestDataStore?.rawOf?.(store) || store;
2040
+ const arr = raw?.[dataSourceName];
2041
+ if (!Array.isArray(arr)) return { store, arr: null, index: -1 };
2042
+ return { store, arr, index: arr.findIndex(entry => entry && entry.$id === entryId) };
2043
+ }
2044
+
1867
2045
  // Update a single entry in the store (scoped update)
1868
2046
  function updateEntryInStore(dataSourceName, entryId, updates, options = {}) {
1869
2047
  if (typeof Alpine === 'undefined' || !Alpine.store) {
1870
2048
  return false;
1871
2049
  }
1872
2050
 
1873
- const store = Alpine.store('data');
1874
- if (!store || !store[dataSourceName] || !Array.isArray(store[dataSourceName])) {
1875
- return false;
1876
- }
1877
-
1878
- const currentArray = store[dataSourceName];
1879
- const index = currentArray.findIndex(entry => entry.$id === entryId);
1880
-
1881
- if (index === -1) {
2051
+ const { store, arr, index } = findRawIndex(dataSourceName, entryId);
2052
+ if (!store || !arr || index === -1) {
1882
2053
  return false;
1883
2054
  }
1884
2055
 
1885
- // CRITICAL DEBUG: Log all updates to projects, especially fileIds changes
1886
- if (dataSourceName === 'projects') {
1887
- const existingEntry = currentArray[index];
1888
- const existingFileIds = existingEntry?.fileIds || [];
1889
- const newFileIds = updates?.fileIds || (updates === existingEntry ? existingFileIds : undefined);
1890
-
1891
- // Get stack trace to see who's calling this
1892
- const stack = new Error().stack;
1893
- const caller = stack?.split('\n')[2]?.trim() || 'unknown';
1894
-
1895
- }
1896
-
1897
- // Create new array with updated entry
1898
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
1899
- const newArray = currentArray.map((entry, i) => {
1900
- if (i === index) {
1901
- // Merge updates into entry, creating new object reference
1902
- const updatedEntry = createReactiveReferences
1903
- ? createReactiveReferences({ ...entry, ...updates }, dataSourceName)
1904
- : { ...entry, ...updates };
1905
- return updatedEntry;
1906
- }
1907
- return entry;
1908
- });
1909
-
1910
- // Create reactive references for entire array
1911
- const reactiveArray = createReactiveReferences
1912
- ? createReactiveReferences(newArray, dataSourceName)
1913
- : newArray;
1914
-
1915
- // Update store
1916
- Alpine.store('data', {
1917
- ...store,
1918
- [dataSourceName]: reactiveArray
1919
- });
1920
-
1921
- // Attach methods to new array reference
1922
- if (window.ManifestDataProxies?.attachArrayMethods) {
1923
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
1924
- if (loadDataSource) {
1925
- window.ManifestDataProxies.attachArrayMethods(reactiveArray, dataSourceName, loadDataSource);
1926
- }
1927
- }
1928
-
1929
- // Clear caches
1930
- if (window.ManifestDataProxies?.clearAccessCache) {
1931
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
1932
- }
1933
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
1934
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
1935
- }
1936
- if (window.ManifestDataProxies?.clearRouteProxyCacheForDataSource) {
1937
- window.ManifestDataProxies.clearRouteProxyCacheForDataSource(dataSourceName);
1938
- }
2056
+ // Local write: merge onto the tracked row, sync
2057
+ const ds = window.ManifestDataStore;
2058
+ ds.mergeRowFields(store[dataSourceName][index], updates, dataSourceName);
2059
+ ds.noteLocalWrite(dataSourceName, entryId, { patch: updates });
2060
+ ds.touchSource(dataSourceName);
2061
+ clearSourceCaches(dataSourceName);
1939
2062
 
1940
2063
  // Dispatch mutation event
1941
2064
  window.dispatchEvent(new CustomEvent('manifest:data-mutated', {
@@ -1956,53 +2079,17 @@ function addEntryToStore(dataSourceName, entry, options = {}) {
1956
2079
  return false;
1957
2080
  }
1958
2081
 
1959
- const store = Alpine.store('data');
1960
- if (!store) {
2082
+ const ds = window.ManifestDataStore;
2083
+ const target = ds?.ensureSourceArray?.(dataSourceName);
2084
+ if (!target) {
1961
2085
  return false;
1962
2086
  }
1963
2087
 
1964
- // Get current array or initialize
1965
- const currentArray = store[dataSourceName] || [];
1966
- if (!Array.isArray(currentArray)) {
1967
- return false;
1968
- }
1969
-
1970
- // Create reactive references for new entry
1971
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
1972
- const newEntry = createReactiveReferences
1973
- ? createReactiveReferences(entry, dataSourceName)
1974
- : entry;
1975
-
1976
- // Create new array with new entry
1977
- const newArray = [...currentArray, newEntry];
1978
- const reactiveArray = createReactiveReferences
1979
- ? createReactiveReferences(newArray, dataSourceName)
1980
- : newArray;
1981
-
1982
- // Update store
1983
- Alpine.store('data', {
1984
- ...store,
1985
- [dataSourceName]: reactiveArray
1986
- });
1987
-
1988
- // Attach methods to new array reference
1989
- if (window.ManifestDataProxies?.attachArrayMethods) {
1990
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
1991
- if (loadDataSource) {
1992
- window.ManifestDataProxies.attachArrayMethods(reactiveArray, dataSourceName, loadDataSource);
1993
- }
1994
- }
1995
-
1996
- // Clear caches
1997
- if (window.ManifestDataProxies?.clearAccessCache) {
1998
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
1999
- }
2000
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
2001
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
2002
- }
2003
- if (window.ManifestDataProxies?.clearRouteProxyCacheForDataSource) {
2004
- window.ManifestDataProxies.clearRouteProxyCacheForDataSource(dataSourceName);
2005
- }
2088
+ // Local write: push onto the tracked array, sync
2089
+ target.push(ds.createReactiveReferences(entry, dataSourceName));
2090
+ ds.noteLocalWrite(dataSourceName, entry?.$id, { patch: entry });
2091
+ ds.touchSource(dataSourceName);
2092
+ clearSourceCaches(dataSourceName);
2006
2093
 
2007
2094
  // Dispatch mutation event
2008
2095
  window.dispatchEvent(new CustomEvent('manifest:data-mutated', {
@@ -2023,49 +2110,20 @@ function removeEntryFromStore(dataSourceName, entryId, options = {}) {
2023
2110
  return false;
2024
2111
  }
2025
2112
 
2026
- const store = Alpine.store('data');
2027
- if (!store || !store[dataSourceName] || !Array.isArray(store[dataSourceName])) {
2028
- return false;
2029
- }
2030
-
2031
- const currentArray = store[dataSourceName];
2032
- const index = currentArray.findIndex(entry => entry.$id === entryId);
2033
-
2034
- if (index === -1) {
2113
+ const { store, arr, index } = findRawIndex(dataSourceName, entryId);
2114
+ if (!store || !arr || index === -1) {
2035
2115
  return false;
2036
2116
  }
2037
2117
 
2038
2118
  // Store original entry for rollback
2039
- const originalEntry = currentArray[index];
2040
-
2041
- // Create new array without entry
2042
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
2043
- const newArray = currentArray.filter((entry, i) => i !== index);
2044
- const reactiveArray = createReactiveReferences
2045
- ? createReactiveReferences(newArray, dataSourceName)
2046
- : newArray;
2047
-
2048
- // Update store
2049
- Alpine.store('data', {
2050
- ...store,
2051
- [dataSourceName]: reactiveArray
2052
- });
2053
-
2054
- // Attach methods to new array reference
2055
- if (window.ManifestDataProxies?.attachArrayMethods) {
2056
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
2057
- if (loadDataSource) {
2058
- window.ManifestDataProxies.attachArrayMethods(reactiveArray, dataSourceName, loadDataSource);
2059
- }
2060
- }
2119
+ const originalEntry = arr[index];
2061
2120
 
2062
- // Clear caches
2063
- if (window.ManifestDataProxies?.clearAccessCache) {
2064
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
2065
- }
2066
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
2067
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
2068
- }
2121
+ // Local write: splice the tracked array, sync
2122
+ const ds = window.ManifestDataStore;
2123
+ store[dataSourceName].splice(index, 1);
2124
+ ds.noteLocalWrite(dataSourceName, entryId, { removed: true });
2125
+ ds.touchSource(dataSourceName);
2126
+ clearSourceCaches(dataSourceName);
2069
2127
 
2070
2128
  // Dispatch mutation event
2071
2129
  window.dispatchEvent(new CustomEvent('manifest:data-mutated', {
@@ -2157,10 +2215,11 @@ async function executeMutation(mutationConfig) {
2157
2215
  addEntryToStore(dataSourceName, optimisticData, options);
2158
2216
  originalData = null; // Nothing to rollback for create
2159
2217
  } else if (type === 'update') {
2160
- // Store original entry for rollback
2218
+ // Snapshot for rollback (the row itself is mutated in place)
2161
2219
  const store = Alpine.store('data');
2162
2220
  if (store && store[dataSourceName] && Array.isArray(store[dataSourceName])) {
2163
- originalData = store[dataSourceName].find(e => e.$id === entryId);
2221
+ const found = store[dataSourceName].find(e => e.$id === entryId);
2222
+ originalData = found ? { ...found } : null;
2164
2223
  }
2165
2224
 
2166
2225
  // Set updating state
@@ -2174,10 +2233,11 @@ async function executeMutation(mutationConfig) {
2174
2233
  updateEntryInStore(dataSourceName, entryId, data, options);
2175
2234
  }
2176
2235
  } else if (type === 'delete') {
2177
- // Store original entry for rollback
2236
+ // Snapshot for rollback
2178
2237
  const store = Alpine.store('data');
2179
2238
  if (store && store[dataSourceName] && Array.isArray(store[dataSourceName])) {
2180
- originalData = store[dataSourceName].find(e => e.$id === entryId);
2239
+ const found = store[dataSourceName].find(e => e.$id === entryId);
2240
+ originalData = found ? { ...found } : null;
2181
2241
  }
2182
2242
 
2183
2243
  // Set deleting state
@@ -2207,47 +2267,14 @@ async function executeMutation(mutationConfig) {
2207
2267
  // Step 4: Background sync - update with server response
2208
2268
  if (result && result.$id) {
2209
2269
  if (type === 'create') {
2210
- // Replace temporary entry with real one from server
2211
- const store = Alpine.store('data');
2212
- if (store && store[dataSourceName] && Array.isArray(store[dataSourceName])) {
2213
- const index = store[dataSourceName].findIndex(e => e.$id === optimisticData.$id);
2270
+ // Ack: swap the temp row for the server row in place (new $id → new $files binding)
2271
+ const { store, arr, index } = findRawIndex(dataSourceName, optimisticData.$id);
2272
+ if (store && arr) {
2214
2273
  if (index !== -1) {
2215
- // Remove temporary entry and add real one
2216
- const currentArray = store[dataSourceName];
2217
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
2218
- const newArray = currentArray.map((entry, i) => {
2219
- if (i === index) {
2220
- // Replace with server response
2221
- return createReactiveReferences
2222
- ? createReactiveReferences(result, dataSourceName)
2223
- : result;
2224
- }
2225
- return entry;
2226
- });
2227
- const reactiveArray = createReactiveReferences
2228
- ? createReactiveReferences(newArray, dataSourceName)
2229
- : newArray;
2230
- Alpine.store('data', {
2231
- ...store,
2232
- [dataSourceName]: reactiveArray
2233
- });
2234
- // Attach methods
2235
- if (window.ManifestDataProxies?.attachArrayMethods) {
2236
- const loadDataSource = window.ManifestDataMain?.loadDataSource;
2237
- if (loadDataSource) {
2238
- window.ManifestDataProxies.attachArrayMethods(reactiveArray, dataSourceName, loadDataSource);
2239
- }
2240
- }
2241
- // Clear caches
2242
- if (window.ManifestDataProxies?.clearAccessCache) {
2243
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
2244
- }
2245
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
2246
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
2247
- }
2248
- if (window.ManifestDataProxies?.clearRouteProxyCacheForDataSource) {
2249
- window.ManifestDataProxies.clearRouteProxyCacheForDataSource(dataSourceName);
2250
- }
2274
+ const ds = window.ManifestDataStore;
2275
+ store[dataSourceName].splice(index, 1, ds.createReactiveReferences(result, dataSourceName));
2276
+ ds.touchSource(dataSourceName);
2277
+ clearSourceCaches(dataSourceName);
2251
2278
  } else {
2252
2279
  // Temporary entry not found, just add the real one
2253
2280
  addEntryToStore(dataSourceName, result, options);
@@ -6534,15 +6561,12 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
6534
6561
  if (store) {
6535
6562
  const stateKey = `_${dataSourceName}_state`;
6536
6563
  const currentState = store[stateKey] || { loading: false, error: null, ready: false };
6537
- const updatedStore = {
6538
- ...store,
6539
- [stateKey]: {
6540
- ...currentState,
6541
- error: error?.message || error || 'Operation failed',
6542
- errorTime: Date.now()
6543
- }
6564
+ // State-only write, in place (never replace the store object)
6565
+ store[stateKey] = {
6566
+ ...currentState,
6567
+ error: error?.message || error || 'Operation failed',
6568
+ errorTime: Date.now()
6544
6569
  };
6545
- Alpine.store('data', updatedStore);
6546
6570
  }
6547
6571
  console.error(`[Manifest Data] ${dataSourceName} operation failed:`, error);
6548
6572
  };
@@ -6553,15 +6577,7 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
6553
6577
  const stateKey = `_${dataSourceName}_state`;
6554
6578
  const currentState = store[stateKey];
6555
6579
  if (currentState?.error) {
6556
- const updatedStore = {
6557
- ...store,
6558
- [stateKey]: {
6559
- ...currentState,
6560
- error: null,
6561
- errorTime: null
6562
- }
6563
- };
6564
- Alpine.store('data', updatedStore);
6580
+ store[stateKey] = { ...currentState, error: null, errorTime: null };
6565
6581
  }
6566
6582
  }
6567
6583
  };
@@ -6936,29 +6952,9 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
6936
6952
  tableId,
6937
6953
  appwriteQueries
6938
6954
  );
6939
- // Update store with query results
6940
- const store = Alpine.store('data');
6941
- if (store) {
6942
- // Create a new array reference to ensure Alpine detects the change
6943
- const newArray = Array.isArray(result) ? [...result] : result;
6944
-
6945
- // Use Alpine.store() to replace the entire store, which triggers reactivity
6946
- const currentStore = Alpine.store('data');
6947
- const updatedStore = {
6948
- ...currentStore,
6949
- [dataSourceName]: newArray
6950
- };
6951
- Alpine.store('data', updatedStore);
6952
-
6953
- // Attach methods to the new array reference
6954
- if (Array.isArray(newArray) && window.ManifestDataProxies?.attachArrayMethods) {
6955
- window.ManifestDataProxies.attachArrayMethods(newArray, dataSourceName, reloadDataSource);
6956
- }
6957
-
6958
- // Clear proxy cache to force fresh read
6959
- if (window.ManifestDataProxies?.clearAccessCache) {
6960
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
6961
- }
6955
+ // Network landing: query result replaces the source (coalesced, identity-preserving)
6956
+ if (result !== undefined && window.ManifestDataStore?.landRows) {
6957
+ await window.ManifestDataStore.landRows(dataSourceName, result, { mode: 'replace' });
6962
6958
  }
6963
6959
  return result;
6964
6960
  }
@@ -7267,33 +7263,13 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7267
7263
  // We already have the real file from the API response, so no
7268
7264
  // background reload; scope filtering (if any) is handled by realtime.
7269
7265
  if (!addEntryToStore) {
7270
- // Fallback to old behavior
7266
+ // Fallback: reload lands through loadDataSource (coalesced landing)
7271
7267
  if (window.ManifestDataStore?.dataSourceCache) {
7272
7268
  const cacheKey = `${dataSourceName}:en`;
7273
7269
  window.ManifestDataStore.dataSourceCache.delete(cacheKey);
7274
7270
  }
7275
7271
  clearAccessCache(dataSourceName);
7276
- const reloadedData = await reloadDataSource(dataSourceName);
7277
- if (reloadedData && Array.isArray(reloadedData) && typeof Alpine !== 'undefined' && Alpine.store) {
7278
- const store = Alpine.store('data');
7279
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
7280
- const newArray = createReactiveReferences
7281
- ? createReactiveReferences(reloadedData, dataSourceName)
7282
- : reloadedData.map(entry => ({ ...entry }));
7283
- Alpine.store('data', {
7284
- ...store,
7285
- [dataSourceName]: newArray
7286
- });
7287
- if (window.ManifestDataProxies?.attachArrayMethods) {
7288
- window.ManifestDataProxies.attachArrayMethods(newArray, dataSourceName, reloadDataSource);
7289
- }
7290
- if (window.ManifestDataProxies?.clearAccessCache) {
7291
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
7292
- }
7293
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
7294
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
7295
- }
7296
- }
7272
+ await reloadDataSource(dataSourceName);
7297
7273
  }
7298
7274
 
7299
7275
  return result;
@@ -7677,53 +7653,19 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7677
7653
  // Do delete and reload in parallel
7678
7654
  const [result, reloadedData] = await Promise.all([deletePromise, reloadPromise]);
7679
7655
 
7680
- // Update store
7681
- const store = Alpine.store('data');
7682
- if (store && reloadedData) {
7683
- const newArray = Array.isArray(reloadedData) ? [...reloadedData] : reloadedData;
7684
- const currentStore = Alpine.store('data');
7685
- const updatedStore = {
7686
- ...currentStore,
7687
- [dataSourceName]: newArray
7688
- };
7689
- Alpine.store('data', updatedStore);
7690
- // Attach methods to the new array reference
7691
- if (Array.isArray(newArray) && window.ManifestDataProxies?.attachArrayMethods) {
7692
- window.ManifestDataProxies.attachArrayMethods(newArray, dataSourceName, reloadDataSource);
7693
- }
7694
- if (window.ManifestDataProxies?.clearAccessCache) {
7695
- window.ManifestDataProxies.clearAccessCache(dataSourceName);
7696
- }
7697
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
7698
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(dataSourceName);
7699
- }
7656
+ // Network landing: reloaded bucket listing replaces the source (coalesced)
7657
+ if (reloadedData && window.ManifestDataStore?.landRows) {
7658
+ await window.ManifestDataStore.landRows(dataSourceName, reloadedData, { mode: 'replace' });
7700
7659
  }
7701
7660
 
7702
- // Reload affected table data sources to ensure Alpine reactivity
7703
- // This ensures project fileIds arrays and counters update in the UI
7661
+ // Reload affected table data sources so fileIds arrays and counters
7662
+ // update in the UI (loadDataSource lands the rows itself)
7704
7663
  const reloadDataSourceFunc = window.ManifestDataMain?._loadDataSource;
7705
7664
  if (reloadDataSourceFunc && Object.keys(affectedEntries).length > 0) {
7706
7665
  const reloadPromises = [];
7707
7666
  for (const tableName of Object.keys(affectedEntries)) {
7708
7667
  reloadPromises.push(
7709
- reloadDataSourceFunc(tableName).then(reloadedData => {
7710
- // Ensure we create new object references so Alpine detects nested property changes
7711
- if (reloadedData && Array.isArray(reloadedData) && typeof Alpine !== 'undefined' && Alpine.store) {
7712
- const store = Alpine.store('data');
7713
- const newArray = reloadedData.map(entry => ({ ...entry }));
7714
- Alpine.store('data', {
7715
- ...store,
7716
- [tableName]: newArray
7717
- });
7718
- // Clear cache for reactivity
7719
- if (window.ManifestDataProxies?.clearAccessCache) {
7720
- window.ManifestDataProxies.clearAccessCache(tableName);
7721
- }
7722
- if (window.ManifestDataProxies?.clearArrayProxyCacheForDataSource) {
7723
- window.ManifestDataProxies.clearArrayProxyCacheForDataSource(tableName);
7724
- }
7725
- }
7726
- }).catch(err => {
7668
+ reloadDataSourceFunc(tableName).catch(err => {
7727
7669
  console.warn(`[Manifest Data] Failed to reload ${tableName} after file delete:`, err);
7728
7670
  })
7729
7671
  );
@@ -8348,6 +8290,15 @@ function registerXMagicMethod(loadDataSource) {
8348
8290
  };
8349
8291
  }
8350
8292
 
8293
+ // $x.all — lazy cross-source array, versioned by _v.all
8294
+ if (prop === 'all') {
8295
+ return window.ManifestDataStore?.getAll?.() ?? [];
8296
+ }
8297
+
8298
+ // Per-source subscription: read _v[prop] only (never the whole
8299
+ // store or _dataVersion) so landings elsewhere don't re-run us
8300
+ const track = () => { const v = Alpine.store('data')?._v; if (v) void v[prop]; };
8301
+
8351
8302
  // Resolve+cache from raw data BEFORE reading Alpine.store('data'):
8352
8303
  // the store read registers a reactive dep that can re-run this
8353
8304
  // effect and re-enter get() → stack overflow. Caching first makes
@@ -8363,9 +8314,7 @@ function registerXMagicMethod(loadDataSource) {
8363
8314
  if (nestedCache.has(prop) && hasData) {
8364
8315
  const cachedProxy = nestedCache.get(prop);
8365
8316
  if (cachedProxy) {
8366
- // Subscribe to store so locale change (updateStore) triggers re-run; we only read _dataVersion, still return cached proxy.
8367
- const store = Alpine.store('data');
8368
- void (store && store._dataVersion);
8317
+ track(); // locale change / reload re-runs us; still the cached proxy
8369
8318
  return cachedProxy;
8370
8319
  }
8371
8320
  }
@@ -8378,7 +8327,7 @@ function registerXMagicMethod(loadDataSource) {
8378
8327
  const nestedProxy = createNestedObjectProxy(rawValueEarly, prop, reloadDataSource, []);
8379
8328
  if (nestedProxy) {
8380
8329
  nestedCache.set(prop, nestedProxy);
8381
- void (Alpine.store('data') && Alpine.store('data')._dataVersion); // reactivity only
8330
+ track();
8382
8331
  return nestedProxy;
8383
8332
  }
8384
8333
  } catch (e) {
@@ -8388,9 +8337,9 @@ function registerXMagicMethod(loadDataSource) {
8388
8337
  }
8389
8338
  }
8390
8339
 
8391
- // No raw data yet: start load, subscribe (read _dataVersion so
8392
- // Alpine tracks the dep and re-runs on updateStore), return loading
8393
- // proxy. Reading only the version avoids re-entry/overflow.
8340
+ // No raw data yet: start load, subscribe to _v[prop] (re-runs when
8341
+ // this source lands), return loading proxy. Reading only the
8342
+ // version avoids re-entry/overflow.
8394
8343
  if (!hasData) {
8395
8344
  if (!pendingLoads.has(prop)) {
8396
8345
  const locale = typeof document !== 'undefined' && document.documentElement
@@ -8401,8 +8350,7 @@ function registerXMagicMethod(loadDataSource) {
8401
8350
  setTimeout(() => pendingLoads.delete(prop), 1000);
8402
8351
  });
8403
8352
  }
8404
- const store = Alpine.store('data');
8405
- void (store && store._dataVersion);
8353
+ track();
8406
8354
  const createLoadingProxy = window.ManifestDataProxiesCore?.createLoadingProxy;
8407
8355
  if (createLoadingProxy) {
8408
8356
  return createLoadingProxy(prop);
@@ -8412,7 +8360,7 @@ function registerXMagicMethod(loadDataSource) {
8412
8360
 
8413
8361
  // Get current store for paths that need it (arrays, or object with raw data for cache/consistency)
8414
8362
  const currentStoreForCache = Alpine.store('data');
8415
- void (currentStoreForCache && currentStoreForCache._dataVersion);
8363
+ track();
8416
8364
 
8417
8365
  // No activeProps circular check here: a legit store-read re-entry
8418
8366
  // would be misread as circular and return a loading proxy, breaking
@@ -10472,38 +10420,15 @@ async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, event
10472
10420
  return;
10473
10421
  }
10474
10422
 
10475
- // Use scoped updates via mutation system
10476
- const addEntryToStore = window.ManifestDataMutations?.addEntryToStore;
10477
- const updateEntryInStore = window.ManifestDataMutations?.updateEntryInStore;
10478
- const removeEntryFromStore = window.ManifestDataMutations?.removeEntryFromStore;
10423
+ // Network landings: coalesced per frame (landRows upserts by $id), never the sync mutation path
10424
+ const { landRows, landRemove } = window.ManifestDataStore;
10479
10425
 
10480
10426
  if (eventType === 'create') {
10481
- // New file created - check if it matches scope before adding
10482
10427
  const file = payload?.$id ? payload : (payload?.file || payload);
10483
10428
  if (file && file.$id) {
10484
- // Check if file matches current scope
10485
10429
  const fileMatchesScope = await checkFileMatchesScope(file, scope);
10486
10430
  if (fileMatchesScope) {
10487
- // Check if already exists
10488
- const exists = currentFiles.some(f => f.$id === file.$id);
10489
- if (!exists && addEntryToStore) {
10490
- // Use scoped update: add only this file
10491
- addEntryToStore(dataSourceName, file);
10492
- } else if (!exists) {
10493
- // Fallback: update entire array
10494
- const updatedFiles = [...currentFiles, file];
10495
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10496
- const reactiveFiles = createReactiveReferences
10497
- ? createReactiveReferences(updatedFiles, dataSourceName)
10498
- : updatedFiles;
10499
- Alpine.store('data', { ...store, [dataSourceName]: reactiveFiles });
10500
- if (window.ManifestDataProxies?.attachArrayMethods) {
10501
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10502
- if (loadDataSource) {
10503
- window.ManifestDataProxies.attachArrayMethods(reactiveFiles, dataSourceName, loadDataSource);
10504
- }
10505
- }
10506
- }
10431
+ landRows(dataSourceName, [file], { mode: 'append' });
10507
10432
 
10508
10433
  // Emit custom event for new file creation so UI can refresh project files
10509
10434
  window.dispatchEvent(new CustomEvent('manifest:file-created', {
@@ -10514,97 +10439,20 @@ async function handleStorageRealtimeEvent(dataSourceName, bucketId, scope, event
10514
10439
  console.warn('[Manifest Data] Invalid file payload in create event:', payload);
10515
10440
  }
10516
10441
  } else if (eventType === 'update') {
10517
- // File updated - update in array if it exists
10518
10442
  const file = payload?.$id ? payload : (payload?.file || payload);
10519
10443
  if (file && file.$id) {
10520
- const existingFile = currentFiles.find(f => f.$id === file.$id);
10521
- if (existingFile) {
10522
- // Check if file still matches scope after update
10523
- const fileMatchesScope = await checkFileMatchesScope(file, scope);
10524
- if (fileMatchesScope) {
10525
- // Use scoped update: update only this file
10526
- if (updateEntryInStore) {
10527
- updateEntryInStore(dataSourceName, file.$id, file);
10528
- } else {
10529
- // Fallback: update entire array
10530
- const updatedFiles = currentFiles.map(f => f.$id === file.$id ? file : f);
10531
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10532
- const reactiveFiles = createReactiveReferences
10533
- ? createReactiveReferences(updatedFiles)
10534
- : updatedFiles;
10535
- Alpine.store('data', { ...store, [dataSourceName]: reactiveFiles });
10536
- if (window.ManifestDataProxies?.attachArrayMethods) {
10537
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10538
- if (loadDataSource) {
10539
- window.ManifestDataProxies.attachArrayMethods(reactiveFiles, dataSourceName, loadDataSource);
10540
- }
10541
- }
10542
- }
10543
- } else {
10544
- // File no longer matches scope, remove it
10545
- if (removeEntryFromStore) {
10546
- removeEntryFromStore(dataSourceName, file.$id);
10547
- } else {
10548
- // Fallback: update entire array
10549
- const updatedFiles = currentFiles.filter(f => f.$id !== file.$id);
10550
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10551
- const reactiveFiles = createReactiveReferences
10552
- ? createReactiveReferences(updatedFiles)
10553
- : updatedFiles;
10554
- Alpine.store('data', { ...store, [dataSourceName]: reactiveFiles });
10555
- if (window.ManifestDataProxies?.attachArrayMethods) {
10556
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10557
- if (loadDataSource) {
10558
- window.ManifestDataProxies.attachArrayMethods(reactiveFiles, dataSourceName, loadDataSource);
10559
- }
10560
- }
10561
- }
10562
- }
10444
+ const fileMatchesScope = await checkFileMatchesScope(file, scope);
10445
+ if (fileMatchesScope) {
10446
+ landRows(dataSourceName, [file], { mode: 'append' });
10563
10447
  } else {
10564
- // File not in list, but might match scope now - add it
10565
- const fileMatchesScope = await checkFileMatchesScope(file, scope);
10566
- if (fileMatchesScope && addEntryToStore) {
10567
- addEntryToStore(dataSourceName, file);
10568
- } else if (fileMatchesScope) {
10569
- // Fallback: update entire array
10570
- const updatedFiles = [...currentFiles, file];
10571
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10572
- const reactiveFiles = createReactiveReferences
10573
- ? createReactiveReferences(updatedFiles, dataSourceName)
10574
- : updatedFiles;
10575
- Alpine.store('data', { ...store, [dataSourceName]: reactiveFiles });
10576
- if (window.ManifestDataProxies?.attachArrayMethods) {
10577
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10578
- if (loadDataSource) {
10579
- window.ManifestDataProxies.attachArrayMethods(reactiveFiles, dataSourceName, loadDataSource);
10580
- }
10581
- }
10582
- }
10448
+ // File no longer matches scope, remove it
10449
+ landRemove(dataSourceName, [file.$id]);
10583
10450
  }
10584
10451
  }
10585
10452
  } else if (eventType === 'delete') {
10586
- // File deleted - remove from array
10587
10453
  const fileId = payload?.$id || payload?.file?.$id || payload?.fileId || payload;
10588
10454
  if (fileId) {
10589
- const actualFileId = fileId.$id || fileId;
10590
- if (removeEntryFromStore) {
10591
- // Use scoped update: remove only this file
10592
- removeEntryFromStore(dataSourceName, actualFileId);
10593
- } else {
10594
- // Fallback: update entire array
10595
- const updatedFiles = currentFiles.filter(f => f.$id !== actualFileId);
10596
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10597
- const reactiveFiles = createReactiveReferences
10598
- ? createReactiveReferences(updatedFiles)
10599
- : updatedFiles;
10600
- Alpine.store('data', { ...store, [dataSourceName]: reactiveFiles });
10601
- if (window.ManifestDataProxies?.attachArrayMethods) {
10602
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10603
- if (loadDataSource) {
10604
- window.ManifestDataProxies.attachArrayMethods(reactiveFiles, dataSourceName, loadDataSource);
10605
- }
10606
- }
10607
- }
10455
+ landRemove(dataSourceName, [fileId.$id || fileId]);
10608
10456
  }
10609
10457
  }
10610
10458
  }
@@ -10676,201 +10524,105 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
10676
10524
  return;
10677
10525
  }
10678
10526
 
10679
- let updatedRows = [...currentRows];
10680
-
10681
- // Use scoped updates via mutation system
10682
- const addEntryToStore = window.ManifestDataMutations?.addEntryToStore;
10683
- const updateEntryInStore = window.ManifestDataMutations?.updateEntryInStore;
10684
- const removeEntryFromStore = window.ManifestDataMutations?.removeEntryFromStore;
10527
+ // Network landings: coalesced per frame (landRows upserts by $id), never the sync mutation path
10528
+ const { landRows, landRemove } = window.ManifestDataStore;
10685
10529
 
10686
10530
  if (eventType === 'create') {
10687
- // New row created - check if it matches scope before adding
10688
10531
  const row = payload?.$id ? payload : (payload?.row || payload);
10689
10532
  if (row && row.$id) {
10690
- // Check if row matches current scope
10691
10533
  const rowMatchesScope = await checkRowMatchesScope(row, scope);
10692
10534
  if (rowMatchesScope) {
10693
- // Check if already exists
10694
- const exists = currentRows.some(r => r.$id === row.$id);
10695
- if (!exists && addEntryToStore) {
10696
- // Use scoped update: add only this entry
10697
- addEntryToStore(dataSourceName, row);
10698
- } else if (!exists) {
10699
- // Fallback: update entire array
10700
- const updatedRows = [...currentRows, row];
10701
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10702
- const reactiveRows = createReactiveReferences
10703
- ? createReactiveReferences(updatedRows, dataSourceName)
10704
- : updatedRows;
10705
- Alpine.store('data', { ...store, [dataSourceName]: reactiveRows });
10706
- if (window.ManifestDataProxies?.attachArrayMethods) {
10707
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10708
- if (loadDataSource) {
10709
- window.ManifestDataProxies.attachArrayMethods(reactiveRows, dataSourceName, loadDataSource);
10710
- }
10711
- }
10712
- }
10535
+ landRows(dataSourceName, [row], { mode: 'append' });
10713
10536
  }
10714
10537
  } else {
10715
10538
  console.warn('[Manifest Data] Invalid row payload in create event:', payload);
10716
10539
  }
10717
10540
  } else if (eventType === 'update') {
10718
- // Row updated - update in array if it exists
10719
10541
  const row = payload?.$id ? payload : (payload?.row || payload);
10720
10542
  if (row && row.$id) {
10721
10543
  const existingRow = currentRows.find(r => r.$id === row.$id);
10722
- if (existingRow) {
10723
- // Check if row still matches scope after update
10724
- const rowMatchesScope = await checkRowMatchesScope(row, scope);
10725
- if (rowMatchesScope) {
10726
- // Most sources update unconditionally; projects-with-fileIds get
10727
- // special handling to protect optimistic uploads (see below).
10728
- const existingFileIds = existingRow?.fileIds || [];
10729
- const incomingFileIds = row?.fileIds || [];
10730
- const hasFileIds = existingFileIds.length > 0 || incomingFileIds.length > 0;
10731
- const isProjectWithFiles = hasFileIds && dataSourceName === 'projects';
10732
-
10733
- let shouldUpdate = true;
10734
-
10735
- if (isProjectWithFiles) {
10736
- const existingUpdatedAt = existingRow?.$updatedAt || existingRow?.$sequence;
10737
- const newUpdatedAt = row?.$updatedAt || row?.$sequence;
10738
- const shouldUpdateByTimestamp = !newUpdatedAt || !existingUpdatedAt || newUpdatedAt !== existingUpdatedAt;
10739
-
10740
- if (!shouldUpdateByTimestamp) {
10741
- shouldUpdate = false; // identical timestamps → no-op
10742
- } else {
10743
- const existingFileIdsSet = new Set(existingFileIds);
10744
- const incomingFileIdsSet = new Set(incomingFileIds);
10745
-
10746
- const isSuperset = incomingFileIds.every(id => existingFileIdsSet.has(id)) &&
10747
- incomingFileIds.length > existingFileIds.length;
10748
-
10749
- const isMissingFiles = existingFileIds.some(id => !incomingFileIdsSet.has(id));
10750
-
10751
- const timestampDiff = newUpdatedAt > existingUpdatedAt ?
10752
- (new Date(newUpdatedAt) - new Date(existingUpdatedAt)) :
10753
- (new Date(existingUpdatedAt) - new Date(newUpdatedAt));
10754
- const isDefinitelyNewer = timestampDiff > 1000; // ms
10755
-
10756
- // Incoming missing files + not clearly newer = stale event
10757
- // racing an optimistic update; ignore it to protect the upload.
10758
- if (isMissingFiles && !isDefinitelyNewer) {
10759
- console.warn('[Manifest Data] Ignoring stale realtime update (protecting optimistic update):', {
10760
- projectId: row.$id,
10761
- existingFileIds: existingFileIds,
10762
- incomingFileIds: incomingFileIds,
10763
- existingFileIdsCount: existingFileIds.length,
10764
- incomingFileIdsCount: incomingFileIds.length,
10765
- existingUpdatedAt,
10766
- newUpdatedAt,
10767
- timestampDiff,
10768
- isMissingFiles,
10769
- isDefinitelyNewer,
10770
- reason: 'Incoming data missing files that exist in store - likely stale realtime event'
10771
- });
10772
- shouldUpdate = false;
10773
- }
10774
- }
10775
- }
10544
+ const rowMatchesScope = await checkRowMatchesScope(row, scope);
10545
+ if (!rowMatchesScope) {
10546
+ // Row no longer matches scope, remove it
10547
+ if (existingRow) landRemove(dataSourceName, [row.$id]);
10548
+ return;
10549
+ }
10550
+ if (!existingRow) {
10551
+ // Row not in list, but matches scope now - add it
10552
+ landRows(dataSourceName, [row], { mode: 'append' });
10553
+ return;
10554
+ }
10776
10555
 
10777
- // Apply the update if shouldUpdate is true
10778
- if (shouldUpdate) {
10779
- // Check if fileIds changed (for projects with linked files)
10780
- const fileIdsChanged = isProjectWithFiles &&
10781
- JSON.stringify(existingFileIds) !== JSON.stringify(incomingFileIds);
10556
+ // Most sources update unconditionally; projects-with-fileIds get
10557
+ // special handling to protect optimistic uploads (see below).
10558
+ const existingFileIds = existingRow?.fileIds || [];
10559
+ const incomingFileIds = row?.fileIds || [];
10560
+ const hasFileIds = existingFileIds.length > 0 || incomingFileIds.length > 0;
10561
+ const isProjectWithFiles = hasFileIds && dataSourceName === 'projects';
10782
10562
 
10783
- // Use scoped update: update only this entry
10784
- if (updateEntryInStore) {
10785
- updateEntryInStore(dataSourceName, row.$id, row);
10786
- } else {
10787
- // Fallback: update entire array
10788
- const updatedRows = currentRows.map(r => r.$id === row.$id ? row : r);
10789
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10790
- const reactiveRows = createReactiveReferences
10791
- ? createReactiveReferences(updatedRows, dataSourceName)
10792
- : updatedRows;
10793
- Alpine.store('data', { ...store, [dataSourceName]: reactiveRows });
10794
- if (window.ManifestDataProxies?.attachArrayMethods) {
10795
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10796
- if (loadDataSource) {
10797
- window.ManifestDataProxies.attachArrayMethods(reactiveRows, dataSourceName, loadDataSource);
10798
- }
10799
- }
10800
- }
10563
+ let shouldUpdate = true;
10801
10564
 
10802
- // Emit custom event for project file updates so UI can refresh
10803
- if (fileIdsChanged && dataSourceName === 'projects') {
10804
- window.dispatchEvent(new CustomEvent('manifest:project-files-updated', {
10805
- detail: { projectId: row.$id, fileIds: row.fileIds }
10806
- }));
10807
- }
10808
- }
10565
+ if (isProjectWithFiles) {
10566
+ const existingUpdatedAt = existingRow?.$updatedAt || existingRow?.$sequence;
10567
+ const newUpdatedAt = row?.$updatedAt || row?.$sequence;
10568
+ const shouldUpdateByTimestamp = !newUpdatedAt || !existingUpdatedAt || newUpdatedAt !== existingUpdatedAt;
10569
+
10570
+ if (!shouldUpdateByTimestamp) {
10571
+ shouldUpdate = false; // identical timestamps → no-op
10809
10572
  } else {
10810
- // Row no longer matches scope, remove it
10811
- if (removeEntryFromStore) {
10812
- removeEntryFromStore(dataSourceName, row.$id);
10813
- } else {
10814
- // Fallback: update entire array
10815
- const updatedRows = currentRows.filter(r => r.$id !== row.$id);
10816
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10817
- const reactiveRows = createReactiveReferences
10818
- ? createReactiveReferences(updatedRows, dataSourceName)
10819
- : updatedRows;
10820
- Alpine.store('data', { ...store, [dataSourceName]: reactiveRows });
10821
- if (window.ManifestDataProxies?.attachArrayMethods) {
10822
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10823
- if (loadDataSource) {
10824
- window.ManifestDataProxies.attachArrayMethods(reactiveRows, dataSourceName, loadDataSource);
10825
- }
10826
- }
10573
+ const existingFileIdsSet = new Set(existingFileIds);
10574
+ const incomingFileIdsSet = new Set(incomingFileIds);
10575
+
10576
+ const isSuperset = incomingFileIds.every(id => existingFileIdsSet.has(id)) &&
10577
+ incomingFileIds.length > existingFileIds.length;
10578
+
10579
+ const isMissingFiles = existingFileIds.some(id => !incomingFileIdsSet.has(id));
10580
+
10581
+ const timestampDiff = newUpdatedAt > existingUpdatedAt ?
10582
+ (new Date(newUpdatedAt) - new Date(existingUpdatedAt)) :
10583
+ (new Date(existingUpdatedAt) - new Date(newUpdatedAt));
10584
+ const isDefinitelyNewer = timestampDiff > 1000; // ms
10585
+
10586
+ // Incoming missing files + not clearly newer = stale event
10587
+ // racing an optimistic update; ignore it to protect the upload.
10588
+ if (isMissingFiles && !isDefinitelyNewer) {
10589
+ console.warn('[Manifest Data] Ignoring stale realtime update (protecting optimistic update):', {
10590
+ projectId: row.$id,
10591
+ existingFileIds: existingFileIds,
10592
+ incomingFileIds: incomingFileIds,
10593
+ existingFileIdsCount: existingFileIds.length,
10594
+ incomingFileIdsCount: incomingFileIds.length,
10595
+ existingUpdatedAt,
10596
+ newUpdatedAt,
10597
+ timestampDiff,
10598
+ isMissingFiles,
10599
+ isDefinitelyNewer,
10600
+ reason: 'Incoming data missing files that exist in store - likely stale realtime event'
10601
+ });
10602
+ shouldUpdate = false;
10827
10603
  }
10828
10604
  }
10829
- } else {
10830
- // Row not in list, but might match scope now - add it
10831
- const rowMatchesScope = await checkRowMatchesScope(row, scope);
10832
- if (rowMatchesScope && addEntryToStore) {
10833
- addEntryToStore(dataSourceName, row);
10834
- } else if (rowMatchesScope) {
10835
- // Fallback: update entire array
10836
- const updatedRows = [...currentRows, row];
10837
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10838
- const reactiveRows = createReactiveReferences
10839
- ? createReactiveReferences(updatedRows, dataSourceName)
10840
- : updatedRows;
10841
- Alpine.store('data', { ...store, [dataSourceName]: reactiveRows });
10842
- if (window.ManifestDataProxies?.attachArrayMethods) {
10843
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10844
- if (loadDataSource) {
10845
- window.ManifestDataProxies.attachArrayMethods(reactiveRows, dataSourceName, loadDataSource);
10846
- }
10847
- }
10605
+ }
10606
+
10607
+ if (shouldUpdate) {
10608
+ // Check if fileIds changed (for projects with linked files)
10609
+ const fileIdsChanged = isProjectWithFiles &&
10610
+ JSON.stringify(existingFileIds) !== JSON.stringify(incomingFileIds);
10611
+
10612
+ landRows(dataSourceName, [row], { mode: 'append' });
10613
+
10614
+ // Emit custom event for project file updates so UI can refresh
10615
+ if (fileIdsChanged && dataSourceName === 'projects') {
10616
+ window.dispatchEvent(new CustomEvent('manifest:project-files-updated', {
10617
+ detail: { projectId: row.$id, fileIds: row.fileIds }
10618
+ }));
10848
10619
  }
10849
10620
  }
10850
10621
  }
10851
10622
  } else if (eventType === 'delete') {
10852
- // Row deleted - remove from array
10853
10623
  const rowId = payload?.$id || payload?.row?.$id || payload?.rowId || payload;
10854
10624
  if (rowId) {
10855
- const actualRowId = rowId.$id || rowId;
10856
- if (removeEntryFromStore) {
10857
- // Use scoped update: remove only this entry
10858
- removeEntryFromStore(dataSourceName, actualRowId);
10859
- } else {
10860
- // Fallback: update entire array
10861
- const updatedRows = currentRows.filter(r => r.$id !== actualRowId);
10862
- const createReactiveReferences = window.ManifestDataStore?.createReactiveReferences;
10863
- const reactiveRows = createReactiveReferences
10864
- ? createReactiveReferences(updatedRows, dataSourceName)
10865
- : updatedRows;
10866
- Alpine.store('data', { ...store, [dataSourceName]: reactiveRows });
10867
- if (window.ManifestDataProxies?.attachArrayMethods) {
10868
- const loadDataSource = window.ManifestDataMain?._loadDataSource;
10869
- if (loadDataSource) {
10870
- window.ManifestDataProxies.attachArrayMethods(reactiveRows, dataSourceName, loadDataSource);
10871
- }
10872
- }
10873
- }
10625
+ landRemove(dataSourceName, [rowId.$id || rowId]);
10874
10626
  }
10875
10627
  }
10876
10628
 
@@ -11002,7 +10754,7 @@ async function checkFileMatchesScope(file, scope) {
11002
10754
  // Load dataSource data
11003
10755
  async function loadDataSource(dataSourceName, locale = 'en') {
11004
10756
  const cacheKey = `${dataSourceName}:${locale}`;
11005
- const { dataSourceCache, loadingPromises, isInitializing, updateStore } = window.ManifestDataStore;
10757
+ const { dataSourceCache, loadingPromises, isInitializing, updateStore, landRows } = window.ManifestDataStore;
11006
10758
 
11007
10759
  // Memory cache. Serving cached data is fine, but writing an old locale to the
11008
10760
  // live store would clobber a concurrent locale switch — so guard the store
@@ -11013,7 +10765,8 @@ async function loadDataSource(dataSourceName, locale = 'en') {
11013
10765
  || document.documentElement.lang || locale;
11014
10766
  const staleLocaleHit = locale !== liveLocale && !!(cachedData && cachedData._locale);
11015
10767
  if (!isInitializing && !staleLocaleHit) {
11016
- updateStore(dataSourceName, cachedData, { loading: false, error: null, ready: true });
10768
+ // Landing (coalesced): resolves once the cached rows are visible
10769
+ await landRows(dataSourceName, cachedData, { mode: 'replace', loading: false, error: null, ready: true });
11017
10770
  }
11018
10771
  return cachedData;
11019
10772
  }
@@ -11290,10 +11043,9 @@ async function loadDataSource(dataSourceName, locale = 'en') {
11290
11043
  || document.documentElement.lang || locale;
11291
11044
  const staleLocale = localeSensitive && liveLocale !== locale;
11292
11045
 
11293
- // Update store only if not initializing
11294
- // Note: updateStore will seal the data to prevent Alpine from proxying it
11046
+ // Network landing (coalesced per frame); resolves once visible
11295
11047
  if (!isInitializing && !staleLocale) {
11296
- updateStore(dataSourceName, enhancedData, { loading: false, error: null, ready: true });
11048
+ await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true });
11297
11049
  }
11298
11050
 
11299
11051
  // Return unsealed version for our proxy system
@@ -11413,7 +11165,7 @@ function setupUrlChangeListeners() {
11413
11165
  // Initialize plugin when either DOM is ready or Alpine is ready
11414
11166
  async function initializeDataSourcesPlugin() {
11415
11167
 
11416
- const { initializeStore, setupLocaleChangeListener, setupTeamChangeListener, setIsInitializing, setInitializationComplete, updateStore } = window.ManifestDataStore;
11168
+ const { initializeStore, setupLocaleChangeListener, setupTeamChangeListener, setIsInitializing, setInitializationComplete, updateStore, landRows } = window.ManifestDataStore;
11417
11169
 
11418
11170
  // Initialize empty data sources store
11419
11171
  initializeStore();
@@ -11484,16 +11236,11 @@ async function initializeDataSourcesPlugin() {
11484
11236
  setIsInitializing(true);
11485
11237
 
11486
11238
  try {
11487
- // Initialize store - preserve existing store properties like _currentUrl
11488
- const existingStore = Alpine.store('data') || {};
11489
- Alpine.store('data', {
11490
- ...existingStore,
11491
- all: [],
11492
- _initialized: true,
11493
- _ready: false,
11494
- // Ensure _currentUrl is preserved or initialized
11495
- _currentUrl: existingStore._currentUrl || window.location.pathname
11496
- });
11239
+ // Mark the store initialized in place (never replace the store object)
11240
+ const dataStore = Alpine.store('data');
11241
+ dataStore._initialized = true;
11242
+ dataStore._ready = false;
11243
+ if (!dataStore._currentUrl) dataStore._currentUrl = window.location.pathname;
11497
11244
 
11498
11245
  // Pre-load local file-backed sources so $x.* renders real data first pass;
11499
11246
  // Appwrite, API-URL, and "manifest" sources stay on-demand.
@@ -11516,8 +11263,9 @@ async function initializeDataSourcesPlugin() {
11516
11263
  preloadNames.map(async (name) => {
11517
11264
  try {
11518
11265
  const data = await loadDataSource(name, locale);
11519
- if (data != null && window.ManifestDataStore?.updateStore) {
11520
- window.ManifestDataStore.updateStore(name, data, { loading: false, error: null, ready: true, allowDuringInit: true });
11266
+ if (data != null) {
11267
+ // Boot landing: every pre-loaded source lands in ONE flush
11268
+ await landRows(name, data, { mode: 'replace', loading: false, error: null, ready: true, allowDuringInit: true });
11521
11269
  }
11522
11270
  } catch (err) {
11523
11271
  console.warn(`[Manifest Data] Failed to pre-load ${name}:`, err);
@@ -11536,36 +11284,17 @@ async function initializeDataSourcesPlugin() {
11536
11284
  // flag updateStore short-circuits and $x.manifest stays unpopulated.
11537
11285
  window.ManifestDataStore.dataSourceCache.set(`manifest:${locale}`, publicManifest);
11538
11286
  updateStore('manifest', publicManifest, { loading: false, error: null, ready: true, allowDuringInit: true });
11539
-
11540
- const store = Alpine.store('data');
11541
- Alpine.store('data', {
11542
- ...store,
11543
- _ready: true
11544
- });
11545
- } else {
11546
- // No manifest data source, mark as ready anyway
11547
- const store = Alpine.store('data');
11548
- Alpine.store('data', {
11549
- ...store,
11550
- _ready: true
11551
- });
11552
11287
  }
11288
+ Alpine.store('data')._ready = true;
11553
11289
  } catch (error) {
11554
11290
  // If manifest pre-load fails, mark as ready anyway - it will load on-demand
11555
11291
  console.warn('[Manifest Data] Failed to pre-load manifest:', error);
11556
- const store = Alpine.store('data');
11557
- Alpine.store('data', {
11558
- ...store,
11559
- _ready: true
11560
- });
11292
+ Alpine.store('data')._ready = true;
11561
11293
  }
11562
11294
 
11563
11295
  // Force Alpine to re-run effects that read $x.content (they may have run before pre-load and got loading proxy)
11564
11296
  const flushThenDispatch = () => {
11565
- if (typeof Alpine !== 'undefined') {
11566
- const s = Alpine.store('data');
11567
- Alpine.store('data', { ...s, _dataVersion: (s._dataVersion || 0) + 1 });
11568
- }
11297
+ window.ManifestDataStore?.bumpAllVersions?.();
11569
11298
  if (typeof window !== 'undefined') {
11570
11299
  window.dispatchEvent(new CustomEvent('manifest:data-ready'));
11571
11300
  }