mnfst 0.5.273 → 0.5.275

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.273';
5
+ const MANIFEST_BUILD_VERSION = '0.5.275';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
@@ -1642,337 +1642,213 @@ window.ManifestDataQueries = {
1642
1642
 
1643
1643
  /* Manifest Data Sources - Pagination (Appwrite) */
1644
1644
 
1645
- // First page, cursor-based
1646
- // Returns { items, cursor, total, hasMore }
1647
- async function getFirstPage(dataSourceName, limit, baseQueries = []) {
1645
+ // Resolve the source's table or bucket
1646
+ async function resolvePagedSource(dataSourceName) {
1648
1647
  const manifest = await window.ManifestDataConfig.ensureManifest();
1649
1648
  if (!manifest?.data) {
1650
1649
  throw new Error('[Manifest Data] Manifest not available');
1651
1650
  }
1652
-
1653
1651
  const dataSource = manifest.data[dataSourceName];
1654
1652
  if (!dataSource) {
1655
1653
  throw new Error(`[Manifest Data] Data source "${dataSourceName}" not found`);
1656
1654
  }
1657
-
1658
- // Check if this is an Appwrite data source
1659
- const isAppwriteTable = window.ManifestDataConfig.isAppwriteCollection(dataSource);
1660
- if (!isAppwriteTable) {
1655
+ if (!window.ManifestDataConfig.isAppwriteCollection(dataSource)) {
1661
1656
  throw new Error(`[Manifest Data] Pagination is only supported for Appwrite data sources`);
1662
1657
  }
1663
-
1664
1658
  const appwriteConfig = await window.ManifestDataConfig.getAppwriteConfig(dataSource);
1665
1659
  if (!appwriteConfig) {
1666
1660
  throw new Error(`[Manifest Data] Invalid Appwrite configuration for "${dataSourceName}"`);
1667
1661
  }
1668
-
1669
1662
  const tableId = window.ManifestDataConfig.getAppwriteTableId(dataSource);
1670
1663
  const bucketId = window.ManifestDataConfig.getAppwriteBucketId(dataSource);
1664
+ if (!tableId && !bucketId) {
1665
+ throw new Error(`[Manifest Data] Data source "${dataSourceName}" is not a table or bucket`);
1666
+ }
1667
+ return { appwriteConfig, tableId, bucketId };
1668
+ }
1671
1669
 
1672
- // Build queries with limit
1673
- const queries = [
1674
- ...baseQueries,
1675
- window.Appwrite.Query.limit(limit)
1676
- ];
1677
-
1670
+ // One page request. Asks for limit + 1 rows: the extra row only answers "is there more"
1671
+ async function fetchPage(dataSourceName, baseQueries, pageQueries, limit) {
1672
+ const { appwriteConfig, tableId, bucketId } = await resolvePagedSource(dataSourceName);
1673
+ const queries = [...baseQueries, ...pageQueries, window.Appwrite.Query.limit(limit + 1)];
1674
+ const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1678
1675
  let response;
1679
1676
  if (tableId) {
1680
- // Table pagination - need to call Appwrite directly to get full response with total
1681
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1682
- if (!services?.tablesDB) {
1683
- throw new Error('[Manifest Data] Appwrite TablesDB service not available');
1684
- }
1685
-
1686
- const appwriteResponse = await services.tablesDB.listRows({
1687
- databaseId: appwriteConfig.databaseId,
1688
- tableId: tableId,
1689
- queries: queries
1690
- });
1691
-
1692
- const items = appwriteResponse.rows || [];
1693
- const total = appwriteResponse.total || 0;
1694
- const cursor = items.length > 0 ? items[items.length - 1].$id : null;
1695
- const hasMore = items.length === limit && total > limit;
1696
-
1697
- return {
1698
- items,
1699
- cursor,
1700
- total,
1701
- hasMore
1702
- };
1703
- } else if (bucketId) {
1704
- // Storage pagination
1705
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1706
- if (!services?.storage) {
1707
- throw new Error('[Manifest Data] Appwrite Storage service not available');
1708
- }
1709
-
1710
- const storageResponse = await services.storage.listFiles(bucketId, queries);
1711
- const items = storageResponse.files || [];
1712
- const total = storageResponse.total || 0;
1713
- const cursor = items.length > 0 ? items[items.length - 1].$id : null;
1714
- const hasMore = items.length === limit && total > limit;
1715
-
1716
- return {
1717
- items,
1718
- cursor,
1719
- total,
1720
- hasMore
1721
- };
1677
+ if (!services?.tablesDB) throw new Error('[Manifest Data] Appwrite TablesDB service not available');
1678
+ response = await services.tablesDB.listRows({ databaseId: appwriteConfig.databaseId, tableId, queries });
1722
1679
  } else {
1723
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" is not a table or bucket`);
1680
+ if (!services?.storage) throw new Error('[Manifest Data] Appwrite Storage service not available');
1681
+ response = await services.storage.listFiles(bucketId, queries);
1724
1682
  }
1683
+ const rows = (tableId ? response.rows : response.files) || [];
1684
+ return { rows, total: response.total || 0, extra: rows.length > limit };
1685
+ }
1686
+
1687
+ // First page, cursor-based. Returns { items, cursor, total, hasMore }
1688
+ async function getFirstPage(dataSourceName, limit, baseQueries = []) {
1689
+ const { rows, total, extra } = await fetchPage(dataSourceName, baseQueries, [], limit);
1690
+ const items = rows.slice(0, limit);
1691
+ return { items, cursor: items.length ? items[items.length - 1].$id : null, total, hasMore: extra };
1725
1692
  }
1726
1693
 
1727
- // Next page from cursor
1728
- // Returns { items, cursor, total, hasMore }
1694
+ // Next page after a cursor. Returns { items, cursor, total, hasMore }
1729
1695
  async function getNextPage(dataSourceName, cursor, limit, baseQueries = []) {
1730
1696
  if (!cursor) {
1731
1697
  throw new Error('[Manifest Data] Cursor is required for next page');
1732
1698
  }
1733
-
1734
- const manifest = await window.ManifestDataConfig.ensureManifest();
1735
- if (!manifest?.data) {
1736
- throw new Error('[Manifest Data] Manifest not available');
1737
- }
1738
-
1739
- const dataSource = manifest.data[dataSourceName];
1740
- if (!dataSource) {
1741
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" not found`);
1742
- }
1743
-
1744
- const appwriteConfig = await window.ManifestDataConfig.getAppwriteConfig(dataSource);
1745
- if (!appwriteConfig) {
1746
- throw new Error(`[Manifest Data] Invalid Appwrite configuration for "${dataSourceName}"`);
1747
- }
1748
-
1749
- const tableId = window.ManifestDataConfig.getAppwriteTableId(dataSource);
1750
- const bucketId = window.ManifestDataConfig.getAppwriteBucketId(dataSource);
1751
-
1752
- // Build queries with cursorAfter and limit
1753
- const queries = [
1754
- ...baseQueries,
1755
- window.Appwrite.Query.cursorAfter(cursor),
1756
- window.Appwrite.Query.limit(limit)
1757
- ];
1758
-
1759
- let response;
1760
- if (tableId) {
1761
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1762
- if (!services?.tablesDB) {
1763
- throw new Error('[Manifest Data] Appwrite TablesDB service not available');
1764
- }
1765
-
1766
- const appwriteResponse = await services.tablesDB.listRows({
1767
- databaseId: appwriteConfig.databaseId,
1768
- tableId: tableId,
1769
- queries: queries
1770
- });
1771
-
1772
- const items = appwriteResponse.rows || [];
1773
- const total = appwriteResponse.total || 0;
1774
- const newCursor = items.length > 0 ? items[items.length - 1].$id : null;
1775
- const hasMore = items.length === limit && total > limit;
1776
-
1777
- return {
1778
- items,
1779
- cursor: newCursor,
1780
- total,
1781
- hasMore
1782
- };
1783
- } else if (bucketId) {
1784
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1785
- if (!services?.storage) {
1786
- throw new Error('[Manifest Data] Appwrite Storage service not available');
1787
- }
1788
-
1789
- const storageResponse = await services.storage.listFiles(bucketId, queries);
1790
- const items = storageResponse.files || [];
1791
- const total = storageResponse.total || 0;
1792
- const newCursor = items.length > 0 ? items[items.length - 1].$id : null;
1793
- const hasMore = items.length === limit && total > (limit * 2); // Rough estimate
1794
-
1795
- return {
1796
- items,
1797
- cursor: newCursor,
1798
- total,
1799
- hasMore
1800
- };
1801
- } else {
1802
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" is not a table or bucket`);
1803
- }
1699
+ const { rows, total, extra } = await fetchPage(dataSourceName, baseQueries, [window.Appwrite.Query.cursorAfter(cursor)], limit);
1700
+ const items = rows.slice(0, limit);
1701
+ return { items, cursor: items.length ? items[items.length - 1].$id : null, total, hasMore: extra };
1804
1702
  }
1805
1703
 
1806
- // Previous page from cursor
1807
- // Returns { items, cursor, total, hasMore }
1704
+ // Previous page before a cursor. hasMore: rows exist before this page. Returns { items, cursor, total, hasMore }
1808
1705
  async function getPrevPage(dataSourceName, cursor, limit, baseQueries = []) {
1809
1706
  if (!cursor) {
1810
1707
  throw new Error('[Manifest Data] Cursor is required for previous page');
1811
1708
  }
1812
-
1813
- const manifest = await window.ManifestDataConfig.ensureManifest();
1814
- if (!manifest?.data) {
1815
- throw new Error('[Manifest Data] Manifest not available');
1816
- }
1817
-
1818
- const dataSource = manifest.data[dataSourceName];
1819
- if (!dataSource) {
1820
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" not found`);
1821
- }
1822
-
1823
- const appwriteConfig = await window.ManifestDataConfig.getAppwriteConfig(dataSource);
1824
- if (!appwriteConfig) {
1825
- throw new Error(`[Manifest Data] Invalid Appwrite configuration for "${dataSourceName}"`);
1826
- }
1827
-
1828
- const tableId = window.ManifestDataConfig.getAppwriteTableId(dataSource);
1829
- const bucketId = window.ManifestDataConfig.getAppwriteBucketId(dataSource);
1830
-
1831
- // Build queries with cursorBefore and limit
1832
- const queries = [
1833
- ...baseQueries,
1834
- window.Appwrite.Query.cursorBefore(cursor),
1835
- window.Appwrite.Query.limit(limit)
1836
- ];
1837
-
1838
- let response;
1839
- if (tableId) {
1840
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1841
- if (!services?.tablesDB) {
1842
- throw new Error('[Manifest Data] Appwrite TablesDB service not available');
1843
- }
1844
-
1845
- const appwriteResponse = await services.tablesDB.listRows({
1846
- databaseId: appwriteConfig.databaseId,
1847
- tableId: tableId,
1848
- queries: queries
1849
- });
1850
-
1851
- const items = appwriteResponse.rows || [];
1852
- const total = appwriteResponse.total || 0;
1853
- const newCursor = items.length > 0 ? items[0].$id : null; // First item's ID for going back further
1854
- const hasMore = true; // Can't easily determine if there's a previous page
1855
-
1856
- return {
1857
- items,
1858
- cursor: newCursor,
1859
- total,
1860
- hasMore
1861
- };
1862
- } else if (bucketId) {
1863
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1864
- if (!services?.storage) {
1865
- throw new Error('[Manifest Data] Appwrite Storage service not available');
1866
- }
1867
-
1868
- const storageResponse = await services.storage.listFiles(bucketId, queries);
1869
- const items = storageResponse.files || [];
1870
- const total = storageResponse.total || 0;
1871
- const newCursor = items.length > 0 ? items[0].$id : null;
1872
- const hasMore = true;
1873
-
1874
- return {
1875
- items,
1876
- cursor: newCursor,
1877
- total,
1878
- hasMore
1879
- };
1880
- } else {
1881
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" is not a table or bucket`);
1882
- }
1709
+ const { rows, total, extra } = await fetchPage(dataSourceName, baseQueries, [window.Appwrite.Query.cursorBefore(cursor)], limit);
1710
+ // cursorBefore returns the rows nearest the cursor last; the extra row is the first one
1711
+ const items = extra ? rows.slice(rows.length - limit) : rows;
1712
+ return { items, cursor: items.length ? items[0].$id : null, total, hasMore: extra };
1883
1713
  }
1884
1714
 
1885
- // Specific page (1-based), offset-based
1886
- // Returns { items, page, total, totalPages, hasMore }
1715
+ // Specific page (1-based), offset-based. Returns { items, page, total, totalPages, hasMore }
1887
1716
  async function getPage(dataSourceName, pageNumber, limit, baseQueries = []) {
1888
- if (pageNumber < 1) {
1717
+ if (!pageNumber || pageNumber < 1) {
1889
1718
  throw new Error('[Manifest Data] Page number must be >= 1');
1890
1719
  }
1720
+ const offset = (pageNumber - 1) * limit;
1721
+ const { rows, total, extra } = await fetchPage(dataSourceName, baseQueries, [window.Appwrite.Query.offset(offset)], limit);
1722
+ const items = rows.slice(0, limit);
1723
+ return { items, page: pageNumber, total, totalPages: Math.ceil(total / limit), hasMore: extra };
1724
+ }
1891
1725
 
1892
- const manifest = await window.ManifestDataConfig.ensureManifest();
1893
- if (!manifest?.data) {
1894
- throw new Error('[Manifest Data] Manifest not available');
1895
- }
1896
-
1897
- const dataSource = manifest.data[dataSourceName];
1898
- if (!dataSource) {
1899
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" not found`);
1900
- }
1726
+ window.ManifestDataPagination = {
1727
+ getFirstPage,
1728
+ getNextPage,
1729
+ getPrevPage,
1730
+ getPage
1731
+ };
1901
1732
 
1902
- const appwriteConfig = await window.ManifestDataConfig.getAppwriteConfig(dataSource);
1903
- if (!appwriteConfig) {
1904
- throw new Error(`[Manifest Data] Invalid Appwrite configuration for "${dataSourceName}"`);
1905
- }
1906
1733
 
1907
- const tableId = window.ManifestDataConfig.getAppwriteTableId(dataSource);
1908
- const bucketId = window.ManifestDataConfig.getAppwriteBucketId(dataSource);
1734
+ /* Manifest Data Sources - Contract checks */
1909
1735
 
1910
- const offset = (pageNumber - 1) * limit;
1736
+ // Rows of Appwrite sources whose table the contract declares throw when app code
1737
+ // reads a column that is undeclared and never seen on a row. Dev hosts by default.
1738
+ (function () {
1739
+ const proxies = new WeakMap();
1740
+ const targets = new WeakMap();
1741
+ const warned = new Set();
1742
+ // Read by the framework on every row; storage columns are added per source
1743
+ const ALLOWED = new Set(['then', 'catch', 'finally', 'toJSON', 'length', 'contentType', 'fileIds']);
1744
+ let enabled;
1745
+ let tables = null;
1746
+ const tableBySource = new Map();
1747
+ const seenBySource = new Map();
1748
+ let appwriteSources = null;
1749
+
1750
+ function devHost(h) {
1751
+ return h === 'localhost' || h === '127.0.0.1' || h === '[::1]' || h === '::1' || h === '0.0.0.0' ||
1752
+ h.endsWith('.localhost') || h.endsWith('.local') || h.endsWith('.test');
1753
+ }
1754
+
1755
+ function hostMatches(pattern, host) {
1756
+ const re = new RegExp('^' + String(pattern).toLowerCase().replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^.]*') + '$');
1757
+ return re.test(host);
1758
+ }
1759
+
1760
+ function config(manifest) {
1761
+ const c = manifest && manifest.contract;
1762
+ if (typeof c === 'string') return { module: c, checks: 'dev' };
1763
+ if (c && typeof c === 'object' && typeof c.module === 'string') return { module: c.module, checks: c.checks === undefined ? 'dev' : c.checks };
1764
+ return null;
1765
+ }
1911
1766
 
1912
- // Build queries with offset and limit
1913
- const queries = [
1914
- ...baseQueries,
1915
- window.Appwrite.Query.offset(offset),
1916
- window.Appwrite.Query.limit(limit)
1917
- ];
1767
+ function decide(manifest) {
1768
+ const cfg = config(manifest);
1769
+ if (!cfg || cfg.checks === false) return false;
1770
+ const host = String(location.hostname || '').toLowerCase();
1771
+ if (cfg.checks === true) return cfg;
1772
+ if (devHost(host)) return cfg;
1773
+ return Array.isArray(cfg.checks) && cfg.checks.some(p => hostMatches(p, host)) ? cfg : false;
1774
+ }
1775
+
1776
+ function load(manifest, cfg) {
1777
+ import(new URL(cfg.module, location.origin + '/').href).then(mod => {
1778
+ tables = mod && mod.tables && typeof mod.tables === 'object' ? mod.tables : {};
1779
+ const defaultDb = manifest.appwrite && (manifest.appwrite.databaseId || manifest.appwrite.database);
1780
+ for (const [source, src] of Object.entries(manifest.data || {})) {
1781
+ if (!src || typeof src.appwriteTableId !== 'string') continue;
1782
+ const db = src.appwriteDatabaseId || defaultDb;
1783
+ const hit = Object.entries(tables).find(([key, t]) => t && (t.id || key) === src.appwriteTableId && (!t.database || !db || t.database === db));
1784
+ if (!hit || !Array.isArray(hit[1].columns)) continue;
1785
+ const columns = new Set(hit[1].columns);
1786
+ for (const c of Object.values(src.storage || {})) columns.add(typeof c === 'string' ? c : (c && c.column) || 'fileIds');
1787
+ tableBySource.set(source, { key: hit[0], columns });
1788
+ }
1789
+ }).catch(e => {
1790
+ console.warn(`[Manifest] contract ${cfg.module} failed to load; column checks are off`, e);
1791
+ tables = {};
1792
+ });
1793
+ }
1918
1794
 
1919
- let response;
1920
- let total;
1921
- if (tableId) {
1922
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1923
- if (!services?.tablesDB) {
1924
- throw new Error('[Manifest Data] Appwrite TablesDB service not available');
1795
+ function isEnabled() {
1796
+ if (enabled !== undefined) return enabled;
1797
+ const manifest = window.__manifestLoaded;
1798
+ if (!manifest) return false;
1799
+ const cfg = decide(manifest);
1800
+ enabled = !!cfg;
1801
+ if (cfg) {
1802
+ appwriteSources = new Set(Object.entries(manifest.data || {}).filter(([, s]) => s && typeof s.appwriteTableId === 'string').map(([k]) => k));
1803
+ load(manifest, cfg);
1925
1804
  }
1805
+ return enabled;
1806
+ }
1926
1807
 
1927
- const appwriteResponse = await services.tablesDB.listRows({
1928
- databaseId: appwriteConfig.databaseId,
1929
- tableId: tableId,
1930
- queries: queries
1931
- });
1932
-
1933
- const items = appwriteResponse.rows || [];
1934
- total = appwriteResponse.total || 0;
1935
- const totalPages = Math.ceil(total / limit);
1936
- const hasMore = pageNumber < totalPages;
1808
+ const known = (key, table) => key[0] === '$' || key[0] === '_' || ALLOWED.has(key) || table.columns.has(key);
1937
1809
 
1938
- return {
1939
- items,
1940
- page: pageNumber,
1941
- total,
1942
- totalPages,
1943
- hasMore
1944
- };
1945
- } else if (bucketId) {
1946
- const services = await window.ManifestDataAppwrite._getAppwriteDataServices();
1947
- if (!services?.storage) {
1948
- throw new Error('[Manifest Data] Appwrite Storage service not available');
1810
+ function drift(source, table, row) {
1811
+ for (const key of Object.keys(row)) {
1812
+ if (known(key, table) || warned.has(source + '\0' + key)) continue;
1813
+ warned.add(source + '\0' + key);
1814
+ console.warn(`[Manifest] data source "${source}" has column "${key}", which the contract does not declare for "${table.key}"`);
1949
1815
  }
1816
+ }
1950
1817
 
1951
- const storageResponse = await services.storage.listFiles(bucketId, queries);
1952
- const items = storageResponse.files || [];
1953
- total = storageResponse.total || 0;
1954
- const totalPages = Math.ceil(total / limit);
1955
- const hasMore = pageNumber < totalPages;
1956
-
1957
- return {
1958
- items,
1959
- page: pageNumber,
1960
- total,
1961
- totalPages,
1962
- hasMore
1963
- };
1964
- } else {
1965
- throw new Error(`[Manifest Data] Data source "${dataSourceName}" is not a table or bucket`);
1818
+ function guard(source, row) {
1819
+ if (!row || typeof row !== 'object' || Array.isArray(row) || !isEnabled() || !appwriteSources.has(source)) return row;
1820
+ const existing = proxies.get(row);
1821
+ if (existing) return existing;
1822
+ let seen = seenBySource.get(source);
1823
+ if (!seen) seenBySource.set(source, seen = new Set());
1824
+ for (const key of Object.keys(row)) seen.add(key);
1825
+ let checked = false;
1826
+ const proxy = new Proxy(row, {
1827
+ // App reads arrive through Alpine's reactive proxy; its own old-value reads use this one
1828
+ get(target, key, receiver) {
1829
+ if (typeof key === 'string' && tables && receiver !== proxy) {
1830
+ const table = tableBySource.get(source);
1831
+ if (table) {
1832
+ if (!checked) { checked = true; drift(source, table, target); }
1833
+ if (!known(key, table) && !(key in target) && !seen.has(key)) {
1834
+ throw new Error(`[Manifest] "${key}" is not a column of "${table.key}" in the contract (data source "${source}")`);
1835
+ }
1836
+ }
1837
+ }
1838
+ return Reflect.get(target, key, receiver);
1839
+ }
1840
+ });
1841
+ proxies.set(row, proxy);
1842
+ proxies.set(proxy, proxy);
1843
+ targets.set(proxy, row);
1844
+ return proxy;
1966
1845
  }
1967
- }
1968
1846
 
1969
- // Export functions
1970
- window.ManifestDataPagination = {
1971
- getFirstPage,
1972
- getNextPage,
1973
- getPrevPage,
1974
- getPage
1975
- };
1847
+ // Framework reads go through the unchecked row
1848
+ const raw = (obj) => (obj && targets.get(obj)) || obj;
1849
+
1850
+ window.ManifestDataContract = { guard, raw };
1851
+ })();
1976
1852
 
1977
1853
 
1978
1854
  })();
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.273';
5
+ const MANIFEST_BUILD_VERSION = '0.5.275';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -397,7 +397,7 @@ function mergeRowFields(target, fields, dataSourceName) {
397
397
  if (key === '$files') continue;
398
398
  const value = fields[key];
399
399
  if (sameScalarOrList(raw[key], value)) continue;
400
- target[key] = createReactiveReferences(value, dataSourceName);
400
+ target[key] = createReactiveReferences(value, dataSourceName, true);
401
401
  }
402
402
  }
403
403
 
@@ -432,9 +432,12 @@ function sourceGeneration(dataSourceName) {
432
432
  // next read hydrates/fetches for the new scope; older loads are discarded
433
433
  function resetSource(dataSourceName) {
434
434
  sourceGen.set(dataSourceName, sourceGeneration(dataSourceName) + 1);
435
+ latestRead.delete(dataSourceName);
436
+ window.ManifestDataMain?.forgetLiveSource?.(dataSourceName);
435
437
  const prefix = `${dataSourceName}:`;
436
438
  for (const key of [...dataSourceCache.keys()]) if (key.startsWith(prefix)) dataSourceCache.delete(key);
437
439
  for (const key of [...loadingPromises.keys()]) if (key.startsWith(prefix)) loadingPromises.delete(key);
440
+ for (const key of [...loadReadTokens.keys()]) if (key.startsWith(prefix)) loadReadTokens.delete(key);
438
441
  freshness.delete(dataSourceName);
439
442
  pendingLocal.delete(dataSourceName);
440
443
  for (let i = pendingLandings.length - 1; i >= 0; i--) {
@@ -468,6 +471,15 @@ function setSourceState(dataSourceName, patch) {
468
471
  return next;
469
472
  }
470
473
 
474
+ // Last-issued read per source: a slower earlier read ($query or load) never lands over a later one
475
+ const latestRead = new Map();
476
+ let readSeq = 0;
477
+ const loadReadTokens = new Map(); // source:locale → token of the load in flight under that key
478
+ function beginRead(dataSourceName) { const token = ++readSeq; latestRead.set(dataSourceName, token); return token; }
479
+ function adoptRead(dataSourceName, token) { if (token !== undefined) latestRead.set(dataSourceName, token); }
480
+ // A reset clears the record: generation stamps already drop the older reads
481
+ function isLatestRead(dataSourceName, token) { return token === undefined || !latestRead.has(dataSourceName) || latestRead.get(dataSourceName) === token; }
482
+
471
483
  // Authoritative in-flight map: one promise per key, cleared on settle
472
484
  function runDeduped(key, factory) {
473
485
  if (loadingPromises.has(key)) return loadingPromises.get(key);
@@ -492,16 +504,74 @@ function ensureSourceArray(dataSourceName) {
492
504
  return store[dataSourceName];
493
505
  }
494
506
 
507
+ // Sort spec [[attr, 1|-1], ...] → comparator; nulls sort first ascending,
508
+ // strings case-insensitively (Appwrite's collation)
509
+ function compareBy(order) {
510
+ const norm = v => (typeof v === 'string' ? v.toLowerCase() : v);
511
+ return (a, b) => {
512
+ for (const [key, dir] of order) {
513
+ const x = norm(a?.[key]), y = norm(b?.[key]);
514
+ if (x === y) continue;
515
+ if (x === null || x === undefined) return -dir;
516
+ if (y === null || y === undefined) return dir;
517
+ return (x < y ? -1 : 1) * dir;
518
+ }
519
+ return 0;
520
+ };
521
+ }
522
+
523
+ function sortedIndex(arr, row, cmp) {
524
+ let lo = 0, hi = arr.length;
525
+ while (lo < hi) {
526
+ const mid = (lo + hi) >> 1;
527
+ if (cmp(arr[mid], row) <= 0) lo = mid + 1; else hi = mid;
528
+ }
529
+ return lo;
530
+ }
531
+
532
+ // Ordered append (realtime on a sorted source): new rows insert at their sort
533
+ // position; merged rows whose sort key moved are re-positioned. Structure is
534
+ // rebuilt on a raw copy and assigned once (one reactive write, not per splice)
535
+ function upsertOrdered(store, dataSourceName, rows, order) {
536
+ const cmp = compareBy(order);
537
+ const cur = store[dataSourceName];
538
+ const curRaw = rawOf(store)[dataSourceName];
539
+ let next = null;
540
+ for (const row of rows) {
541
+ const list = next || curRaw;
542
+ const id = rowKey(row);
543
+ const i = id !== null ? list.findIndex(r => rowKey(r) === id) : -1;
544
+ if (i !== -1) {
545
+ // Only a changed sort value moves a held row; the server's placement otherwise stands
546
+ const keyChanged = order.some(([key]) => Object.prototype.hasOwnProperty.call(row, key) && !sameScalarOrList(list[i][key], row[key]));
547
+ mergeRowFields(next ? (Alpine.reactive ? Alpine.reactive(list[i]) : list[i]) : cur[i], row, dataSourceName);
548
+ const misplaced = keyChanged && ((i > 0 && cmp(list[i - 1], list[i]) > 0) || (i < list.length - 1 && cmp(list[i], list[i + 1]) > 0));
549
+ if (!misplaced) continue;
550
+ if (!next) next = curRaw.slice();
551
+ const [moved] = next.splice(i, 1);
552
+ next.splice(sortedIndex(next, moved, cmp), 0, moved);
553
+ continue;
554
+ }
555
+ if (!next) next = curRaw.slice();
556
+ next.splice(sortedIndex(next, row, cmp), 0, rawOf(createReactiveReferences(row, dataSourceName)));
557
+ }
558
+ if (next) store[dataSourceName] = next;
559
+ }
560
+
495
561
  // Identity-preserving upsert by $id: existing rows are merged in place, new
496
562
  // rows created; append keeps the array, replace swaps it only when membership
497
563
  // or order changed
498
- function upsertRows(store, dataSourceName, rows, mode) {
564
+ function upsertRows(store, dataSourceName, rows, mode, order) {
499
565
  const raw = rawOf(store);
500
566
  const curRaw = Array.isArray(raw[dataSourceName]) ? raw[dataSourceName] : null;
501
567
  if (!curRaw) {
502
568
  store[dataSourceName] = rows.map(row => createReactiveReferences(row, dataSourceName));
503
569
  return;
504
570
  }
571
+ if (mode === 'append' && Array.isArray(order) && order.length) {
572
+ upsertOrdered(store, dataSourceName, rows, order);
573
+ return;
574
+ }
505
575
  const cur = store[dataSourceName];
506
576
  const index = new Map();
507
577
  curRaw.forEach((row, i) => { const id = rowKey(row); if (id !== null) index.set(id, i); });
@@ -557,7 +627,7 @@ function writeSource(dataSourceName, data, options = {}) {
557
627
  rawDataStore.set(dataSourceName, data);
558
628
  }
559
629
  } else if (Array.isArray(data)) {
560
- upsertRows(store, dataSourceName, data, mode);
630
+ upsertRows(store, dataSourceName, data, mode, options.order);
561
631
  const arr = rawOf(store)[dataSourceName];
562
632
  rawDataStore.set(dataSourceName, arr);
563
633
  attachMethods(arr, dataSourceName);
@@ -729,24 +799,25 @@ function getRawData(dataSourceName) {
729
799
  }
730
800
 
731
801
  // New object/array references (incl. nested arrays like fileIds) so Alpine tracks changes
732
- function createReactiveReferences(data, dataSourceName = null) {
802
+ function createReactiveReferences(data, dataSourceName = null, nested = false) {
733
803
  if (data === null || data === undefined) {
734
804
  return data;
735
805
  }
736
806
 
737
807
  if (Array.isArray(data)) {
738
808
  // Create new array with new references for each item
739
- return data.map(item => createReactiveReferences(item, dataSourceName));
809
+ return data.map(item => createReactiveReferences(item, dataSourceName, nested));
740
810
  }
741
811
 
742
812
  if (typeof data === 'object') {
743
813
  // Object.keys, not for…in + .hasOwnProperty (see deepSeal). Hot path for
744
814
  // every Appwrite mutation result and realtime event.
745
815
  const newObj = {};
816
+ let self = newObj;
746
817
  for (const key of Object.keys(data)) {
747
818
  const value = data[key];
748
819
  // Recursively create new references for nested objects/arrays
749
- newObj[key] = createReactiveReferences(value, dataSourceName);
820
+ newObj[key] = createReactiveReferences(value, dataSourceName, true);
750
821
  }
751
822
 
752
823
  // Detect file objects (have mimeType or sizeOriginal)
@@ -785,7 +856,7 @@ function createReactiveReferences(data, dataSourceName = null) {
785
856
  data.$id,
786
857
  bucketName,
787
858
  columnName,
788
- { owner: newObj }
859
+ { owner: self }
789
860
  );
790
861
 
791
862
  // Assign computed array to entry
@@ -972,7 +1043,11 @@ function createReactiveReferences(data, dataSourceName = null) {
972
1043
  });
973
1044
  }
974
1045
 
975
- return newObj;
1046
+ // Contract checks wrap top-level Appwrite rows (dev and opted-in hosts)
1047
+ if (!nested && isDatabaseEntry && dataSourceName && window.ManifestDataContract) {
1048
+ self = window.ManifestDataContract.guard(dataSourceName, newObj);
1049
+ }
1050
+ return self;
976
1051
  }
977
1052
 
978
1053
  // Primitives can be returned as-is
@@ -1523,6 +1598,10 @@ window.ManifestDataStore = {
1523
1598
  // Stale-first + dedupe (§11.1)
1524
1599
  setSourceState,
1525
1600
  runDeduped,
1601
+ beginRead,
1602
+ adoptRead,
1603
+ loadReadTokens,
1604
+ isLatestRead,
1526
1605
  sourceFreshness,
1527
1606
  // Persistence (§12.2)
1528
1607
  sourceGeneration,
@@ -2865,7 +2944,8 @@ window.ManifestDataLoaders = {
2865
2944
  // Load from API endpoint (read-only)
2866
2945
  async function loadFromAPI(dataSource) {
2867
2946
  try {
2868
- const url = new URL(window.ManifestDataConfig.interpolateEnvVars(dataSource.url));
2947
+ // Relative urls resolve against the page
2948
+ const url = new URL(window.ManifestDataConfig.interpolateEnvVars(dataSource.url), window.location.href);
2869
2949
 
2870
2950
  // Add query parameters
2871
2951
  if (dataSource.params) {
@@ -3176,7 +3256,15 @@ async function executeMutation(mutationConfig) {
3176
3256
  if (type === 'create') {
3177
3257
  // Ack: swap the temp row for the server row in place (new $id → new $files binding)
3178
3258
  const { store, arr, index } = findRawIndex(dataSourceName, optimisticData.$id);
3179
- if (store && arr) {
3259
+ // Realtime echo of this create may have landed first under the real $id
3260
+ const held = (arr && result.$id !== optimisticData.$id) ? arr.findIndex(r => r && r.$id === result.$id) : -1;
3261
+ if (store && arr && held !== -1) {
3262
+ const ds = window.ManifestDataStore;
3263
+ ds.mergeRowFields(store[dataSourceName][held], result, dataSourceName);
3264
+ if (index !== -1) store[dataSourceName].splice(index, 1);
3265
+ ds.touchSource(dataSourceName);
3266
+ clearSourceCaches(dataSourceName);
3267
+ } else if (store && arr) {
3180
3268
  if (index !== -1) {
3181
3269
  const ds = window.ManifestDataStore;
3182
3270
  store[dataSourceName].splice(index, 1, ds.createReactiveReferences(result, dataSourceName));
@@ -4602,52 +4690,9 @@ function attachArrayMethods(array, dataSourceName, reloadDataSource) {
4602
4690
  });
4603
4691
  }
4604
4692
 
4605
- // Attach pagination methods
4606
- const createPaginationMethod = (methodName) => {
4607
- return async function (...args) {
4608
- const manifest = await window.ManifestDataConfig.ensureManifest();
4609
- if (!manifest?.data) {
4610
- throw new Error('[Manifest Data] Manifest not available');
4611
- }
4612
- const dataSource = manifest.data[dataSourceName];
4613
- if (!dataSource || !window.ManifestDataConfig.isAppwriteCollection(dataSource)) {
4614
- throw new Error(`[Manifest Data] Pagination is only supported for Appwrite data sources`);
4615
- }
4616
- const scope = window.ManifestDataConfig.getScope(dataSource);
4617
- const scopeColumns = window.ManifestDataConfig.getScopeColumns(dataSource);
4618
- const queriesConfig = window.ManifestDataConfig.getQueries(dataSource);
4619
- const baseQueries = queriesConfig
4620
- ? await window.ManifestDataQueries.buildAppwriteQueries(queriesConfig.default || queriesConfig, scope, scopeColumns)
4621
- : await window.ManifestDataQueries.buildAppwriteQueries([], scope, scopeColumns);
4622
-
4623
- if (baseQueries === null) {
4624
- throw new Error(`[Manifest Data] "${dataSourceName}" pagination: auth not ready yet — retry after auth settles`);
4625
- }
4626
-
4627
- if (methodName === '$first') {
4628
- const limit = args[0] || 10;
4629
- return await window.ManifestDataPagination.getFirstPage(dataSourceName, limit, baseQueries);
4630
- } else if (methodName === '$next') {
4631
- const [cursor, limit = 10] = args;
4632
- if (!cursor) {
4633
- throw new Error('[Manifest Data] Cursor is required for $next');
4634
- }
4635
- return await window.ManifestDataPagination.getNextPage(dataSourceName, cursor, limit, baseQueries);
4636
- } else if (methodName === '$prev') {
4637
- const [cursor, limit = 10] = args;
4638
- if (!cursor) {
4639
- throw new Error('[Manifest Data] Cursor is required for $prev');
4640
- }
4641
- return await window.ManifestDataPagination.getPrevPage(dataSourceName, cursor, limit, baseQueries);
4642
- } else if (methodName === '$page') {
4643
- const [pageNumber, limit = 10] = args;
4644
- if (!pageNumber || pageNumber < 1) {
4645
- throw new Error('[Manifest Data] Page number must be >= 1');
4646
- }
4647
- return await window.ManifestDataPagination.getPage(dataSourceName, pageNumber, limit, baseQueries);
4648
- }
4649
- };
4650
- };
4693
+ // Attach pagination methods (one implementation: magic.pagination)
4694
+ const createPaginationMethod = (methodName) => (...args) =>
4695
+ window.ManifestDataProxiesMagic.createPaginationMethod(methodName, dataSourceName)(...args);
4651
4696
 
4652
4697
  ['$first', '$next', '$prev', '$page'].forEach(methodName => {
4653
4698
  Object.defineProperty(array, methodName, {
@@ -4952,52 +4997,7 @@ function createArrayProxyWithRoute(arrayTarget, dataSourceName = null, reloadDat
4952
4997
 
4953
4998
  // Handle pagination methods
4954
4999
  if (key === '$first' || key === '$next' || key === '$prev' || key === '$page') {
4955
- return async function (...args) {
4956
- const manifest = await window.ManifestDataConfig.ensureManifest();
4957
- if (!manifest?.data) {
4958
- throw new Error('[Manifest Data] Manifest not available');
4959
- }
4960
-
4961
- const dataSource = manifest.data[dataSourceName];
4962
- if (!dataSource || !window.ManifestDataConfig.isAppwriteCollection(dataSource)) {
4963
- throw new Error(`[Manifest Data] Pagination is only supported for Appwrite data sources`);
4964
- }
4965
-
4966
- // Get base queries (from manifest or scope)
4967
- const scope = window.ManifestDataConfig.getScope(dataSource);
4968
- const scopeColumns = window.ManifestDataConfig.getScopeColumns(dataSource);
4969
- const queriesConfig = window.ManifestDataConfig.getQueries(dataSource);
4970
- const baseQueries = queriesConfig
4971
- ? await window.ManifestDataQueries.buildAppwriteQueries(queriesConfig.default || queriesConfig, scope, scopeColumns)
4972
- : await window.ManifestDataQueries.buildAppwriteQueries([], scope, scopeColumns);
4973
-
4974
- if (baseQueries === null) {
4975
- throw new Error(`[Manifest Data] "${dataSourceName}" pagination: auth not ready yet — retry after auth settles`);
4976
- }
4977
-
4978
- if (key === '$first') {
4979
- const limit = args[0] || 10;
4980
- return await window.ManifestDataPagination.getFirstPage(dataSourceName, limit, baseQueries);
4981
- } else if (key === '$next') {
4982
- const [cursor, limit = 10] = args;
4983
- if (!cursor) {
4984
- throw new Error('[Manifest Data] Cursor is required for $next');
4985
- }
4986
- return await window.ManifestDataPagination.getNextPage(dataSourceName, cursor, limit, baseQueries);
4987
- } else if (key === '$prev') {
4988
- const [cursor, limit = 10] = args;
4989
- if (!cursor) {
4990
- throw new Error('[Manifest Data] Cursor is required for $prev');
4991
- }
4992
- return await window.ManifestDataPagination.getPrevPage(dataSourceName, cursor, limit, baseQueries);
4993
- } else if (key === '$page') {
4994
- const [pageNumber, limit = 10] = args;
4995
- if (!pageNumber || pageNumber < 1) {
4996
- throw new Error('[Manifest Data] Page number must be >= 1');
4997
- }
4998
- return await window.ManifestDataPagination.getPage(dataSourceName, pageNumber, limit, baseQueries);
4999
- }
5000
- };
5000
+ return (...args) => window.ManifestDataProxiesMagic.createPaginationMethod(key, dataSourceName)(...args);
5001
5001
  }
5002
5002
 
5003
5003
  // $files handler for tables
@@ -7701,6 +7701,8 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7701
7701
  const handleMethod = async function (method, ...args) {
7702
7702
  const sourceUnloaded = args[3]?.sourceUnloaded === true;
7703
7703
  if (!sourceUnloaded) clearErrorState();
7704
+ // Reads are ordered by call, not by network start: the last-issued one lands
7705
+ const readToken = method === '$query' ? window.ManifestDataStore?.beginRead?.(dataSourceName) : undefined;
7704
7706
 
7705
7707
  try {
7706
7708
  const manifest = await window.ManifestDataConfig?.ensureManifest?.();
@@ -7793,13 +7795,19 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7793
7795
 
7794
7796
  // Support batch updates
7795
7797
  if (Array.isArray(idOrArray)) {
7796
- const updates = idOrArray;
7798
+ // Batch: ids (or rows) sharing `data`, or [{ id, data }] entries
7799
+ const updates = idOrArray.map(update => (update !== null && typeof update === 'object')
7800
+ ? { id: update.$id || update.id || update.rowId, data: update.data || data || update }
7801
+ : { id: update, data });
7802
+ if (updates.some(u => u.id === undefined || u.id === null || u.id === '' || u.data === undefined)) {
7803
+ throw new Error(`[Manifest Data] "${dataSourceName}" $update: batch entries need an id and data`);
7804
+ }
7797
7805
  if (executeMutation) {
7798
7806
  // Use optimistic updates for batch
7799
7807
  const results = await Promise.all(
7800
7808
  updates.map(update => {
7801
- const entryId = update.id || update.$id || update.rowId;
7802
- const updateData = update.data || update;
7809
+ const entryId = update.id;
7810
+ const updateData = update.data;
7803
7811
  return executeMutation({
7804
7812
  type: 'update',
7805
7813
  dataSourceName,
@@ -7829,8 +7837,8 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
7829
7837
  window.ManifestDataAppwrite.updateRow(
7830
7838
  appwriteConfig.databaseId,
7831
7839
  tableId,
7832
- update.id || update.$id || update.rowId,
7833
- update.data || update
7840
+ update.id,
7841
+ update.data
7834
7842
  )
7835
7843
  )
7836
7844
  );
@@ -8077,23 +8085,24 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8077
8085
  }
8078
8086
  // Dedupe key: source + serialized queries (never across different queries)
8079
8087
  const key = `${dataSourceName}:$query:${JSON.stringify(appwriteQueries.map(q => String(q)))}`;
8080
- const { runDeduped, landRows, sourceGeneration } = window.ManifestDataStore || {};
8088
+ const { runDeduped, landRows, sourceGeneration, isLatestRead } = window.ManifestDataStore || {};
8081
8089
  const generation = sourceGeneration?.(dataSourceName);
8082
8090
  const requireAuth = window.ManifestDataMain?.requiresIdentity?.({ ...dataSource, queries: [dataSource.queries, queries] }) ?? true;
8083
- const run = async () => {
8084
- const result = await window.ManifestDataAppwrite.loadTableRows(
8085
- appwriteConfig.databaseId,
8086
- tableId,
8087
- appwriteQueries,
8088
- { requireAuth }
8089
- );
8090
- // Network landing: query result replaces the source (coalesced, identity-preserving)
8091
- if (result !== undefined && landRows) {
8092
- await landRows(dataSourceName, result, { mode: 'replace', fresh: true, generation });
8093
- }
8094
- return result;
8095
- };
8096
- return runDeduped ? await runDeduped(key, run) : await run();
8091
+ const run = () => window.ManifestDataAppwrite.loadTableRows(
8092
+ appwriteConfig.databaseId,
8093
+ tableId,
8094
+ appwriteQueries,
8095
+ { requireAuth }
8096
+ );
8097
+ const result = runDeduped ? await runDeduped(key, run) : await run();
8098
+ // Network landing: query result replaces the source (coalesced, identity-preserving);
8099
+ // only the last-issued read of this source lands
8100
+ const latest = !isLatestRead || isLatestRead(dataSourceName, readToken);
8101
+ if (result !== undefined && landRows && latest && sourceGeneration?.(dataSourceName) === generation) {
8102
+ window.ManifestDataMain?.setActiveQuery?.(dataSourceName, queries || [], Array.isArray(result) ? result.length : 0);
8103
+ await landRows(dataSourceName, result, { mode: 'replace', fresh: true, generation });
8104
+ }
8105
+ return result;
8097
8106
  }
8098
8107
  }
8099
8108
 
@@ -9198,6 +9207,8 @@ window.ManifestDataProxiesMagic.createUploadMethod = createUploadMethod;
9198
9207
  /* Manifest Data Sources - Magic Method Pagination Handlers */
9199
9208
  // $first, $next, $prev, $page
9200
9209
 
9210
+ const PAGE_WINDOW_METHODS = new Set(['limit', 'offset', 'cursorAfter', 'cursorBefore']);
9211
+
9201
9212
  function createPaginationMethod(methodName, dataSourceName) {
9202
9213
  return async function (...args) {
9203
9214
  const manifest = await window.ManifestDataConfig.ensureManifest();
@@ -9213,10 +9224,13 @@ function createPaginationMethod(methodName, dataSourceName) {
9213
9224
  // Base queries from manifest or scope
9214
9225
  const scope = window.ManifestDataConfig.getScope(dataSource);
9215
9226
  const scopeColumns = window.ManifestDataConfig.getScopeColumns(dataSource);
9227
+ // Pages follow the active $query, else the default queries; paging supplies its own window
9216
9228
  const queriesConfig = window.ManifestDataConfig.getQueries(dataSource);
9217
- const baseQueries = queriesConfig
9218
- ? await window.ManifestDataQueries.buildAppwriteQueries(queriesConfig.default || queriesConfig, scope, scopeColumns)
9219
- : await window.ManifestDataQueries.buildAppwriteQueries([], scope, scopeColumns);
9229
+ const defaults = queriesConfig ? (queriesConfig.default || queriesConfig) : [];
9230
+ const active = window.ManifestDataMain?.getActiveQuery?.(dataSourceName);
9231
+ const filters = (Array.isArray(active) ? active : (Array.isArray(defaults) ? defaults : []))
9232
+ .filter(q => !(Array.isArray(q) && PAGE_WINDOW_METHODS.has(q[0])));
9233
+ const baseQueries = await window.ManifestDataQueries.buildAppwriteQueries(filters, scope, scopeColumns);
9220
9234
 
9221
9235
  if (baseQueries === null) {
9222
9236
  throw new Error(`[Manifest Data] "${dataSourceName}" pagination: auth not ready yet — retry after auth settles`);
@@ -11905,6 +11919,56 @@ function configureRealtimeMeta(dataSourceName, config, initialCount, limitQuery)
11905
11919
  window.ManifestDataStore.setSourceState(dataSourceName, { realtimeOverflow: 0 });
11906
11920
  }
11907
11921
 
11922
+ // Active $query per source (manifest-format): realtime matches, orders and
11923
+ // server-filters by it until the next default load replaces the rows
11924
+ const activeQueries = new Map();
11925
+ const realtimeBinds = new Map(); // Map<name, { config, mode, rebind }>
11926
+
11927
+ function realtimeQueriesFor(dataSourceName, defaults) {
11928
+ return activeQueries.has(dataSourceName) ? activeQueries.get(dataSourceName) : defaults;
11929
+ }
11930
+
11931
+ // [['orderDesc', 'n'], ...] → [['n', -1], ...]; orderRandom has no stable position
11932
+ function sortSpec(queries) {
11933
+ if (!Array.isArray(queries)) return null;
11934
+ if (queries.some(q => Array.isArray(q) && q[0] === 'orderRandom')) return null;
11935
+ const order = queries
11936
+ .filter(q => Array.isArray(q) && (q[0] === 'orderAsc' || q[0] === 'orderDesc') && typeof q[1] === 'string')
11937
+ .map(q => [q[1], q[0] === 'orderAsc' ? 1 : -1]);
11938
+ return order.length ? order : null;
11939
+ }
11940
+
11941
+ function rebindLive(dataSourceName) {
11942
+ const bind = realtimeBinds.get(dataSourceName);
11943
+ if (bind?.mode !== 'all') return;
11944
+ Promise.resolve().then(bind.rebind).catch(error => console.error(`[Manifest Data] Realtime rebind failed for "${dataSourceName}":`, error));
11945
+ }
11946
+
11947
+ // $query landing: realtime follows the new filter set (caps reset to its result)
11948
+ function setActiveQuery(dataSourceName, queries, rowCount) {
11949
+ const list = Array.isArray(queries) ? queries : [];
11950
+ activeQueries.set(dataSourceName, list);
11951
+ const bind = realtimeBinds.get(dataSourceName);
11952
+ if (!bind) return;
11953
+ const limitQ = list.find(q => Array.isArray(q) && q[0] === 'limit');
11954
+ configureRealtimeMeta(dataSourceName, bind.config, rowCount || 0, limitQ ? limitQ[1] : null);
11955
+ rebindLive(dataSourceName);
11956
+ }
11957
+
11958
+ // Default rows landed: back to the manifest queries (caps sized to those rows)
11959
+ function clearActiveQuery(dataSourceName, rowCount) {
11960
+ if (!activeQueries.delete(dataSourceName)) return;
11961
+ const bind = realtimeBinds.get(dataSourceName);
11962
+ if (bind) configureRealtimeMeta(dataSourceName, bind.config, rowCount || 0, bind.defaultLimit);
11963
+ rebindLive(dataSourceName);
11964
+ }
11965
+
11966
+ // Source reset (identity/scope) or torn down: nothing of the old binding carries over
11967
+ function forgetLiveSource(dataSourceName) {
11968
+ activeQueries.delete(dataSourceName);
11969
+ realtimeBinds.delete(dataSourceName);
11970
+ }
11971
+
11908
11972
  // Events not applied since the last load — $x.<source>.$realtimeOverflow
11909
11973
  function bumpRealtimeOverflow(dataSourceName) {
11910
11974
  const state = Alpine.store('data')?.[`_${dataSourceName}_state`];
@@ -11913,12 +11977,12 @@ function bumpRealtimeOverflow(dataSourceName) {
11913
11977
 
11914
11978
  // Append under the cap. 'window' evicts the oldest realtime-appended row first
11915
11979
  // (never an initially-loaded row); otherwise the event counts in $realtimeOverflow.
11916
- function realtimeAppend(dataSourceName, row, currentRows, generation) {
11980
+ function realtimeAppend(dataSourceName, row, currentRows, generation, order) {
11917
11981
  const { landRows, landRemove } = window.ManifestDataStore;
11918
11982
  const meta = realtimeMeta.get(dataSourceName);
11919
11983
  if (!meta || meta.maxRows === 0) {
11920
11984
  if (meta) meta.appended.push(row.$id);
11921
- landRows(dataSourceName, [row], { mode: 'append', generation });
11985
+ landRows(dataSourceName, [row], { mode: 'append', generation, order });
11922
11986
  return;
11923
11987
  }
11924
11988
  const len = currentRows.length + meta.pending;
@@ -11943,7 +12007,7 @@ function realtimeAppend(dataSourceName, row, currentRows, generation) {
11943
12007
  }
11944
12008
  meta.pending++;
11945
12009
  meta.appended.push(row.$id);
11946
- landRows(dataSourceName, [row], { mode: 'append', generation })
12010
+ landRows(dataSourceName, [row], { mode: 'append', generation, order })
11947
12011
  .then(() => { meta.pending = Math.max(0, meta.pending - 1); });
11948
12012
  }
11949
12013
 
@@ -11992,6 +12056,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
11992
12056
 
11993
12057
  // Network landings: coalesced per frame (landRows upserts by $id), never the sync mutation path
11994
12058
  const { landRows, landRemove } = window.ManifestDataStore;
12059
+ const order = sortSpec(queriesConfig);
11995
12060
 
11996
12061
  if (eventType === 'create') {
11997
12062
  const row = payload?.$id ? payload : (payload?.row || payload);
@@ -12001,9 +12066,9 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
12001
12066
  if (rowMatchesScope && eventMatchesQueries(row, queriesConfig)) {
12002
12067
  if (currentRows.find(r => r.$id === row.$id)) {
12003
12068
  // Echo of a row already held (optimistic create): merge, never counts as growth
12004
- landRows(dataSourceName, [row], { mode: 'append', generation });
12069
+ landRows(dataSourceName, [row], { mode: 'append', generation, order });
12005
12070
  } else {
12006
- realtimeAppend(dataSourceName, row, currentRows, generation);
12071
+ realtimeAppend(dataSourceName, row, currentRows, generation, order);
12007
12072
  }
12008
12073
  }
12009
12074
  } else {
@@ -12024,7 +12089,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
12024
12089
  }
12025
12090
  if (!existingRow) {
12026
12091
  // Row not in list, but matches now - add it (capped like a create)
12027
- realtimeAppend(dataSourceName, row, currentRows, generation);
12092
+ realtimeAppend(dataSourceName, row, currentRows, generation, order);
12028
12093
  return;
12029
12094
  }
12030
12095
 
@@ -12084,7 +12149,7 @@ async function handleTableRealtimeEvent(dataSourceName, databaseId, tableId, sco
12084
12149
  const fileIdsChanged = isProjectWithFiles &&
12085
12150
  JSON.stringify(existingFileIds) !== JSON.stringify(incomingFileIds);
12086
12151
 
12087
- landRows(dataSourceName, [row], { mode: 'append', generation });
12152
+ landRows(dataSourceName, [row], { mode: 'append', generation, order });
12088
12153
 
12089
12154
  // Emit custom event for project file updates so UI can refresh
12090
12155
  if (fileIdsChanged && dataSourceName === 'projects') {
@@ -12264,11 +12329,17 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12264
12329
  return cachedData;
12265
12330
  }
12266
12331
 
12267
- // In flight for this source+locale: share it (every entry path lands here)
12332
+ // In flight for this source+locale: share it (every entry path lands here);
12333
+ // joining makes it the latest read again
12268
12334
  if (loadingPromises.has(cacheKey)) {
12335
+ window.ManifestDataStore.adoptRead?.(dataSourceName, window.ManifestDataStore.loadReadTokens?.get(cacheKey));
12269
12336
  return loadingPromises.get(cacheKey);
12270
12337
  }
12271
12338
 
12339
+ // Reads are ordered by call: a $query issued after this load wins over it
12340
+ const readToken = window.ManifestDataStore.beginRead?.(dataSourceName);
12341
+ window.ManifestDataStore.loadReadTokens?.set(cacheKey, readToken);
12342
+
12272
12343
  // Loading state: rows already landed stay live; first-ever load clears
12273
12344
  const keepRows = hasLiveRows(dataSourceName);
12274
12345
  if (!isInitializing) {
@@ -12403,22 +12474,29 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12403
12474
  if (dataSource.realtime !== false && window.ManifestDataRealtime && window.ManifestDataRealtime.subscribeToTable) {
12404
12475
  const rtQueries = Array.isArray(queriesConfig?.default || queriesConfig) ? (queriesConfig.default || queriesConfig) : [];
12405
12476
  const limitQ = rtQueries.find(q => Array.isArray(q) && q[0] === 'limit');
12406
- configureRealtimeMeta(dataSourceName, dataSource.realtime, Array.isArray(data) ? data.length : 0, limitQ ? limitQ[1] : null);
12477
+ if (!activeQueries.has(dataSourceName) || !realtimeMeta.has(dataSourceName)) {
12478
+ configureRealtimeMeta(dataSourceName, dataSource.realtime, Array.isArray(data) ? data.length : 0, limitQ ? limitQ[1] : null);
12479
+ }
12407
12480
  // Events server-filtered where the SDK supports realtime queries; the client
12408
12481
  // matcher below still runs. serverFilter: 'scope' (default) | 'all' (alias true) | false
12409
12482
  const rtMode = serverFilterMode(dataSource.realtime);
12410
- const rtFilterSpec = rtMode ? { queriesConfig: rtQueries, scope, scopeColumns, mode: rtMode } : null;
12411
- const unsubscribe = await window.ManifestDataRealtime.subscribeToTable(
12483
+ const rtFilterSpec = rtMode ? {
12484
+ get queriesConfig() { return realtimeQueriesFor(dataSourceName, rtQueries); },
12485
+ scope, scopeColumns, mode: rtMode
12486
+ } : null;
12487
+ const subscribe = () => window.ManifestDataRealtime.subscribeToTable(
12412
12488
  dataSourceName,
12413
12489
  appwriteConfig.databaseId,
12414
12490
  tableId,
12415
12491
  scope,
12416
12492
  async (eventType, payload) => {
12417
- await handleTableRealtimeEvent(dataSourceName, appwriteConfig.databaseId, tableId, scope, scopeColumns, eventType, payload, generation, rtQueries);
12493
+ await handleTableRealtimeEvent(dataSourceName, appwriteConfig.databaseId, tableId, scope, scopeColumns, eventType, payload, generation, realtimeQueriesFor(dataSourceName, rtQueries));
12418
12494
  },
12419
12495
  () => !superseded(),
12420
12496
  rtFilterSpec
12421
12497
  );
12498
+ realtimeBinds.set(dataSourceName, { config: dataSource.realtime, mode: rtMode, rebind: subscribe, defaultLimit: limitQ ? limitQ[1] : null });
12499
+ const unsubscribe = await subscribe();
12422
12500
  if (superseded()) { dropSubscription(dataSourceName, unsubscribe); landed = true; return null; }
12423
12501
  }
12424
12502
  } else if (bucketId) {
@@ -12594,6 +12672,13 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12594
12672
 
12595
12673
  if (superseded()) { landed = true; return null; }
12596
12674
 
12675
+ // A $query issued since this load began: its rows stand, this result never lands
12676
+ if (window.ManifestDataStore.isLatestRead && !window.ManifestDataStore.isLatestRead(dataSourceName, readToken)) {
12677
+ landed = true;
12678
+ if (!isInitializing) setSourceState(dataSourceName, { loading: false });
12679
+ return enhancedData;
12680
+ }
12681
+
12597
12682
  // Cache is always safe — the key carries this load's locale.
12598
12683
  dataSourceCache.set(cacheKey, enhancedData);
12599
12684
 
@@ -12610,6 +12695,7 @@ async function loadDataSource(dataSourceName, locale = 'en', options = {}) {
12610
12695
  // Network landing (coalesced per frame, merges by $id); resolves once visible
12611
12696
  if (!isInitializing && !staleLocale) {
12612
12697
  landed = true;
12698
+ clearActiveQuery(dataSourceName, Array.isArray(enhancedData) ? enhancedData.length : 0);
12613
12699
  await landRows(dataSourceName, enhancedData, { mode: 'replace', loading: false, error: null, ready: true, fresh: true, generation });
12614
12700
  }
12615
12701
 
@@ -12780,12 +12866,38 @@ async function initializeDataSourcesPlugin() {
12780
12866
  }
12781
12867
 
12782
12868
  // Export loadDataSource for use by team change listener
12869
+ // Catch-up: events missed while offline or asleep never arrive, so live sources
12870
+ // refetch (their active $query, else the default load) on reconnect or wake
12871
+ const CATCH_UP_HIDDEN_MS = 60000;
12872
+ let hiddenSince = document.hidden ? Date.now() : null;
12873
+ let catchUpTimer = null;
12874
+ const catchUpLiveSources = () => {
12875
+ clearTimeout(catchUpTimer);
12876
+ catchUpTimer = setTimeout(() => {
12877
+ for (const name of realtimeBinds.keys()) {
12878
+ const active = activeQueries.get(name);
12879
+ const handler = active && window.ManifestDataProxiesAppwrite?.createAppwriteMethodsHandler?.(name, reloadDataSource);
12880
+ Promise.resolve(handler ? handler('$query', active) : reloadDataSource(name)).catch(() => { /* $error carries it */ });
12881
+ }
12882
+ }, 250);
12883
+ };
12884
+ window.addEventListener('online', catchUpLiveSources);
12885
+ document.addEventListener('visibilitychange', () => {
12886
+ if (document.hidden) { hiddenSince = Date.now(); return; }
12887
+ const away = hiddenSince !== null ? Date.now() - hiddenSince : 0;
12888
+ hiddenSince = null;
12889
+ if (away >= CATCH_UP_HIDDEN_MS) catchUpLiveSources();
12890
+ });
12891
+
12783
12892
  window.ManifestDataMain = {
12784
12893
  loadDataSource,
12785
12894
  reloadDataSource,
12786
12895
  _loadDataSource: reloadDataSource, // Internal: every user is a reload-after-mutation
12787
12896
  filterFilesByScope, // Export for use by getFilesForEntry
12788
- requiresIdentity
12897
+ requiresIdentity,
12898
+ setActiveQuery,
12899
+ forgetLiveSource,
12900
+ getActiveQuery: (dataSourceName) => activeQueries.get(dataSourceName) || null
12789
12901
  };
12790
12902
 
12791
12903
  // Listen for manifest:dev-reload (fired by mnfst-run when a data file changes).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest.appwrite.auth.js": "sha384-gWLSeVPqCLMiUZB8MU7GqMg3x2yULFUsp8nTTZrPp/9Dlkix42rBn9yIimvwrHqZ",
3
- "manifest.appwrite.data.js": "sha384-42+RFwO3akEoogPnpUFNOeXgDGztU1Kmk/WAag6yJrdjOFIXRZcEGqy9DToHqssG",
3
+ "manifest.appwrite.data.js": "sha384-DL3DcEUoVXDqLwODzd/CBtnTyBPqeFl6RVx01oBv1lCwgMkNEGQeeKdA9l132m2m",
4
4
  "manifest.appwrite.presences.js": "sha384-LINPhIPTCK+rA8zd+HPUzMSEgH6Sl2vQMEyItT97bnEE2FYGYKqjXX+CWdt6nl9z",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-AFbP7mQnigy7DU1s+YE30yKTeQAKfgNazhbfSNbfd9ZpLgeulZKaPi/fAq3gJHDO",
@@ -11,7 +11,7 @@
11
11
  "manifest.combobox.js": "sha384-/lJo6vaxKjgnBSRs/dzkhK1dzGk3GBw8FISoNJZB70mwAVwu1k5veZyBXjOb4keS",
12
12
  "manifest.components.js": "sha384-1el5WMdgmzu/2ocaYN21OjLi/lfjllvA8aju1J3knYuOizw+XFfJU1+rvGqYW7qk",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-OtzsgGwSGzMkj9of9mCLKC9ZbLjIdDV8Q4nUcWTHJADTbWh69nI2HfEQScK0CUN1",
14
+ "manifest.data.js": "sha384-pOXyFP7tUdQG4c52usKJkWYgGxMN5g2a6dWAKYGAJEHrJL4gZDYRNgOsSq/+jnTm",
15
15
  "manifest.datepicker.js": "sha384-EzlMwvIxlla/0BuyhAEqxN0M8TGcWIWgEIg840V0akKQQK1uY648wPWMkobvtq/o",
16
16
  "manifest.defer.js": "sha384-PRp7+dCZHnnl55rX2xrHutLrQnszHo7o5o9Tq28M/yjLZPuXDzcwQNxCdvbtCYSz",
17
17
  "manifest.device.js": "sha384-YeQaS56L8IILqrQNBSeAOLwIh5DuAOrE60cYLK1+46qJtUrXlAvqGe2j5mbV4Vq7",
@@ -29,7 +29,7 @@
29
29
  "manifest.status.js": "sha384-ofu7nBnVIGZ1U1o2R9d2H3V8aEMPUCVbl5FO3gIToXqi0oa36fTviEdh5UU7Kitw",
30
30
  "manifest.submissions.js": "sha384-v9eS47ytAfTGIKAyVnbd0QWegucVS7yduvigqfsxxoqXmbaEoy0M2y+BiI4xeZBd",
31
31
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
32
- "manifest.sw.js": "sha384-aAbMkl5+e8xG9U16+K2l6XUphDZP/binE6AnswmuNEjKl6A4DsOSzWaqHXSiOgpL",
32
+ "manifest.sw.js": "sha384-hyn8eZtfGN2VxD1+L4AxEyI7CQlPPguEkG5AgjB19A2Ca/W4HhOnTiF3QfoAMnLq",
33
33
  "manifest.tabs.js": "sha384-otU09H7jnawVKQdLAYpMCd3jx00tgyPVr42D2hLIBzN5dG4se505YesavdnMqEi1",
34
34
  "manifest.tailwind.js": "sha384-yuP3LEvWU+QVjxjkZ0YhErtC9RrGmhOmHjjFjhS9obqZbp5/KTIEnSR0CXDvHE0B",
35
35
  "manifest.toasts.js": "sha384-UpOz9fAdGeGUhL0NamBg5iPepEfIy73p9HYdcxfOQC5b7CVsl7Our0skdZzaLNcC",
@@ -137,6 +137,28 @@
137
137
  "type": "string"
138
138
  }
139
139
  },
140
+ "contract": {
141
+ "description": "Shared contract module, a path from the project root (e.g. /shared/contract.js), whose `tables` export declares Appwrite tables and columns. mnfst-test checks data sources against it; on dev hosts (or hosts listed in `checks`) app code reading a column of an Appwrite row that is undeclared and absent from every row of the source throws.",
142
+ "oneOf": [
143
+ { "type": "string" },
144
+ {
145
+ "type": "object",
146
+ "properties": {
147
+ "module": { "type": "string" },
148
+ "checks": {
149
+ "description": "Where column checks run: 'dev' (default, local hosts), true (everywhere), false (nowhere), or host patterns such as '*-staging.example.com' (dev hosts included).",
150
+ "oneOf": [
151
+ { "type": "boolean" },
152
+ { "type": "string", "enum": ["dev"] },
153
+ { "type": "array", "items": { "type": "string" } }
154
+ ]
155
+ }
156
+ },
157
+ "required": ["module"],
158
+ "additionalProperties": false
159
+ }
160
+ ]
161
+ },
140
162
  "data": {
141
163
  "type": "object",
142
164
  "description": "Named data sources made available via $x.<sourceName>. Each value is either a string path/URL or a config object.",
@@ -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.273';
19
+ const BUILD_VERSION = '0.5.275';
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.273",
3
+ "version": "0.5.275",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",
@@ -53,6 +53,7 @@
53
53
  "test:e2e:test-kit": "node tests/e2e/test-kit-journeys.e2e.mjs",
54
54
  "test:e2e:submissions": "node tests/e2e/submissions.e2e.mjs",
55
55
  "test:e2e:modules": "node tests/e2e/modules.e2e.mjs",
56
+ "test:e2e:contract": "node tests/e2e/contract.e2e.mjs && node tests/e2e/contract-checks.e2e.mjs",
56
57
  "lint": "echo 'No linting configured'",
57
58
  "release:docs": "node scripts/release-bump.mjs packages/docs && cd packages/docs && npm publish --auth-type=web",
58
59
  "release:check": "node scripts/release-check.mjs"