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.
- package/lib/manifest.appwrite.data.js +157 -281
- package/lib/manifest.data.js +251 -139
- package/lib/manifest.integrity.json +3 -3
- package/lib/manifest.schema.json +22 -0
- package/lib/manifest.sw.js +1 -1
- package/package.json +2 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.
|
|
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
|
-
//
|
|
1646
|
-
|
|
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
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
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
|
-
|
|
1681
|
-
|
|
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(
|
|
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
|
|
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
|
|
1735
|
-
|
|
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
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
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
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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
|
-
|
|
1908
|
-
const bucketId = window.ManifestDataConfig.getAppwriteBucketId(dataSource);
|
|
1734
|
+
/* Manifest Data Sources - Contract checks */
|
|
1909
1735
|
|
|
1910
|
-
|
|
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
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
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
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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
|
-
//
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
})();
|
package/lib/manifest.data.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
(function () {
|
|
4
4
|
|
|
5
|
-
const MANIFEST_BUILD_VERSION = '0.5.
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
7802
|
-
const updateData = update.data
|
|
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
|
|
7833
|
-
update.data
|
|
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 =
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
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
|
|
9218
|
-
|
|
9219
|
-
|
|
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
|
-
|
|
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 ? {
|
|
12411
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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",
|
package/lib/manifest.schema.json
CHANGED
|
@@ -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.",
|
package/lib/manifest.sw.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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"
|