fauxbase 0.5.8 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +649 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +176 -6
- package/dist/index.d.ts +176 -6
- package/dist/index.js +649 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1588,10 +1588,12 @@ var HttpDriver = class {
|
|
|
1588
1588
|
const params = new URLSearchParams(options.query);
|
|
1589
1589
|
url += `?${params.toString()}`;
|
|
1590
1590
|
}
|
|
1591
|
-
|
|
1591
|
+
const raw = await this._fetch(url, {
|
|
1592
1592
|
method: options?.method ?? (options?.body !== void 0 ? "POST" : "GET"),
|
|
1593
1593
|
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
1594
1594
|
});
|
|
1595
|
+
const parsed = this.preset.response.single(raw);
|
|
1596
|
+
return parsed.data ?? raw;
|
|
1595
1597
|
}
|
|
1596
1598
|
// Seed methods are no-ops for HTTP — backend owns data
|
|
1597
1599
|
seed() {
|
|
@@ -1605,6 +1607,593 @@ var HttpDriver = class {
|
|
|
1605
1607
|
}
|
|
1606
1608
|
};
|
|
1607
1609
|
|
|
1610
|
+
// src/drivers/sync/sync-queue.ts
|
|
1611
|
+
var STORE_NAME = "queue";
|
|
1612
|
+
function generateId() {
|
|
1613
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1614
|
+
return crypto.randomUUID();
|
|
1615
|
+
}
|
|
1616
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1617
|
+
const r = Math.random() * 16 | 0;
|
|
1618
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1619
|
+
return v.toString(16);
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
var SyncQueue = class {
|
|
1623
|
+
db = null;
|
|
1624
|
+
cache = [];
|
|
1625
|
+
_ready;
|
|
1626
|
+
constructor(dbName = "fauxbase-sync-queue") {
|
|
1627
|
+
this._ready = this.open(dbName);
|
|
1628
|
+
}
|
|
1629
|
+
get ready() {
|
|
1630
|
+
return this._ready;
|
|
1631
|
+
}
|
|
1632
|
+
open(dbName) {
|
|
1633
|
+
return new Promise((resolve, reject) => {
|
|
1634
|
+
const req = indexedDB.open(dbName, 1);
|
|
1635
|
+
req.onupgradeneeded = () => {
|
|
1636
|
+
const db = req.result;
|
|
1637
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
1638
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "id" });
|
|
1639
|
+
store.createIndex("resource_entity", ["resource", "entityId"]);
|
|
1640
|
+
store.createIndex("status", "status");
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
req.onsuccess = async () => {
|
|
1644
|
+
this.db = req.result;
|
|
1645
|
+
await this.loadAll();
|
|
1646
|
+
resolve();
|
|
1647
|
+
};
|
|
1648
|
+
req.onerror = () => reject(req.error);
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
async loadAll() {
|
|
1652
|
+
const db = this.db;
|
|
1653
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
1654
|
+
const store = tx.objectStore(STORE_NAME);
|
|
1655
|
+
return new Promise((resolve, reject) => {
|
|
1656
|
+
const req = store.getAll();
|
|
1657
|
+
req.onsuccess = () => {
|
|
1658
|
+
this.cache = req.result.sort((a, b) => a.timestamp - b.timestamp);
|
|
1659
|
+
resolve();
|
|
1660
|
+
};
|
|
1661
|
+
req.onerror = () => reject(req.error);
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
persist(entry) {
|
|
1665
|
+
if (!this.db) return;
|
|
1666
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
1667
|
+
tx.objectStore(STORE_NAME).put(entry);
|
|
1668
|
+
}
|
|
1669
|
+
removeFromDb(id) {
|
|
1670
|
+
if (!this.db) return;
|
|
1671
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
1672
|
+
tx.objectStore(STORE_NAME).delete(id);
|
|
1673
|
+
}
|
|
1674
|
+
async enqueue(entry) {
|
|
1675
|
+
const existing = this.cache.find(
|
|
1676
|
+
(e) => e.resource === entry.resource && e.entityId === entry.entityId && (e.status === "pending" || e.status === "failed")
|
|
1677
|
+
);
|
|
1678
|
+
if (existing) {
|
|
1679
|
+
if (existing.action === "create" && entry.action === "update") {
|
|
1680
|
+
existing.data = { ...existing.data, ...entry.data };
|
|
1681
|
+
this.persist(existing);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (existing.action === "create" && entry.action === "delete") {
|
|
1685
|
+
this.cache = this.cache.filter((e) => e.id !== existing.id);
|
|
1686
|
+
this.removeFromDb(existing.id);
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (existing.action === "update" && entry.action === "update") {
|
|
1690
|
+
existing.data = { ...existing.data, ...entry.data };
|
|
1691
|
+
this.persist(existing);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (existing.action === "update" && entry.action === "delete") {
|
|
1695
|
+
existing.action = "delete";
|
|
1696
|
+
existing.data = null;
|
|
1697
|
+
this.persist(existing);
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
const full = {
|
|
1702
|
+
...entry,
|
|
1703
|
+
id: generateId(),
|
|
1704
|
+
status: "pending",
|
|
1705
|
+
retries: 0
|
|
1706
|
+
};
|
|
1707
|
+
this.cache.push(full);
|
|
1708
|
+
this.persist(full);
|
|
1709
|
+
}
|
|
1710
|
+
getPending() {
|
|
1711
|
+
return this.cache.filter((e) => e.status === "pending" || e.status === "failed").sort((a, b) => a.timestamp - b.timestamp);
|
|
1712
|
+
}
|
|
1713
|
+
getPendingCount() {
|
|
1714
|
+
return this.cache.filter((e) => e.status === "pending" || e.status === "failed").length;
|
|
1715
|
+
}
|
|
1716
|
+
async markSyncing(id) {
|
|
1717
|
+
const entry = this.cache.find((e) => e.id === id);
|
|
1718
|
+
if (entry) {
|
|
1719
|
+
entry.status = "syncing";
|
|
1720
|
+
this.persist(entry);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
async remove(id) {
|
|
1724
|
+
this.cache = this.cache.filter((e) => e.id !== id);
|
|
1725
|
+
this.removeFromDb(id);
|
|
1726
|
+
}
|
|
1727
|
+
async markFailed(id, error) {
|
|
1728
|
+
const entry = this.cache.find((e) => e.id === id);
|
|
1729
|
+
if (entry) {
|
|
1730
|
+
entry.status = "failed";
|
|
1731
|
+
entry.retries++;
|
|
1732
|
+
entry.error = error;
|
|
1733
|
+
this.persist(entry);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
getDeadLetters(maxRetries) {
|
|
1737
|
+
return this.cache.filter((e) => e.status === "failed" && e.retries >= maxRetries);
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
|
|
1741
|
+
// src/drivers/sync/sync-engine.ts
|
|
1742
|
+
var SyncEngine = class {
|
|
1743
|
+
localDriver;
|
|
1744
|
+
httpDriver;
|
|
1745
|
+
queue;
|
|
1746
|
+
config;
|
|
1747
|
+
eventBus = null;
|
|
1748
|
+
online = true;
|
|
1749
|
+
syncing = false;
|
|
1750
|
+
pullTimer = null;
|
|
1751
|
+
pushTimer = null;
|
|
1752
|
+
lastSynced = null;
|
|
1753
|
+
registeredResources = /* @__PURE__ */ new Map();
|
|
1754
|
+
// resource -> endpoint
|
|
1755
|
+
_syncPromise = null;
|
|
1756
|
+
constructor(localDriver, httpDriver, queue, syncConfig) {
|
|
1757
|
+
this.localDriver = localDriver;
|
|
1758
|
+
this.httpDriver = httpDriver;
|
|
1759
|
+
this.queue = queue;
|
|
1760
|
+
this.config = {
|
|
1761
|
+
interval: syncConfig?.interval ?? 3e4,
|
|
1762
|
+
retryDelay: syncConfig?.retryDelay ?? 5e3,
|
|
1763
|
+
maxRetries: syncConfig?.maxRetries ?? 10,
|
|
1764
|
+
conflictStrategy: syncConfig?.conflictStrategy ?? "last-write-wins",
|
|
1765
|
+
resources: syncConfig?.resources ?? null,
|
|
1766
|
+
pingUrl: syncConfig?.pingUrl ?? "/ping"
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
setEventBus(bus) {
|
|
1770
|
+
this.eventBus = bus;
|
|
1771
|
+
}
|
|
1772
|
+
registerResource(resource, endpoint) {
|
|
1773
|
+
this.registeredResources.set(resource, endpoint);
|
|
1774
|
+
}
|
|
1775
|
+
start() {
|
|
1776
|
+
this.setupConnectivityListeners();
|
|
1777
|
+
if (this.config.interval > 0) {
|
|
1778
|
+
this.pullTimer = setInterval(() => {
|
|
1779
|
+
this.sync().catch(() => {
|
|
1780
|
+
});
|
|
1781
|
+
}, this.config.interval);
|
|
1782
|
+
}
|
|
1783
|
+
this.sync().catch(() => {
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
stop() {
|
|
1787
|
+
if (this.pullTimer) {
|
|
1788
|
+
clearInterval(this.pullTimer);
|
|
1789
|
+
this.pullTimer = null;
|
|
1790
|
+
}
|
|
1791
|
+
if (this.pushTimer) {
|
|
1792
|
+
clearTimeout(this.pushTimer);
|
|
1793
|
+
this.pushTimer = null;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
async sync() {
|
|
1797
|
+
if (this._syncPromise) return this._syncPromise;
|
|
1798
|
+
this._syncPromise = this._doSync();
|
|
1799
|
+
try {
|
|
1800
|
+
return await this._syncPromise;
|
|
1801
|
+
} finally {
|
|
1802
|
+
this._syncPromise = null;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
async _doSync() {
|
|
1806
|
+
if (this.syncing) return { pushed: 0, pulled: 0 };
|
|
1807
|
+
this.syncing = true;
|
|
1808
|
+
this.emitSyncState();
|
|
1809
|
+
this.emitEvent("sync:start");
|
|
1810
|
+
let pushed = 0;
|
|
1811
|
+
let pulled = 0;
|
|
1812
|
+
try {
|
|
1813
|
+
this.online = await this.checkOnline();
|
|
1814
|
+
if (!this.online) {
|
|
1815
|
+
this.emitEvent("sync:complete", { pushed: 0, pulled: 0 });
|
|
1816
|
+
return { pushed: 0, pulled: 0 };
|
|
1817
|
+
}
|
|
1818
|
+
pushed = await this.push();
|
|
1819
|
+
pulled = await this.pull();
|
|
1820
|
+
this.lastSynced = Date.now();
|
|
1821
|
+
this.emitEvent("sync:complete", { pushed, pulled });
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
this.emitEvent("sync:error", { error: err.message });
|
|
1824
|
+
} finally {
|
|
1825
|
+
this.syncing = false;
|
|
1826
|
+
this.emitSyncState();
|
|
1827
|
+
}
|
|
1828
|
+
return { pushed, pulled };
|
|
1829
|
+
}
|
|
1830
|
+
// --- Push: replay queued mutations to remote ---
|
|
1831
|
+
async push() {
|
|
1832
|
+
const pending = this.queue.getPending();
|
|
1833
|
+
let pushed = 0;
|
|
1834
|
+
for (const entry of pending) {
|
|
1835
|
+
if (entry.retries >= this.config.maxRetries) continue;
|
|
1836
|
+
if (this.config.resources && !this.config.resources.includes(entry.resource)) continue;
|
|
1837
|
+
await this.queue.markSyncing(entry.id);
|
|
1838
|
+
try {
|
|
1839
|
+
switch (entry.action) {
|
|
1840
|
+
case "create": {
|
|
1841
|
+
const result = await this.httpDriver.create(entry.resource, entry.data);
|
|
1842
|
+
const serverData = result.data;
|
|
1843
|
+
if (serverData && serverData.id) {
|
|
1844
|
+
this.localDriver.getStorageBackend().set(entry.resource, serverData.id, serverData);
|
|
1845
|
+
if (serverData.id !== entry.entityId) {
|
|
1846
|
+
this.localDriver.getStorageBackend().remove(entry.resource, entry.entityId);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
case "update": {
|
|
1852
|
+
const result = await this.httpDriver.update(entry.resource, entry.entityId, entry.data);
|
|
1853
|
+
const serverData = result.data;
|
|
1854
|
+
if (serverData) {
|
|
1855
|
+
this.localDriver.getStorageBackend().set(entry.resource, entry.entityId, serverData);
|
|
1856
|
+
}
|
|
1857
|
+
break;
|
|
1858
|
+
}
|
|
1859
|
+
case "delete": {
|
|
1860
|
+
await this.httpDriver.delete(entry.resource, entry.entityId);
|
|
1861
|
+
break;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
await this.queue.remove(entry.id);
|
|
1865
|
+
pushed++;
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
if (err.status === 409 || err.code === "CONFLICT") {
|
|
1868
|
+
await this.handleConflict(entry);
|
|
1869
|
+
} else {
|
|
1870
|
+
await this.queue.markFailed(entry.id, err.message ?? "Sync failed");
|
|
1871
|
+
this.emitEvent("sync:error", {
|
|
1872
|
+
resource: entry.resource,
|
|
1873
|
+
action: entry.action,
|
|
1874
|
+
error: err.message
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return pushed;
|
|
1880
|
+
}
|
|
1881
|
+
// --- Pull: fetch updated data from remote ---
|
|
1882
|
+
async pull() {
|
|
1883
|
+
let totalPulled = 0;
|
|
1884
|
+
for (const [resource] of this.registeredResources) {
|
|
1885
|
+
if (this.config.resources && !this.config.resources.includes(resource)) continue;
|
|
1886
|
+
try {
|
|
1887
|
+
totalPulled += await this.pullResource(resource);
|
|
1888
|
+
} catch {
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return totalPulled;
|
|
1892
|
+
}
|
|
1893
|
+
async pullResource(resource) {
|
|
1894
|
+
const storage = this.localDriver.getStorageBackend();
|
|
1895
|
+
const lastPullKey = `_sync:lastPull:${resource}`;
|
|
1896
|
+
const lastPull = storage.getMeta(lastPullKey);
|
|
1897
|
+
const filter = {};
|
|
1898
|
+
if (lastPull) {
|
|
1899
|
+
filter.updatedAt__gt = lastPull;
|
|
1900
|
+
}
|
|
1901
|
+
let page = 1;
|
|
1902
|
+
let pulled = 0;
|
|
1903
|
+
let maxUpdatedAt = lastPull ?? "";
|
|
1904
|
+
while (true) {
|
|
1905
|
+
const result = await this.httpDriver.list(resource, {
|
|
1906
|
+
filter: Object.keys(filter).length > 0 ? filter : void 0,
|
|
1907
|
+
size: 200,
|
|
1908
|
+
page,
|
|
1909
|
+
sort: { field: "updatedAt", direction: "asc" }
|
|
1910
|
+
});
|
|
1911
|
+
for (const item of result.items) {
|
|
1912
|
+
const hasPending = this.queue.getPending().some(
|
|
1913
|
+
(e) => e.resource === resource && e.entityId === item.id
|
|
1914
|
+
);
|
|
1915
|
+
if (hasPending && this.config.conflictStrategy !== "server-wins") continue;
|
|
1916
|
+
storage.set(resource, item.id, item);
|
|
1917
|
+
pulled++;
|
|
1918
|
+
if (item.updatedAt && item.updatedAt > maxUpdatedAt) {
|
|
1919
|
+
maxUpdatedAt = item.updatedAt;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
if (result.items.length < 200 || page >= result.meta.totalPages) break;
|
|
1923
|
+
page++;
|
|
1924
|
+
}
|
|
1925
|
+
if (maxUpdatedAt) {
|
|
1926
|
+
storage.setMeta(lastPullKey, maxUpdatedAt);
|
|
1927
|
+
}
|
|
1928
|
+
if (pulled > 0 && this.eventBus) {
|
|
1929
|
+
this.eventBus.emit({
|
|
1930
|
+
action: "updated",
|
|
1931
|
+
resource,
|
|
1932
|
+
timestamp: Date.now(),
|
|
1933
|
+
source: "remote"
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
return pulled;
|
|
1937
|
+
}
|
|
1938
|
+
// --- Conflict resolution ---
|
|
1939
|
+
async handleConflict(entry) {
|
|
1940
|
+
const strategy = this.config.conflictStrategy;
|
|
1941
|
+
if (strategy === "server-wins") {
|
|
1942
|
+
await this.queue.remove(entry.id);
|
|
1943
|
+
try {
|
|
1944
|
+
const result = await this.httpDriver.get(entry.resource, entry.entityId);
|
|
1945
|
+
this.localDriver.getStorageBackend().set(entry.resource, entry.entityId, result.data);
|
|
1946
|
+
} catch {
|
|
1947
|
+
}
|
|
1948
|
+
} else if (strategy === "client-wins" || strategy === "last-write-wins") {
|
|
1949
|
+
try {
|
|
1950
|
+
const serverResult = await this.httpDriver.get(entry.resource, entry.entityId);
|
|
1951
|
+
const serverData = serverResult.data;
|
|
1952
|
+
if (entry.action === "update" && entry.data) {
|
|
1953
|
+
const merged = { ...serverData, ...entry.data };
|
|
1954
|
+
await this.httpDriver.update(entry.resource, entry.entityId, merged);
|
|
1955
|
+
this.localDriver.getStorageBackend().set(entry.resource, entry.entityId, merged);
|
|
1956
|
+
}
|
|
1957
|
+
await this.queue.remove(entry.id);
|
|
1958
|
+
} catch {
|
|
1959
|
+
await this.queue.markFailed(entry.id, "Conflict resolution failed");
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
this.emitEvent("sync:conflict", {
|
|
1963
|
+
resource: entry.resource,
|
|
1964
|
+
action: entry.action,
|
|
1965
|
+
entityId: entry.entityId
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
// --- Connectivity ---
|
|
1969
|
+
setupConnectivityListeners() {
|
|
1970
|
+
if (typeof window === "undefined") return;
|
|
1971
|
+
window.addEventListener("online", () => this.handleOnline());
|
|
1972
|
+
window.addEventListener("offline", () => this.handleOffline());
|
|
1973
|
+
}
|
|
1974
|
+
async checkOnline() {
|
|
1975
|
+
if (typeof navigator !== "undefined" && !navigator.onLine) return false;
|
|
1976
|
+
try {
|
|
1977
|
+
const baseUrl = this.httpDriver.baseUrl;
|
|
1978
|
+
const response = await fetch(`${baseUrl}${this.config.pingUrl}`, {
|
|
1979
|
+
method: "HEAD",
|
|
1980
|
+
cache: "no-store",
|
|
1981
|
+
signal: AbortSignal.timeout(5e3)
|
|
1982
|
+
});
|
|
1983
|
+
return response.ok;
|
|
1984
|
+
} catch {
|
|
1985
|
+
return false;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
handleOnline() {
|
|
1989
|
+
if (this.online) return;
|
|
1990
|
+
this.online = true;
|
|
1991
|
+
this.emitEvent("online");
|
|
1992
|
+
this.emitSyncState();
|
|
1993
|
+
this.sync().catch(() => {
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
handleOffline() {
|
|
1997
|
+
if (!this.online) return;
|
|
1998
|
+
this.online = false;
|
|
1999
|
+
this.emitEvent("offline");
|
|
2000
|
+
this.emitSyncState();
|
|
2001
|
+
}
|
|
2002
|
+
// --- State ---
|
|
2003
|
+
getSyncState() {
|
|
2004
|
+
return {
|
|
2005
|
+
isOnline: this.online,
|
|
2006
|
+
isSyncing: this.syncing,
|
|
2007
|
+
pendingCount: this.queue.getPendingCount(),
|
|
2008
|
+
lastSynced: this.lastSynced
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
// --- Events ---
|
|
2012
|
+
emitEvent(type, data) {
|
|
2013
|
+
if (!this.eventBus) return;
|
|
2014
|
+
this.eventBus.emit({
|
|
2015
|
+
action: type,
|
|
2016
|
+
resource: "__sync",
|
|
2017
|
+
data: { type, ...data },
|
|
2018
|
+
timestamp: Date.now(),
|
|
2019
|
+
source: "local"
|
|
2020
|
+
});
|
|
2021
|
+
}
|
|
2022
|
+
emitSyncState() {
|
|
2023
|
+
this.emitEvent("state", this.getSyncState());
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
// src/drivers/sync/sync-driver.ts
|
|
2028
|
+
var SyncDriver = class {
|
|
2029
|
+
localDriver;
|
|
2030
|
+
httpDriver;
|
|
2031
|
+
queue;
|
|
2032
|
+
_syncEngine;
|
|
2033
|
+
_ready;
|
|
2034
|
+
_isReady = false;
|
|
2035
|
+
constructor(config) {
|
|
2036
|
+
this.localDriver = new LocalDriver({
|
|
2037
|
+
type: "local",
|
|
2038
|
+
persist: config.local.persist ?? "indexeddb",
|
|
2039
|
+
dbName: config.local.dbName
|
|
2040
|
+
});
|
|
2041
|
+
this.httpDriver = new HttpDriver({
|
|
2042
|
+
type: "http",
|
|
2043
|
+
baseUrl: config.remote.baseUrl,
|
|
2044
|
+
preset: config.remote.preset,
|
|
2045
|
+
timeout: config.remote.timeout,
|
|
2046
|
+
retry: config.remote.retry,
|
|
2047
|
+
headers: config.remote.headers
|
|
2048
|
+
});
|
|
2049
|
+
this.queue = new SyncQueue(
|
|
2050
|
+
config.local.dbName ? `${config.local.dbName}-sync-queue` : void 0
|
|
2051
|
+
);
|
|
2052
|
+
this._syncEngine = new SyncEngine(
|
|
2053
|
+
this.localDriver,
|
|
2054
|
+
this.httpDriver,
|
|
2055
|
+
this.queue,
|
|
2056
|
+
config.sync
|
|
2057
|
+
);
|
|
2058
|
+
this._ready = Promise.all([this.localDriver.ready, this.queue.ready]).then(() => {
|
|
2059
|
+
this._isReady = true;
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
get ready() {
|
|
2063
|
+
return this._ready;
|
|
2064
|
+
}
|
|
2065
|
+
get isReady() {
|
|
2066
|
+
return this._isReady;
|
|
2067
|
+
}
|
|
2068
|
+
get syncEngine() {
|
|
2069
|
+
return this._syncEngine;
|
|
2070
|
+
}
|
|
2071
|
+
/** @internal — exposed for createClient auth wiring */
|
|
2072
|
+
get _httpDriver() {
|
|
2073
|
+
return this.httpDriver;
|
|
2074
|
+
}
|
|
2075
|
+
// --- Reads: delegate to local ---
|
|
2076
|
+
async list(resource, query) {
|
|
2077
|
+
return this.localDriver.list(resource, query);
|
|
2078
|
+
}
|
|
2079
|
+
async get(resource, id) {
|
|
2080
|
+
return this.localDriver.get(resource, id);
|
|
2081
|
+
}
|
|
2082
|
+
async count(resource, filter) {
|
|
2083
|
+
return this.localDriver.count(resource, filter);
|
|
2084
|
+
}
|
|
2085
|
+
// --- Writes: local first, then enqueue ---
|
|
2086
|
+
async create(resource, data) {
|
|
2087
|
+
const result = await this.localDriver.create(resource, data);
|
|
2088
|
+
await this.queue.enqueue({
|
|
2089
|
+
resource,
|
|
2090
|
+
action: "create",
|
|
2091
|
+
entityId: result.data.id,
|
|
2092
|
+
data: result.data,
|
|
2093
|
+
timestamp: Date.now()
|
|
2094
|
+
});
|
|
2095
|
+
return result;
|
|
2096
|
+
}
|
|
2097
|
+
async update(resource, id, data) {
|
|
2098
|
+
const result = await this.localDriver.update(resource, id, data);
|
|
2099
|
+
await this.queue.enqueue({
|
|
2100
|
+
resource,
|
|
2101
|
+
action: "update",
|
|
2102
|
+
entityId: id,
|
|
2103
|
+
data,
|
|
2104
|
+
timestamp: Date.now()
|
|
2105
|
+
});
|
|
2106
|
+
return result;
|
|
2107
|
+
}
|
|
2108
|
+
async delete(resource, id) {
|
|
2109
|
+
const result = await this.localDriver.delete(resource, id);
|
|
2110
|
+
await this.queue.enqueue({
|
|
2111
|
+
resource,
|
|
2112
|
+
action: "delete",
|
|
2113
|
+
entityId: id,
|
|
2114
|
+
data: null,
|
|
2115
|
+
timestamp: Date.now()
|
|
2116
|
+
});
|
|
2117
|
+
return result;
|
|
2118
|
+
}
|
|
2119
|
+
// --- Bulk: delegate to local + enqueue each ---
|
|
2120
|
+
async bulkCreate(resource, data) {
|
|
2121
|
+
const results = [];
|
|
2122
|
+
for (const item of data) {
|
|
2123
|
+
const { data: created } = await this.create(resource, item);
|
|
2124
|
+
results.push(created);
|
|
2125
|
+
}
|
|
2126
|
+
return { data: results };
|
|
2127
|
+
}
|
|
2128
|
+
async bulkUpdate(resource, updates) {
|
|
2129
|
+
const results = [];
|
|
2130
|
+
for (const { id, data } of updates) {
|
|
2131
|
+
const { data: updated } = await this.update(resource, id, data);
|
|
2132
|
+
results.push(updated);
|
|
2133
|
+
}
|
|
2134
|
+
return { data: results };
|
|
2135
|
+
}
|
|
2136
|
+
async bulkDelete(resource, ids) {
|
|
2137
|
+
for (const id of ids) {
|
|
2138
|
+
await this.delete(resource, id);
|
|
2139
|
+
}
|
|
2140
|
+
return { data: { count: ids.length } };
|
|
2141
|
+
}
|
|
2142
|
+
// --- Custom request: local handler offline, HTTP when online ---
|
|
2143
|
+
async request(resource, path, options) {
|
|
2144
|
+
try {
|
|
2145
|
+
return await this.httpDriver.request(resource, path, options);
|
|
2146
|
+
} catch {
|
|
2147
|
+
if (options?.local) {
|
|
2148
|
+
return options.local();
|
|
2149
|
+
}
|
|
2150
|
+
throw new Error(
|
|
2151
|
+
"service.request() failed and no local handler provided. Provide a `local` callback for offline support."
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
// --- Seed/meta: delegate to local ---
|
|
2156
|
+
seed(resource, data, entityClass) {
|
|
2157
|
+
this.localDriver.seed(resource, data, entityClass);
|
|
2158
|
+
}
|
|
2159
|
+
getSeedVersion() {
|
|
2160
|
+
return this.localDriver.getSeedVersion();
|
|
2161
|
+
}
|
|
2162
|
+
setSeedVersion(version) {
|
|
2163
|
+
this.localDriver.setSeedVersion(version);
|
|
2164
|
+
}
|
|
2165
|
+
clear(resource) {
|
|
2166
|
+
this.localDriver.clear(resource);
|
|
2167
|
+
}
|
|
2168
|
+
// --- Wiring methods (called by createClient) ---
|
|
2169
|
+
setAuthProvider(provider) {
|
|
2170
|
+
this.httpDriver.setAuthProvider(provider);
|
|
2171
|
+
this.localDriver.setAuthProvider(() => {
|
|
2172
|
+
const auth = provider();
|
|
2173
|
+
if (!auth) return null;
|
|
2174
|
+
try {
|
|
2175
|
+
const payload = JSON.parse(atob(auth.token));
|
|
2176
|
+
return { userId: payload.userId, userName: payload.email };
|
|
2177
|
+
} catch {
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
setOnUnauthorized(handler) {
|
|
2183
|
+
this.httpDriver.setOnUnauthorized(handler);
|
|
2184
|
+
}
|
|
2185
|
+
registerEndpoint(resource, endpoint) {
|
|
2186
|
+
this.httpDriver.registerEndpoint(resource, endpoint);
|
|
2187
|
+
this._syncEngine.registerResource(resource, endpoint);
|
|
2188
|
+
}
|
|
2189
|
+
registerEntity(resource, entityClass) {
|
|
2190
|
+
this.localDriver.registerEntity(resource, entityClass);
|
|
2191
|
+
}
|
|
2192
|
+
getStorageBackend() {
|
|
2193
|
+
return this.localDriver.getStorageBackend();
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
|
|
1608
2197
|
// src/events/event-bus.ts
|
|
1609
2198
|
var EventBus = class {
|
|
1610
2199
|
listeners = /* @__PURE__ */ new Map();
|
|
@@ -1802,6 +2391,10 @@ function createClient(config) {
|
|
|
1802
2391
|
if (driver instanceof HttpDriver) {
|
|
1803
2392
|
driver.registerEndpoint(name, instance.endpoint);
|
|
1804
2393
|
}
|
|
2394
|
+
if (driver instanceof SyncDriver) {
|
|
2395
|
+
driver.registerEntity(name, instance.entity);
|
|
2396
|
+
driver.registerEndpoint(name, instance.endpoint);
|
|
2397
|
+
}
|
|
1805
2398
|
client[name] = instance;
|
|
1806
2399
|
}
|
|
1807
2400
|
if (config.auth) {
|
|
@@ -1864,6 +2457,45 @@ function createClient(config) {
|
|
|
1864
2457
|
return false;
|
|
1865
2458
|
}
|
|
1866
2459
|
});
|
|
2460
|
+
} else if (defaultDriver instanceof SyncDriver) {
|
|
2461
|
+
authInstance._init(defaultDriver, resourceName);
|
|
2462
|
+
defaultDriver.registerEntity(resourceName, authInstance.entity);
|
|
2463
|
+
defaultDriver.registerEndpoint(resourceName, authInstance.endpoint);
|
|
2464
|
+
const hasLocalStorage = typeof localStorage !== "undefined";
|
|
2465
|
+
const LS_AUTH_KEY = "fauxbase:auth";
|
|
2466
|
+
let memoryAuthState = null;
|
|
2467
|
+
authInstance._initAuth(
|
|
2468
|
+
() => {
|
|
2469
|
+
if (hasLocalStorage) {
|
|
2470
|
+
const raw = localStorage.getItem(LS_AUTH_KEY);
|
|
2471
|
+
return raw ? JSON.parse(raw) : null;
|
|
2472
|
+
}
|
|
2473
|
+
return memoryAuthState;
|
|
2474
|
+
},
|
|
2475
|
+
(state) => {
|
|
2476
|
+
if (hasLocalStorage) {
|
|
2477
|
+
if (state) {
|
|
2478
|
+
localStorage.setItem(LS_AUTH_KEY, JSON.stringify(state));
|
|
2479
|
+
} else {
|
|
2480
|
+
localStorage.removeItem(LS_AUTH_KEY);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
memoryAuthState = state;
|
|
2484
|
+
}
|
|
2485
|
+
);
|
|
2486
|
+
authInstance._setHttpMode(defaultDriver._httpDriver);
|
|
2487
|
+
defaultDriver.setAuthProvider(() => {
|
|
2488
|
+
const token = authInstance.token;
|
|
2489
|
+
return token ? { token } : null;
|
|
2490
|
+
});
|
|
2491
|
+
defaultDriver.setOnUnauthorized(async () => {
|
|
2492
|
+
try {
|
|
2493
|
+
await authInstance.refresh();
|
|
2494
|
+
return true;
|
|
2495
|
+
} catch {
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
1867
2499
|
}
|
|
1868
2500
|
client.auth = authInstance;
|
|
1869
2501
|
for (const driver of overrideDrivers.values()) {
|
|
@@ -1920,8 +2552,14 @@ function createClient(config) {
|
|
|
1920
2552
|
eventSource.reconnect();
|
|
1921
2553
|
});
|
|
1922
2554
|
}
|
|
2555
|
+
if (defaultDriver instanceof SyncDriver) {
|
|
2556
|
+
defaultDriver.syncEngine.setEventBus(eventBus);
|
|
2557
|
+
}
|
|
1923
2558
|
client.disconnect = () => {
|
|
1924
2559
|
eventSource?.disconnect();
|
|
2560
|
+
if (defaultDriver instanceof SyncDriver) {
|
|
2561
|
+
defaultDriver.syncEngine.stop();
|
|
2562
|
+
}
|
|
1925
2563
|
eventBus.destroy();
|
|
1926
2564
|
};
|
|
1927
2565
|
}
|
|
@@ -1939,6 +2577,13 @@ function createClient(config) {
|
|
|
1939
2577
|
}
|
|
1940
2578
|
});
|
|
1941
2579
|
}
|
|
2580
|
+
} else if (defaultDriver instanceof SyncDriver) {
|
|
2581
|
+
readyPromise = defaultDriver.ready.then(() => {
|
|
2582
|
+
if (config.seeds) {
|
|
2583
|
+
applySeedsIfNeeded(defaultDriver, config.seeds);
|
|
2584
|
+
}
|
|
2585
|
+
defaultDriver.syncEngine.start();
|
|
2586
|
+
});
|
|
1942
2587
|
} else {
|
|
1943
2588
|
readyPromise = Promise.resolve();
|
|
1944
2589
|
}
|
|
@@ -1956,6 +2601,8 @@ function createDriver(config) {
|
|
|
1956
2601
|
return new LocalDriver(config);
|
|
1957
2602
|
case "http":
|
|
1958
2603
|
return new HttpDriver(config);
|
|
2604
|
+
case "sync":
|
|
2605
|
+
return new SyncDriver(config);
|
|
1959
2606
|
default:
|
|
1960
2607
|
throw new Error(`Unknown driver type: ${config.type}`);
|
|
1961
2608
|
}
|
|
@@ -1970,6 +2617,6 @@ function applySeedsIfNeeded(driver, seeds) {
|
|
|
1970
2617
|
driver.setSeedVersion(newVersion);
|
|
1971
2618
|
}
|
|
1972
2619
|
|
|
1973
|
-
export { AuthService, ConflictError, Entity, EventBus, FauxbaseError, ForbiddenError, HttpDriver, HttpError, LocalDriver, NetworkError, NotFoundError, Service, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
|
|
2620
|
+
export { AuthService, ConflictError, Entity, EventBus, FauxbaseError, ForbiddenError, HttpDriver, HttpError, LocalDriver, NetworkError, NotFoundError, Service, SyncDriver, TimeoutError, ValidationError, afterCreate, afterUpdate, beforeCreate, beforeUpdate, computed, createClient, defaultPreset, definePreset, djangoPreset, expressPreset, field, getPreset, laravelPreset, nestjsPreset, relation, seed, springBootPreset };
|
|
1974
2621
|
//# sourceMappingURL=index.js.map
|
|
1975
2622
|
//# sourceMappingURL=index.js.map
|