@pylonsync/sync 0.3.140 → 0.3.144
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/package.json +1 -1
- package/src/index.ts +111 -4
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1113,6 +1113,19 @@ export class SyncEngine {
|
|
|
1113
1113
|
return;
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
// Session mutated server-side. Fires for select-org / clear-org
|
|
1117
|
+
// / session revoke — every tab connected as this user gets the
|
|
1118
|
+
// envelope (cross-machine too via the cluster bus). Trigger
|
|
1119
|
+
// a fresh /api/auth/me read which updates the cached session
|
|
1120
|
+
// AND, on tenant flip, resets the replica so stale rows from
|
|
1121
|
+
// the previous tenant disappear. App code calling
|
|
1122
|
+
// /api/auth/select-org via raw fetch no longer needs the
|
|
1123
|
+
// manual `notifySessionChanged()` step.
|
|
1124
|
+
if (msg.type === "session-changed") {
|
|
1125
|
+
void this.refreshResolvedSession();
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1116
1129
|
// Reactive query push: the server-side ReactiveRegistry re-ran
|
|
1117
1130
|
// a subscribed handler and the result hash changed. Route to
|
|
1118
1131
|
// the handler registered by `subscribeReactive` so the React
|
|
@@ -1679,15 +1692,109 @@ export class SyncEngine {
|
|
|
1679
1692
|
}
|
|
1680
1693
|
|
|
1681
1694
|
/**
|
|
1682
|
-
* Public alias for `refreshResolvedSession`.
|
|
1683
|
-
*
|
|
1684
|
-
*
|
|
1685
|
-
*
|
|
1695
|
+
* Public alias for `refreshResolvedSession`. Almost never needed by
|
|
1696
|
+
* app code today — the server pushes a `session-changed` envelope
|
|
1697
|
+
* over WS whenever the session is mutated (select-org, clear-org,
|
|
1698
|
+
* session revoke, even from other tabs / admin tools / server
|
|
1699
|
+
* actions), and the engine's WS handler refreshes automatically.
|
|
1700
|
+
*
|
|
1701
|
+
* Kept as an escape hatch for the rare case where you mutated the
|
|
1702
|
+
* session via a path that doesn't go through the framework's auth
|
|
1703
|
+
* surface (e.g. directly writing to the SessionStore from a Rust
|
|
1704
|
+
* plugin that bypassed `notify_session_changed`).
|
|
1705
|
+
*
|
|
1706
|
+
* The `selectOrg` / `clearOrg` / `signOut` helpers below remain as
|
|
1707
|
+
* convenience wrappers that combine the HTTP call with an immediate
|
|
1708
|
+
* local refresh — useful when the same tab needs the new state
|
|
1709
|
+
* before the WS round-trip lands.
|
|
1686
1710
|
*/
|
|
1687
1711
|
notifySessionChanged(): Promise<void> {
|
|
1688
1712
|
return this.refreshResolvedSession();
|
|
1689
1713
|
}
|
|
1690
1714
|
|
|
1715
|
+
/**
|
|
1716
|
+
* Switch the caller's active tenant (organization) and refresh the
|
|
1717
|
+
* resolved session in one shot. Membership is verified server-side
|
|
1718
|
+
* (POST /api/auth/select-org throws 403 if the user isn't a member
|
|
1719
|
+
* of the target org), and the engine's local replica resets so
|
|
1720
|
+
* `db.useQuery` stops returning the previous tenant's rows.
|
|
1721
|
+
*
|
|
1722
|
+
* Throws on any non-2xx response. The error carries the
|
|
1723
|
+
* server-issued JSON error body when available, so callers can
|
|
1724
|
+
* branch on `err.code === "NOT_A_MEMBER"` etc.
|
|
1725
|
+
*/
|
|
1726
|
+
async selectOrg(orgId: string): Promise<void> {
|
|
1727
|
+
await this.authMutate("/api/auth/select-org", { orgId });
|
|
1728
|
+
await this.refreshResolvedSession();
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
/**
|
|
1732
|
+
* Drop the caller's active tenant — back to the "no active org"
|
|
1733
|
+
* state typical of a login-lobby route. Refreshes the resolved
|
|
1734
|
+
* session so React subscribers re-render with `tenantId: null`.
|
|
1735
|
+
*/
|
|
1736
|
+
async clearOrg(): Promise<void> {
|
|
1737
|
+
await this.authMutate("/api/auth/select-org", { orgId: null });
|
|
1738
|
+
await this.refreshResolvedSession();
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/**
|
|
1742
|
+
* Revoke the current session server-side (DELETE /api/auth/session)
|
|
1743
|
+
* and refresh — leaves the caller anonymous. Local sync stops on
|
|
1744
|
+
* the next pull cycle; replica content stays in IndexedDB so a
|
|
1745
|
+
* subsequent sign-in as the same user is instant.
|
|
1746
|
+
*/
|
|
1747
|
+
async signOut(): Promise<void> {
|
|
1748
|
+
await this.authMutate("/api/auth/session", undefined, "DELETE");
|
|
1749
|
+
await this.refreshResolvedSession();
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/** Shared transport for the auth helpers above. Same bearer/cookie
|
|
1753
|
+
* policy as `request()` — keeps the auth flows on the same
|
|
1754
|
+
* authentication footing as data sync. */
|
|
1755
|
+
private async authMutate(
|
|
1756
|
+
path: string,
|
|
1757
|
+
body?: unknown,
|
|
1758
|
+
method = "POST",
|
|
1759
|
+
): Promise<unknown> {
|
|
1760
|
+
const headers: Record<string, string> = {};
|
|
1761
|
+
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
1762
|
+
const token =
|
|
1763
|
+
this.config.token ??
|
|
1764
|
+
this.storage.get(this.tokenStorageKey()) ??
|
|
1765
|
+
undefined;
|
|
1766
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1767
|
+
const res = await fetch(`${this.config.baseUrl}${path}`, {
|
|
1768
|
+
method,
|
|
1769
|
+
headers,
|
|
1770
|
+
credentials: "include",
|
|
1771
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
1772
|
+
});
|
|
1773
|
+
const text = await res.text();
|
|
1774
|
+
let parsed: unknown = null;
|
|
1775
|
+
if (text) {
|
|
1776
|
+
try {
|
|
1777
|
+
parsed = JSON.parse(text);
|
|
1778
|
+
} catch {
|
|
1779
|
+
// Server returned non-JSON (HTML error page from a proxy,
|
|
1780
|
+
// empty 204, etc.) — fall through; the !res.ok branch will
|
|
1781
|
+
// synthesise a useful Error from the status.
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (!res.ok) {
|
|
1785
|
+
const err = new Error(
|
|
1786
|
+
(parsed as { error?: { message?: string } } | null)?.error?.message ??
|
|
1787
|
+
`${method} ${path} failed: ${res.status}`,
|
|
1788
|
+
) as Error & { status?: number; code?: string };
|
|
1789
|
+
err.status = res.status;
|
|
1790
|
+
const code = (parsed as { error?: { code?: string } } | null)?.error
|
|
1791
|
+
?.code;
|
|
1792
|
+
if (code) err.code = code;
|
|
1793
|
+
throw err;
|
|
1794
|
+
}
|
|
1795
|
+
return parsed;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1691
1798
|
/**
|
|
1692
1799
|
* In-flight push promise. Used as a mutex so a slow push can't be restarted
|
|
1693
1800
|
* by the poll timer or a user mutation, which would resend the same batch
|