@fluxbase/sdk 0.0.1-rc.43 → 0.0.1-rc.45

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 CHANGED
@@ -3,6 +3,10 @@
3
3
  // src/fetch.ts
4
4
  var FluxbaseFetch = class {
5
5
  constructor(baseUrl, options = {}) {
6
+ this.refreshTokenCallback = null;
7
+ this.isRefreshing = false;
8
+ this.refreshPromise = null;
9
+ this.anonKey = null;
6
10
  this.baseUrl = baseUrl.replace(/\/$/, "");
7
11
  this.defaultHeaders = {
8
12
  "Content-Type": "application/json",
@@ -11,12 +15,30 @@ var FluxbaseFetch = class {
11
15
  this.timeout = options.timeout ?? 3e4;
12
16
  this.debug = options.debug ?? false;
13
17
  }
18
+ /**
19
+ * Register a callback to refresh the token when a 401 error occurs
20
+ * The callback should return true if refresh was successful, false otherwise
21
+ */
22
+ setRefreshTokenCallback(callback) {
23
+ this.refreshTokenCallback = callback;
24
+ }
25
+ /**
26
+ * Set the anon key for fallback authentication
27
+ * When setAuthToken(null) is called, the Authorization header will be
28
+ * restored to use this anon key instead of being deleted
29
+ */
30
+ setAnonKey(key) {
31
+ this.anonKey = key;
32
+ }
14
33
  /**
15
34
  * Update the authorization header
35
+ * When token is null, restores to anon key if available
16
36
  */
17
37
  setAuthToken(token) {
18
38
  if (token) {
19
39
  this.defaultHeaders["Authorization"] = `Bearer ${token}`;
40
+ } else if (this.anonKey) {
41
+ this.defaultHeaders["Authorization"] = `Bearer ${this.anonKey}`;
20
42
  } else {
21
43
  delete this.defaultHeaders["Authorization"];
22
44
  }
@@ -25,6 +47,12 @@ var FluxbaseFetch = class {
25
47
  * Make an HTTP request
26
48
  */
27
49
  async request(path, options) {
50
+ return this.requestInternal(path, options, false);
51
+ }
52
+ /**
53
+ * Internal request implementation with retry capability
54
+ */
55
+ async requestInternal(path, options, isRetry) {
28
56
  const url = `${this.baseUrl}${path}`;
29
57
  const headers = { ...this.defaultHeaders, ...options.headers };
30
58
  const controller = new AbortController();
@@ -33,10 +61,14 @@ var FluxbaseFetch = class {
33
61
  console.log(`[Fluxbase SDK] ${options.method} ${url}`, options.body);
34
62
  }
35
63
  try {
64
+ const isFormData = options.body && (options.body.constructor?.name === "FormData" || options.body instanceof FormData);
65
+ const requestHeaders = isFormData ? Object.fromEntries(
66
+ Object.entries(headers).filter(([key]) => key.toLowerCase() !== "content-type")
67
+ ) : headers;
36
68
  const response = await fetch(url, {
37
69
  method: options.method,
38
- headers,
39
- body: options.body ? JSON.stringify(options.body) : void 0,
70
+ headers: requestHeaders,
71
+ body: isFormData ? options.body : options.body ? JSON.stringify(options.body) : void 0,
40
72
  signal: controller.signal
41
73
  });
42
74
  clearTimeout(timeoutId);
@@ -50,6 +82,12 @@ var FluxbaseFetch = class {
50
82
  if (this.debug) {
51
83
  console.log(`[Fluxbase SDK] Response:`, response.status, data);
52
84
  }
85
+ if (response.status === 401 && !isRetry && !options.skipAutoRefresh && this.refreshTokenCallback) {
86
+ const refreshSuccess = await this.handleTokenRefresh();
87
+ if (refreshSuccess) {
88
+ return this.requestInternal(path, options, true);
89
+ }
90
+ }
53
91
  if (!response.ok) {
54
92
  const error = new Error(
55
93
  typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText
@@ -72,12 +110,122 @@ var FluxbaseFetch = class {
72
110
  throw new Error("Unknown error occurred");
73
111
  }
74
112
  }
113
+ /**
114
+ * Handle token refresh with deduplication
115
+ * Multiple concurrent requests that fail with 401 will share the same refresh operation
116
+ */
117
+ async handleTokenRefresh() {
118
+ if (this.isRefreshing && this.refreshPromise) {
119
+ return this.refreshPromise;
120
+ }
121
+ this.isRefreshing = true;
122
+ this.refreshPromise = this.executeRefresh();
123
+ try {
124
+ return await this.refreshPromise;
125
+ } finally {
126
+ this.isRefreshing = false;
127
+ this.refreshPromise = null;
128
+ }
129
+ }
130
+ /**
131
+ * Execute the actual token refresh
132
+ */
133
+ async executeRefresh() {
134
+ if (!this.refreshTokenCallback) {
135
+ return false;
136
+ }
137
+ try {
138
+ return await this.refreshTokenCallback();
139
+ } catch (error) {
140
+ if (this.debug) {
141
+ console.error("[Fluxbase SDK] Token refresh failed:", error);
142
+ }
143
+ return false;
144
+ }
145
+ }
75
146
  /**
76
147
  * GET request
77
148
  */
78
149
  async get(path, options = {}) {
79
150
  return this.request(path, { ...options, method: "GET" });
80
151
  }
152
+ /**
153
+ * GET request that returns response with headers (for count queries)
154
+ */
155
+ async getWithHeaders(path, options = {}) {
156
+ return this.requestWithHeaders(path, { ...options, method: "GET" });
157
+ }
158
+ /**
159
+ * Make an HTTP request and return response with headers
160
+ */
161
+ async requestWithHeaders(path, options) {
162
+ return this.requestWithHeadersInternal(path, options, false);
163
+ }
164
+ /**
165
+ * Internal request implementation that returns response with headers
166
+ */
167
+ async requestWithHeadersInternal(path, options, isRetry) {
168
+ const url = `${this.baseUrl}${path}`;
169
+ const headers = { ...this.defaultHeaders, ...options.headers };
170
+ const controller = new AbortController();
171
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.timeout);
172
+ if (this.debug) {
173
+ console.log(`[Fluxbase SDK] ${options.method} ${url}`, options.body);
174
+ }
175
+ try {
176
+ const isFormData = options.body && (options.body.constructor?.name === "FormData" || options.body instanceof FormData);
177
+ const requestHeaders = isFormData ? Object.fromEntries(
178
+ Object.entries(headers).filter(([key]) => key.toLowerCase() !== "content-type")
179
+ ) : headers;
180
+ const response = await fetch(url, {
181
+ method: options.method,
182
+ headers: requestHeaders,
183
+ body: isFormData ? options.body : options.body ? JSON.stringify(options.body) : void 0,
184
+ signal: controller.signal
185
+ });
186
+ clearTimeout(timeoutId);
187
+ const contentType = response.headers.get("content-type");
188
+ let data;
189
+ if (contentType?.includes("application/json")) {
190
+ data = await response.json();
191
+ } else {
192
+ data = await response.text();
193
+ }
194
+ if (this.debug) {
195
+ console.log(`[Fluxbase SDK] Response:`, response.status, data);
196
+ }
197
+ if (response.status === 401 && !isRetry && !options.skipAutoRefresh && this.refreshTokenCallback) {
198
+ const refreshSuccess = await this.handleTokenRefresh();
199
+ if (refreshSuccess) {
200
+ return this.requestWithHeadersInternal(path, options, true);
201
+ }
202
+ }
203
+ if (!response.ok) {
204
+ const error = new Error(
205
+ typeof data === "object" && data && "error" in data ? String(data.error) : response.statusText
206
+ );
207
+ error.status = response.status;
208
+ error.details = data;
209
+ throw error;
210
+ }
211
+ return {
212
+ data,
213
+ headers: response.headers,
214
+ status: response.status
215
+ };
216
+ } catch (err) {
217
+ clearTimeout(timeoutId);
218
+ if (err instanceof Error) {
219
+ if (err.name === "AbortError") {
220
+ const timeoutError = new Error("Request timeout");
221
+ timeoutError.status = 408;
222
+ throw timeoutError;
223
+ }
224
+ throw err;
225
+ }
226
+ throw new Error("Unknown error occurred");
227
+ }
228
+ }
81
229
  /**
82
230
  * POST request
83
231
  */
@@ -141,6 +289,9 @@ async function wrapAsyncVoid(operation) {
141
289
 
142
290
  // src/auth.ts
143
291
  var AUTH_STORAGE_KEY = "fluxbase.auth.session";
292
+ var AUTO_REFRESH_TICK_THRESHOLD = 10;
293
+ var AUTO_REFRESH_TICK_MINIMUM = 1e3;
294
+ var MAX_REFRESH_RETRIES = 3;
144
295
  var MemoryStorage = class {
145
296
  constructor() {
146
297
  this.store = /* @__PURE__ */ new Map();
@@ -186,6 +337,10 @@ var FluxbaseAuth = class {
186
337
  this.fetch = fetch2;
187
338
  this.persist = persist;
188
339
  this.autoRefresh = autoRefresh;
340
+ this.fetch.setRefreshTokenCallback(async () => {
341
+ const result = await this.refreshSession();
342
+ return !result.error;
343
+ });
189
344
  if (this.persist) {
190
345
  if (isLocalStorageAvailable()) {
191
346
  this.storage = localStorage;
@@ -253,6 +408,24 @@ var FluxbaseAuth = class {
253
408
  };
254
409
  return { data: { subscription } };
255
410
  }
411
+ /**
412
+ * Start the automatic token refresh timer
413
+ * This is called automatically when autoRefresh is enabled and a session exists
414
+ * Only works in browser environments
415
+ */
416
+ startAutoRefresh() {
417
+ this.scheduleTokenRefresh();
418
+ }
419
+ /**
420
+ * Stop the automatic token refresh timer
421
+ * Call this when you want to disable auto-refresh without signing out
422
+ */
423
+ stopAutoRefresh() {
424
+ if (this.refreshTimer) {
425
+ clearTimeout(this.refreshTimer);
426
+ this.refreshTimer = null;
427
+ }
428
+ }
256
429
  /**
257
430
  * Sign in with email and password (Supabase-compatible)
258
431
  * Returns { user, session } if successful, or SignInWith2FAResponse if 2FA is required
@@ -334,10 +507,13 @@ var FluxbaseAuth = class {
334
507
  "/api/v1/auth/refresh",
335
508
  {
336
509
  refresh_token: this.session.refresh_token
337
- }
510
+ },
511
+ { skipAutoRefresh: true }
512
+ // Prevent infinite loop on 401
338
513
  );
339
514
  const session = {
340
515
  ...response,
516
+ user: response.user ?? this.session.user,
341
517
  expires_at: Date.now() + response.expires_in * 1e3
342
518
  };
343
519
  this.setSessionInternal(session, "TOKEN_REFRESHED");
@@ -386,7 +562,10 @@ var FluxbaseAuth = class {
386
562
  if (attributes.nonce) {
387
563
  requestBody.nonce = attributes.nonce;
388
564
  }
389
- const user = await this.fetch.patch("/api/v1/auth/user", requestBody);
565
+ const user = await this.fetch.patch(
566
+ "/api/v1/auth/user",
567
+ requestBody
568
+ );
390
569
  if (this.session) {
391
570
  this.session.user = user;
392
571
  this.saveSession();
@@ -859,24 +1038,57 @@ var FluxbaseAuth = class {
859
1038
  }
860
1039
  /**
861
1040
  * Internal: Schedule automatic token refresh
1041
+ * Only runs in browser environments when autoRefresh is enabled
862
1042
  */
863
1043
  scheduleTokenRefresh() {
864
- if (!this.autoRefresh || !this.session?.expires_at) {
1044
+ if (!this.autoRefresh || typeof window === "undefined") {
1045
+ return;
1046
+ }
1047
+ if (!this.session?.expires_at) {
865
1048
  return;
866
1049
  }
867
1050
  if (this.refreshTimer) {
868
1051
  clearTimeout(this.refreshTimer);
1052
+ this.refreshTimer = null;
869
1053
  }
870
- const refreshAt = this.session.expires_at - 60 * 1e3;
871
- const delay = refreshAt - Date.now();
872
- if (delay > 0) {
873
- this.refreshTimer = setTimeout(async () => {
874
- const result = await this.refreshSession();
875
- if (result.error) {
876
- console.error("Failed to refresh token:", result.error);
877
- this.clearSession();
878
- }
879
- }, delay);
1054
+ const expiresAt = this.session.expires_at;
1055
+ const now = Date.now();
1056
+ const timeUntilExpiry = expiresAt - now;
1057
+ const refreshIn = Math.max(
1058
+ timeUntilExpiry - AUTO_REFRESH_TICK_THRESHOLD * 1e3,
1059
+ AUTO_REFRESH_TICK_MINIMUM
1060
+ );
1061
+ this.refreshTimer = setTimeout(() => {
1062
+ this.attemptRefresh();
1063
+ }, refreshIn);
1064
+ }
1065
+ /**
1066
+ * Internal: Attempt to refresh the token with retry logic
1067
+ * Uses exponential backoff: 1s, 2s, 4s delays between retries
1068
+ */
1069
+ async attemptRefresh(retries = MAX_REFRESH_RETRIES) {
1070
+ try {
1071
+ const result = await this.refreshSession();
1072
+ if (result.error) {
1073
+ throw result.error;
1074
+ }
1075
+ } catch (error) {
1076
+ if (retries > 0) {
1077
+ const delay = Math.pow(2, MAX_REFRESH_RETRIES - retries) * 1e3;
1078
+ console.warn(
1079
+ `Token refresh failed, retrying in ${delay / 1e3}s (${retries} attempts remaining)`,
1080
+ error
1081
+ );
1082
+ this.refreshTimer = setTimeout(() => {
1083
+ this.attemptRefresh(retries - 1);
1084
+ }, delay);
1085
+ } else {
1086
+ console.error(
1087
+ "Token refresh failed after all retries, signing out",
1088
+ error
1089
+ );
1090
+ this.clearSession();
1091
+ }
880
1092
  }
881
1093
  }
882
1094
  /**
@@ -907,14 +1119,24 @@ var RealtimeChannel = class {
907
1119
  this.reconnectAttempts = 0;
908
1120
  this.maxReconnectAttempts = 10;
909
1121
  this.reconnectDelay = 1e3;
1122
+ this.shouldReconnect = true;
910
1123
  this.heartbeatInterval = null;
911
1124
  this.pendingAcks = /* @__PURE__ */ new Map();
912
1125
  this.messageIdCounter = 0;
1126
+ this.onTokenRefreshNeeded = null;
1127
+ this.isRefreshingToken = false;
913
1128
  this.url = url;
914
1129
  this.channelName = channelName;
915
1130
  this.token = token;
916
1131
  this.config = config;
917
1132
  }
1133
+ /**
1134
+ * Set callback to request a token refresh when connection fails due to auth
1135
+ * @internal
1136
+ */
1137
+ setTokenRefreshCallback(callback) {
1138
+ this.onTokenRefreshNeeded = callback;
1139
+ }
918
1140
  // Implementation
919
1141
  on(event, configOrCallback, callback) {
920
1142
  if (event === "postgres_changes" && typeof configOrCallback !== "function") {
@@ -966,6 +1188,7 @@ var RealtimeChannel = class {
966
1188
  * @param _timeout - Optional timeout in milliseconds (currently unused)
967
1189
  */
968
1190
  subscribe(callback, _timeout) {
1191
+ this.shouldReconnect = true;
969
1192
  this.connect();
970
1193
  if (callback) {
971
1194
  const checkConnection = () => {
@@ -987,6 +1210,7 @@ var RealtimeChannel = class {
987
1210
  * @returns Promise resolving to status string (Supabase-compatible)
988
1211
  */
989
1212
  async unsubscribe(timeout) {
1213
+ this.shouldReconnect = false;
990
1214
  return new Promise((resolve) => {
991
1215
  if (this.ws) {
992
1216
  this.sendMessage({
@@ -1162,6 +1386,22 @@ var RealtimeChannel = class {
1162
1386
  presenceState() {
1163
1387
  return { ...this._presenceState };
1164
1388
  }
1389
+ /**
1390
+ * Check if the current token is expired or about to expire
1391
+ */
1392
+ isTokenExpired() {
1393
+ if (!this.token) return false;
1394
+ try {
1395
+ const parts = this.token.split(".");
1396
+ if (parts.length !== 3 || !parts[1]) return false;
1397
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
1398
+ if (!payload.exp) return false;
1399
+ const now = Math.floor(Date.now() / 1e3);
1400
+ return payload.exp <= now + 10;
1401
+ } catch {
1402
+ return true;
1403
+ }
1404
+ }
1165
1405
  /**
1166
1406
  * Internal: Connect to WebSocket
1167
1407
  */
@@ -1169,6 +1409,29 @@ var RealtimeChannel = class {
1169
1409
  if (this.ws && this.ws.readyState === WebSocket.OPEN) {
1170
1410
  return;
1171
1411
  }
1412
+ if (this.isTokenExpired() && this.onTokenRefreshNeeded && !this.isRefreshingToken) {
1413
+ this.isRefreshingToken = true;
1414
+ console.log("[Fluxbase Realtime] Token expired, requesting refresh before connecting");
1415
+ this.onTokenRefreshNeeded().then((newToken) => {
1416
+ this.isRefreshingToken = false;
1417
+ if (newToken) {
1418
+ this.token = newToken;
1419
+ console.log("[Fluxbase Realtime] Token refreshed, connecting with new token");
1420
+ }
1421
+ this.connectWithToken();
1422
+ }).catch((err) => {
1423
+ this.isRefreshingToken = false;
1424
+ console.error("[Fluxbase Realtime] Token refresh failed:", err);
1425
+ this.connectWithToken();
1426
+ });
1427
+ return;
1428
+ }
1429
+ this.connectWithToken();
1430
+ }
1431
+ /**
1432
+ * Internal: Actually establish the WebSocket connection
1433
+ */
1434
+ connectWithToken() {
1172
1435
  const wsUrl = new URL(this.url);
1173
1436
  wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:";
1174
1437
  wsUrl.pathname = "/realtime";
@@ -1190,11 +1453,17 @@ var RealtimeChannel = class {
1190
1453
  this.startHeartbeat();
1191
1454
  };
1192
1455
  this.ws.onmessage = (event) => {
1456
+ let message;
1193
1457
  try {
1194
- const message = JSON.parse(event.data);
1195
- this.handleMessage(message);
1458
+ message = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
1196
1459
  } catch (err) {
1197
1460
  console.error("[Fluxbase Realtime] Failed to parse message:", err);
1461
+ return;
1462
+ }
1463
+ try {
1464
+ this.handleMessage(message);
1465
+ } catch (err) {
1466
+ console.error("[Fluxbase Realtime] Error handling message:", err, message);
1198
1467
  }
1199
1468
  };
1200
1469
  this.ws.onerror = (error) => {
@@ -1230,7 +1499,6 @@ var RealtimeChannel = class {
1230
1499
  handleMessage(message) {
1231
1500
  switch (message.type) {
1232
1501
  case "heartbeat":
1233
- this.ws?.send(JSON.stringify({ type: "heartbeat" }));
1234
1502
  break;
1235
1503
  case "broadcast":
1236
1504
  if (message.broadcast) {
@@ -1250,6 +1518,23 @@ var RealtimeChannel = class {
1250
1518
  if (ackHandler) {
1251
1519
  ackHandler.resolve(message.status || "ok");
1252
1520
  }
1521
+ } else if (message.payload && typeof message.payload === "object" && "type" in message.payload) {
1522
+ const payload = message.payload;
1523
+ if (payload.type === "access_token" && this.pendingAcks.has("access_token")) {
1524
+ const ackHandler = this.pendingAcks.get("access_token");
1525
+ if (ackHandler) {
1526
+ ackHandler.resolve("ok");
1527
+ this.pendingAcks.delete("access_token");
1528
+ }
1529
+ console.log("[Fluxbase Realtime] Token updated successfully");
1530
+ } else {
1531
+ if (payload.subscription_id) {
1532
+ this.subscriptionId = payload.subscription_id;
1533
+ console.log("[Fluxbase Realtime] Subscription ID received:", this.subscriptionId);
1534
+ } else {
1535
+ console.log("[Fluxbase Realtime] Acknowledged:", message);
1536
+ }
1537
+ }
1253
1538
  } else {
1254
1539
  if (message.payload && typeof message.payload === "object" && "subscription_id" in message.payload) {
1255
1540
  this.subscriptionId = message.payload.subscription_id;
@@ -1261,6 +1546,18 @@ var RealtimeChannel = class {
1261
1546
  break;
1262
1547
  case "error":
1263
1548
  console.error("[Fluxbase Realtime] Error:", message.error);
1549
+ if (this.pendingAcks.has("access_token")) {
1550
+ const ackHandler = this.pendingAcks.get("access_token");
1551
+ if (ackHandler) {
1552
+ ackHandler.reject(new Error(message.error || "Token update failed"));
1553
+ this.pendingAcks.delete("access_token");
1554
+ }
1555
+ }
1556
+ break;
1557
+ case "postgres_changes":
1558
+ if (message.payload) {
1559
+ this.handlePostgresChanges(message.payload);
1560
+ }
1264
1561
  break;
1265
1562
  }
1266
1563
  }
@@ -1314,7 +1611,7 @@ var RealtimeChannel = class {
1314
1611
  schema: payload.schema,
1315
1612
  table: payload.table,
1316
1613
  commit_timestamp: payload.timestamp || payload.commit_timestamp || (/* @__PURE__ */ new Date()).toISOString(),
1317
- new: payload.new_record || payload.new || {},
1614
+ new: payload.new_record || payload.new || payload.record || {},
1318
1615
  old: payload.old_record || payload.old || {},
1319
1616
  errors: payload.errors || null
1320
1617
  };
@@ -1331,6 +1628,7 @@ var RealtimeChannel = class {
1331
1628
  * Internal: Start heartbeat interval
1332
1629
  */
1333
1630
  startHeartbeat() {
1631
+ this.stopHeartbeat();
1334
1632
  this.heartbeatInterval = setInterval(() => {
1335
1633
  this.sendMessage({ type: "heartbeat" });
1336
1634
  }, 3e4);
@@ -1344,10 +1642,61 @@ var RealtimeChannel = class {
1344
1642
  this.heartbeatInterval = null;
1345
1643
  }
1346
1644
  }
1645
+ /**
1646
+ * Update the authentication token on an existing connection
1647
+ * Sends an access_token message to the server to update auth context
1648
+ * On failure, silently triggers a reconnect
1649
+ *
1650
+ * @param token - The new JWT access token
1651
+ * @internal
1652
+ */
1653
+ updateToken(token) {
1654
+ this.token = token;
1655
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
1656
+ return;
1657
+ }
1658
+ if (!token) {
1659
+ this.disconnect();
1660
+ this.connect();
1661
+ return;
1662
+ }
1663
+ const message = {
1664
+ type: "access_token",
1665
+ token
1666
+ };
1667
+ try {
1668
+ this.ws.send(JSON.stringify(message));
1669
+ const timeout = setTimeout(() => {
1670
+ console.warn(
1671
+ "[Fluxbase Realtime] Token update acknowledgment timeout, reconnecting"
1672
+ );
1673
+ this.disconnect();
1674
+ this.connect();
1675
+ }, 5e3);
1676
+ this.pendingAcks.set("access_token", {
1677
+ resolve: () => {
1678
+ clearTimeout(timeout);
1679
+ },
1680
+ reject: () => {
1681
+ clearTimeout(timeout);
1682
+ this.disconnect();
1683
+ this.connect();
1684
+ },
1685
+ timeout
1686
+ });
1687
+ } catch (error) {
1688
+ console.error("[Fluxbase Realtime] Failed to send token update:", error);
1689
+ this.disconnect();
1690
+ this.connect();
1691
+ }
1692
+ }
1347
1693
  /**
1348
1694
  * Internal: Attempt to reconnect
1349
1695
  */
1350
1696
  attemptReconnect() {
1697
+ if (!this.shouldReconnect) {
1698
+ return;
1699
+ }
1351
1700
  if (this.reconnectAttempts >= this.maxReconnectAttempts) {
1352
1701
  console.error("[Fluxbase Realtime] Max reconnect attempts reached");
1353
1702
  return;
@@ -1365,9 +1714,21 @@ var RealtimeChannel = class {
1365
1714
  var FluxbaseRealtime = class {
1366
1715
  constructor(url, token = null) {
1367
1716
  this.channels = /* @__PURE__ */ new Map();
1717
+ this.tokenRefreshCallback = null;
1368
1718
  this.url = url;
1369
1719
  this.token = token;
1370
1720
  }
1721
+ /**
1722
+ * Set callback to request a token refresh when connections fail due to auth
1723
+ * This callback should refresh the auth token and return the new access token
1724
+ * @internal
1725
+ */
1726
+ setTokenRefreshCallback(callback) {
1727
+ this.tokenRefreshCallback = callback;
1728
+ this.channels.forEach((channel) => {
1729
+ channel.setTokenRefreshCallback(callback);
1730
+ });
1731
+ }
1371
1732
  /**
1372
1733
  * Create or get a channel with optional configuration
1373
1734
  *
@@ -1395,6 +1756,9 @@ var FluxbaseRealtime = class {
1395
1756
  this.token,
1396
1757
  config
1397
1758
  );
1759
+ if (this.tokenRefreshCallback) {
1760
+ channel.setTokenRefreshCallback(this.tokenRefreshCallback);
1761
+ }
1398
1762
  this.channels.set(key, channel);
1399
1763
  return channel;
1400
1764
  }
@@ -1429,10 +1793,16 @@ var FluxbaseRealtime = class {
1429
1793
  }
1430
1794
  /**
1431
1795
  * Update auth token for all channels
1796
+ * Updates both the stored token for new channels and propagates
1797
+ * the token to all existing connected channels.
1798
+ *
1432
1799
  * @param token - The new auth token
1433
1800
  */
1434
1801
  setAuth(token) {
1435
1802
  this.token = token;
1803
+ this.channels.forEach((channel) => {
1804
+ channel.updateToken(token);
1805
+ });
1436
1806
  }
1437
1807
  };
1438
1808
 
@@ -1445,13 +1815,20 @@ var StorageBucket = class {
1445
1815
  /**
1446
1816
  * Upload a file to the bucket
1447
1817
  * @param path - The path/key for the file
1448
- * @param file - The file to upload (File, Blob, or ArrayBuffer)
1818
+ * @param file - The file to upload (File, Blob, ArrayBuffer, or ArrayBufferView like Uint8Array)
1449
1819
  * @param options - Upload options
1450
1820
  */
1451
1821
  async upload(path, file, options) {
1452
1822
  try {
1453
1823
  const formData = new FormData();
1454
- const blob = file instanceof ArrayBuffer ? new Blob([file]) : file;
1824
+ let blob;
1825
+ if (file instanceof ArrayBuffer) {
1826
+ blob = new Blob([file], { type: options?.contentType });
1827
+ } else if (ArrayBuffer.isView(file)) {
1828
+ blob = new Blob([file], { type: options?.contentType });
1829
+ } else {
1830
+ blob = file;
1831
+ }
1455
1832
  formData.append("file", blob);
1456
1833
  if (options?.contentType) {
1457
1834
  formData.append("content_type", options.contentType);
@@ -1542,23 +1919,228 @@ var StorageBucket = class {
1542
1919
  xhr.send(formData);
1543
1920
  });
1544
1921
  }
1922
+ async download(path, options) {
1923
+ try {
1924
+ const controller = new AbortController();
1925
+ let timeoutId;
1926
+ if (options?.signal) {
1927
+ if (options.signal.aborted) {
1928
+ return { data: null, error: new Error("Download aborted") };
1929
+ }
1930
+ options.signal.addEventListener("abort", () => controller.abort(), {
1931
+ once: true
1932
+ });
1933
+ }
1934
+ const timeout = options?.timeout ?? (options?.stream ? 0 : 3e4);
1935
+ if (timeout > 0) {
1936
+ timeoutId = setTimeout(() => controller.abort(), timeout);
1937
+ }
1938
+ try {
1939
+ const response = await fetch(
1940
+ `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`,
1941
+ {
1942
+ headers: this.fetch["defaultHeaders"],
1943
+ signal: controller.signal
1944
+ }
1945
+ );
1946
+ if (timeoutId) clearTimeout(timeoutId);
1947
+ if (!response.ok) {
1948
+ throw new Error(`Failed to download file: ${response.statusText}`);
1949
+ }
1950
+ if (options?.stream) {
1951
+ if (!response.body) {
1952
+ throw new Error("Response body is not available for streaming");
1953
+ }
1954
+ const contentLength = response.headers.get("content-length");
1955
+ const size = contentLength ? parseInt(contentLength, 10) : null;
1956
+ return {
1957
+ data: { stream: response.body, size },
1958
+ error: null
1959
+ };
1960
+ }
1961
+ const blob = await response.blob();
1962
+ return { data: blob, error: null };
1963
+ } catch (err) {
1964
+ if (timeoutId) clearTimeout(timeoutId);
1965
+ if (err instanceof Error && err.name === "AbortError") {
1966
+ if (options?.signal?.aborted) {
1967
+ return { data: null, error: new Error("Download aborted") };
1968
+ }
1969
+ return { data: null, error: new Error("Download timeout") };
1970
+ }
1971
+ throw err;
1972
+ }
1973
+ } catch (error) {
1974
+ return { data: null, error };
1975
+ }
1976
+ }
1545
1977
  /**
1546
- * Download a file from the bucket
1547
- * @param path - The path/key of the file
1978
+ * Download a file with resumable chunked downloads for large files.
1979
+ * Returns a ReadableStream that abstracts the chunking internally.
1980
+ *
1981
+ * Features:
1982
+ * - Downloads file in chunks using HTTP Range headers
1983
+ * - Automatically retries failed chunks with exponential backoff
1984
+ * - Reports progress via callback
1985
+ * - Falls back to regular streaming if Range not supported
1986
+ *
1987
+ * @param path - The file path within the bucket
1988
+ * @param options - Download options including chunk size, retries, and progress callback
1989
+ * @returns A ReadableStream and file size (consumer doesn't need to know about chunking)
1990
+ *
1991
+ * @example
1992
+ * ```typescript
1993
+ * const { data, error } = await storage.from('bucket').downloadResumable('large.json', {
1994
+ * chunkSize: 5 * 1024 * 1024, // 5MB chunks
1995
+ * maxRetries: 3,
1996
+ * onProgress: (progress) => console.log(`${progress.percentage}% complete`)
1997
+ * });
1998
+ * if (data) {
1999
+ * console.log(`File size: ${data.size} bytes`);
2000
+ * // Process data.stream...
2001
+ * }
2002
+ * ```
1548
2003
  */
1549
- async download(path) {
2004
+ async downloadResumable(path, options) {
1550
2005
  try {
1551
- const response = await fetch(
1552
- `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`,
1553
- {
1554
- headers: this.fetch["defaultHeaders"]
1555
- }
1556
- );
1557
- if (!response.ok) {
1558
- throw new Error(`Failed to download file: ${response.statusText}`);
2006
+ const chunkSize = options?.chunkSize ?? 5 * 1024 * 1024;
2007
+ const maxRetries = options?.maxRetries ?? 3;
2008
+ const retryDelayMs = options?.retryDelayMs ?? 1e3;
2009
+ const chunkTimeout = options?.chunkTimeout ?? 3e4;
2010
+ const url = `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`;
2011
+ const headers = this.fetch["defaultHeaders"];
2012
+ if (options?.signal?.aborted) {
2013
+ return { data: null, error: new Error("Download aborted") };
2014
+ }
2015
+ const headResponse = await fetch(url, {
2016
+ method: "HEAD",
2017
+ headers,
2018
+ signal: options?.signal
2019
+ });
2020
+ if (!headResponse.ok) {
2021
+ throw new Error(`Failed to get file info: ${headResponse.statusText}`);
2022
+ }
2023
+ const contentLength = headResponse.headers.get("content-length");
2024
+ const acceptRanges = headResponse.headers.get("accept-ranges");
2025
+ const totalSize = contentLength ? parseInt(contentLength, 10) : null;
2026
+ if (acceptRanges !== "bytes") {
2027
+ const { data, error } = await this.download(path, {
2028
+ stream: true,
2029
+ timeout: 0,
2030
+ signal: options?.signal
2031
+ });
2032
+ if (error) return { data: null, error };
2033
+ return {
2034
+ data,
2035
+ error: null
2036
+ };
1559
2037
  }
1560
- const blob = await response.blob();
1561
- return { data: blob, error: null };
2038
+ let downloadedBytes = 0;
2039
+ let currentChunk = 0;
2040
+ const totalChunks = totalSize ? Math.ceil(totalSize / chunkSize) : null;
2041
+ let lastProgressTime = Date.now();
2042
+ let lastProgressBytes = 0;
2043
+ const stream = new ReadableStream({
2044
+ async pull(controller) {
2045
+ if (options?.signal?.aborted) {
2046
+ controller.error(new Error("Download aborted"));
2047
+ return;
2048
+ }
2049
+ if (totalSize !== null && downloadedBytes >= totalSize) {
2050
+ controller.close();
2051
+ return;
2052
+ }
2053
+ const rangeStart = downloadedBytes;
2054
+ const rangeEnd = totalSize !== null ? Math.min(downloadedBytes + chunkSize - 1, totalSize - 1) : downloadedBytes + chunkSize - 1;
2055
+ let retryCount = 0;
2056
+ let chunk = null;
2057
+ while (retryCount <= maxRetries && chunk === null) {
2058
+ try {
2059
+ if (options?.signal?.aborted) {
2060
+ controller.error(new Error("Download aborted"));
2061
+ return;
2062
+ }
2063
+ const chunkController = new AbortController();
2064
+ const timeoutId = setTimeout(
2065
+ () => chunkController.abort(),
2066
+ chunkTimeout
2067
+ );
2068
+ if (options?.signal) {
2069
+ options.signal.addEventListener(
2070
+ "abort",
2071
+ () => chunkController.abort(),
2072
+ { once: true }
2073
+ );
2074
+ }
2075
+ const chunkResponse = await fetch(url, {
2076
+ headers: {
2077
+ ...headers,
2078
+ Range: `bytes=${rangeStart}-${rangeEnd}`
2079
+ },
2080
+ signal: chunkController.signal
2081
+ });
2082
+ clearTimeout(timeoutId);
2083
+ if (!chunkResponse.ok && chunkResponse.status !== 206) {
2084
+ throw new Error(
2085
+ `Chunk download failed: ${chunkResponse.statusText}`
2086
+ );
2087
+ }
2088
+ const arrayBuffer = await chunkResponse.arrayBuffer();
2089
+ chunk = new Uint8Array(arrayBuffer);
2090
+ if (totalSize === null && chunk.byteLength < chunkSize) {
2091
+ downloadedBytes += chunk.byteLength;
2092
+ currentChunk++;
2093
+ controller.enqueue(chunk);
2094
+ controller.close();
2095
+ return;
2096
+ }
2097
+ } catch (err) {
2098
+ if (options?.signal?.aborted) {
2099
+ controller.error(new Error("Download aborted"));
2100
+ return;
2101
+ }
2102
+ retryCount++;
2103
+ if (retryCount > maxRetries) {
2104
+ controller.error(
2105
+ new Error(
2106
+ `Failed to download chunk after ${maxRetries} retries`
2107
+ )
2108
+ );
2109
+ return;
2110
+ }
2111
+ const delay = retryDelayMs * Math.pow(2, retryCount - 1);
2112
+ await new Promise((resolve) => setTimeout(resolve, delay));
2113
+ }
2114
+ }
2115
+ if (chunk) {
2116
+ downloadedBytes += chunk.byteLength;
2117
+ currentChunk++;
2118
+ if (options?.onProgress) {
2119
+ const now = Date.now();
2120
+ const elapsed = (now - lastProgressTime) / 1e3;
2121
+ const bytesPerSecond = elapsed > 0 ? (downloadedBytes - lastProgressBytes) / elapsed : 0;
2122
+ lastProgressTime = now;
2123
+ lastProgressBytes = downloadedBytes;
2124
+ options.onProgress({
2125
+ loaded: downloadedBytes,
2126
+ total: totalSize,
2127
+ percentage: totalSize ? Math.round(downloadedBytes / totalSize * 100) : null,
2128
+ currentChunk,
2129
+ totalChunks,
2130
+ bytesPerSecond
2131
+ });
2132
+ }
2133
+ controller.enqueue(chunk);
2134
+ if (totalSize !== null && downloadedBytes >= totalSize) {
2135
+ controller.close();
2136
+ }
2137
+ }
2138
+ }
2139
+ });
2140
+ return {
2141
+ data: { stream, size: totalSize },
2142
+ error: null
2143
+ };
1562
2144
  } catch (error) {
1563
2145
  return { data: null, error };
1564
2146
  }
@@ -2065,6 +2647,7 @@ var FluxbaseJobs = class {
2065
2647
  if (filters?.namespace) params.append("namespace", filters.namespace);
2066
2648
  if (filters?.limit) params.append("limit", filters.limit.toString());
2067
2649
  if (filters?.offset) params.append("offset", filters.offset.toString());
2650
+ if (filters?.includeResult) params.append("include_result", "true");
2068
2651
  const queryString = params.toString();
2069
2652
  const data = await this.fetch.get(
2070
2653
  `/api/v1/jobs${queryString ? `?${queryString}` : ""}`
@@ -4821,6 +5404,7 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
4821
5404
  if (filters?.namespace) params.append("namespace", filters.namespace);
4822
5405
  if (filters?.limit) params.append("limit", filters.limit.toString());
4823
5406
  if (filters?.offset) params.append("offset", filters.offset.toString());
5407
+ if (filters?.includeResult) params.append("include_result", "true");
4824
5408
  const queryString = params.toString();
4825
5409
  const data = await this.fetch.get(
4826
5410
  `/api/v1/admin/jobs/queue${queryString ? `?${queryString}` : ""}`
@@ -4846,7 +5430,9 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
4846
5430
  */
4847
5431
  async getJob(jobId) {
4848
5432
  try {
4849
- const data = await this.fetch.get(`/api/v1/admin/jobs/queue/${jobId}`);
5433
+ const data = await this.fetch.get(
5434
+ `/api/v1/admin/jobs/queue/${jobId}`
5435
+ );
4850
5436
  return { data, error: null };
4851
5437
  } catch (error) {
4852
5438
  return { data: null, error };
@@ -5061,8 +5647,14 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
5061
5647
  return fn;
5062
5648
  }
5063
5649
  const bundled = await _FluxbaseAdminJobs.bundleCode({
5650
+ // Apply global bundle options first
5651
+ ...bundleOptions,
5652
+ // Then override with per-function values (these take priority)
5064
5653
  code: fn.code,
5065
- ...bundleOptions
5654
+ // Use function's sourceDir for resolving relative imports
5655
+ baseDir: fn.sourceDir || bundleOptions?.baseDir,
5656
+ // Use function's nodePaths for additional module resolution
5657
+ nodePaths: fn.nodePaths || bundleOptions?.nodePaths
5066
5658
  });
5067
5659
  return {
5068
5660
  ...fn,
@@ -5122,23 +5714,74 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
5122
5714
  "esbuild is required for bundling. Install it with: npm install esbuild"
5123
5715
  );
5124
5716
  }
5125
- const result = await esbuild.build({
5717
+ const externals = [...options.external ?? []];
5718
+ const alias = {};
5719
+ if (options.importMap) {
5720
+ for (const [key, value] of Object.entries(options.importMap)) {
5721
+ if (value.startsWith("npm:")) {
5722
+ externals.push(key);
5723
+ } else if (value.startsWith("https://") || value.startsWith("http://")) {
5724
+ externals.push(key);
5725
+ } else if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
5726
+ alias[key] = value;
5727
+ } else {
5728
+ externals.push(key);
5729
+ }
5730
+ }
5731
+ }
5732
+ const denoExternalPlugin = {
5733
+ name: "deno-external",
5734
+ setup(build) {
5735
+ build.onResolve({ filter: /^npm:/ }, (args) => ({
5736
+ path: args.path,
5737
+ external: true
5738
+ }));
5739
+ build.onResolve({ filter: /^https?:\/\// }, (args) => ({
5740
+ path: args.path,
5741
+ external: true
5742
+ }));
5743
+ build.onResolve({ filter: /^jsr:/ }, (args) => ({
5744
+ path: args.path,
5745
+ external: true
5746
+ }));
5747
+ }
5748
+ };
5749
+ const resolveDir = options.baseDir || process.cwd?.() || "/";
5750
+ const buildOptions = {
5126
5751
  stdin: {
5127
5752
  contents: options.code,
5128
5753
  loader: "ts",
5129
- resolveDir: process.cwd?.() || "/"
5754
+ resolveDir
5130
5755
  },
5756
+ // Set absWorkingDir for consistent path resolution
5757
+ absWorkingDir: resolveDir,
5131
5758
  bundle: true,
5132
5759
  write: false,
5133
5760
  format: "esm",
5134
- platform: "neutral",
5761
+ // Use 'node' platform for better node_modules resolution (Deno supports Node APIs)
5762
+ platform: "node",
5135
5763
  target: "esnext",
5136
5764
  minify: options.minify ?? false,
5137
5765
  sourcemap: options.sourcemap ? "inline" : false,
5138
- external: options.external ?? [],
5766
+ external: externals,
5767
+ plugins: [denoExternalPlugin],
5139
5768
  // Preserve handler export
5140
- treeShaking: true
5141
- });
5769
+ treeShaking: true,
5770
+ // Resolve .ts, .js, .mjs extensions
5771
+ resolveExtensions: [".ts", ".tsx", ".js", ".mjs", ".json"],
5772
+ // ESM conditions for better module resolution
5773
+ conditions: ["import", "module"]
5774
+ };
5775
+ if (Object.keys(alias).length > 0) {
5776
+ buildOptions.alias = alias;
5777
+ }
5778
+ if (options.nodePaths && options.nodePaths.length > 0) {
5779
+ buildOptions.nodePaths = options.nodePaths;
5780
+ }
5781
+ if (options.define) {
5782
+ buildOptions.define = options.define;
5783
+ }
5784
+ const result = await esbuild.build(buildOptions);
5142
5785
  const output = result.outputFiles?.[0];
5143
5786
  if (!output) {
5144
5787
  throw new Error("Bundling failed: no output generated");
@@ -5495,7 +6138,7 @@ var FluxbaseAdmin = class {
5495
6138
 
5496
6139
  // src/query-builder.ts
5497
6140
  var QueryBuilder = class {
5498
- constructor(fetch2, table) {
6141
+ constructor(fetch2, table, schema) {
5499
6142
  this.selectQuery = "*";
5500
6143
  this.filters = [];
5501
6144
  this.orFilters = [];
@@ -5504,17 +6147,33 @@ var QueryBuilder = class {
5504
6147
  this.singleRow = false;
5505
6148
  this.maybeSingleRow = false;
5506
6149
  this.operationType = "select";
6150
+ this.headOnly = false;
5507
6151
  this.fetch = fetch2;
5508
6152
  this.table = table;
6153
+ this.schema = schema;
6154
+ }
6155
+ /**
6156
+ * Build the API path for this table, including schema if specified
6157
+ */
6158
+ buildTablePath() {
6159
+ return this.schema ? `/api/v1/tables/${this.schema}/${this.table}` : `/api/v1/tables/${this.table}`;
5509
6160
  }
5510
6161
  /**
5511
6162
  * Select columns to return
5512
6163
  * @example select('*')
5513
6164
  * @example select('id, name, email')
5514
6165
  * @example select('id, name, posts(title, content)')
6166
+ * @example select('*', { count: 'exact' }) // Get exact count
6167
+ * @example select('*', { count: 'exact', head: true }) // Get count only (no data)
5515
6168
  */
5516
- select(columns = "*") {
6169
+ select(columns = "*", options) {
5517
6170
  this.selectQuery = columns;
6171
+ if (options?.count) {
6172
+ this.countType = options.count;
6173
+ }
6174
+ if (options?.head) {
6175
+ this.headOnly = true;
6176
+ }
5518
6177
  return this;
5519
6178
  }
5520
6179
  /**
@@ -5544,7 +6203,7 @@ var QueryBuilder = class {
5544
6203
  const headers = {
5545
6204
  Prefer: preferValues.join(",")
5546
6205
  };
5547
- let path = `/api/v1/tables/${this.table}`;
6206
+ let path = this.buildTablePath();
5548
6207
  if (options?.onConflict) {
5549
6208
  path += `?on_conflict=${encodeURIComponent(options.onConflict)}`;
5550
6209
  }
@@ -6109,10 +6768,7 @@ var QueryBuilder = class {
6109
6768
  throw new Error("Insert data is required for insert operation");
6110
6769
  }
6111
6770
  const body = Array.isArray(this.insertData) ? this.insertData : this.insertData;
6112
- const response = await this.fetch.post(
6113
- `/api/v1/tables/${this.table}`,
6114
- body
6115
- );
6771
+ const response = await this.fetch.post(this.buildTablePath(), body);
6116
6772
  return {
6117
6773
  data: response,
6118
6774
  error: null,
@@ -6126,7 +6782,7 @@ var QueryBuilder = class {
6126
6782
  throw new Error("Update data is required for update operation");
6127
6783
  }
6128
6784
  const queryString2 = this.buildQueryString();
6129
- const path2 = `/api/v1/tables/${this.table}${queryString2}`;
6785
+ const path2 = `${this.buildTablePath()}${queryString2}`;
6130
6786
  const response = await this.fetch.patch(path2, this.updateData);
6131
6787
  return {
6132
6788
  data: response,
@@ -6138,7 +6794,7 @@ var QueryBuilder = class {
6138
6794
  }
6139
6795
  if (this.operationType === "delete") {
6140
6796
  const queryString2 = this.buildQueryString();
6141
- const path2 = `/api/v1/tables/${this.table}${queryString2}`;
6797
+ const path2 = `${this.buildTablePath()}${queryString2}`;
6142
6798
  await this.fetch.delete(path2);
6143
6799
  return {
6144
6800
  data: null,
@@ -6149,7 +6805,66 @@ var QueryBuilder = class {
6149
6805
  };
6150
6806
  }
6151
6807
  const queryString = this.buildQueryString();
6152
- const path = `/api/v1/tables/${this.table}${queryString}`;
6808
+ const path = `${this.buildTablePath()}${queryString}`;
6809
+ if (this.countType) {
6810
+ const response = await this.fetch.getWithHeaders(path);
6811
+ const serverCount = this.parseContentRangeCount(response.headers);
6812
+ const data2 = response.data;
6813
+ if (this.headOnly) {
6814
+ return {
6815
+ data: null,
6816
+ error: null,
6817
+ count: serverCount,
6818
+ status: response.status,
6819
+ statusText: "OK"
6820
+ };
6821
+ }
6822
+ if (this.singleRow) {
6823
+ if (Array.isArray(data2) && data2.length === 0) {
6824
+ return {
6825
+ data: null,
6826
+ error: { message: "No rows found", code: "PGRST116" },
6827
+ count: serverCount ?? 0,
6828
+ status: 404,
6829
+ statusText: "Not Found"
6830
+ };
6831
+ }
6832
+ const singleData = Array.isArray(data2) ? data2[0] : data2;
6833
+ return {
6834
+ data: singleData,
6835
+ error: null,
6836
+ count: serverCount ?? 1,
6837
+ status: 200,
6838
+ statusText: "OK"
6839
+ };
6840
+ }
6841
+ if (this.maybeSingleRow) {
6842
+ if (Array.isArray(data2) && data2.length === 0) {
6843
+ return {
6844
+ data: null,
6845
+ error: null,
6846
+ count: serverCount ?? 0,
6847
+ status: 200,
6848
+ statusText: "OK"
6849
+ };
6850
+ }
6851
+ const singleData = Array.isArray(data2) ? data2[0] : data2;
6852
+ return {
6853
+ data: singleData,
6854
+ error: null,
6855
+ count: serverCount ?? 1,
6856
+ status: 200,
6857
+ statusText: "OK"
6858
+ };
6859
+ }
6860
+ return {
6861
+ data: data2,
6862
+ error: null,
6863
+ count: serverCount ?? (Array.isArray(data2) ? data2.length : null),
6864
+ status: 200,
6865
+ statusText: "OK"
6866
+ };
6867
+ }
6153
6868
  const data = await this.fetch.get(path);
6154
6869
  if (this.singleRow) {
6155
6870
  if (Array.isArray(data) && data.length === 0) {
@@ -6292,6 +7007,9 @@ var QueryBuilder = class {
6292
7007
  if (this.offsetValue !== void 0) {
6293
7008
  params.append("offset", String(this.offsetValue));
6294
7009
  }
7010
+ if (this.countType) {
7011
+ params.append("count", this.countType);
7012
+ }
6295
7013
  const queryString = params.toString();
6296
7014
  return queryString ? `?${queryString}` : "";
6297
7015
  }
@@ -6313,6 +7031,38 @@ var QueryBuilder = class {
6313
7031
  }
6314
7032
  return String(value);
6315
7033
  }
7034
+ /**
7035
+ * Parse the Content-Range header to extract the total count
7036
+ * Header format: "0-999/50000" or "* /50000" (when no rows returned)
7037
+ */
7038
+ parseContentRangeCount(headers) {
7039
+ const contentRange = headers.get("Content-Range");
7040
+ if (!contentRange) {
7041
+ return null;
7042
+ }
7043
+ const match = contentRange.match(/\/(\d+)$/);
7044
+ if (match && match[1]) {
7045
+ return parseInt(match[1], 10);
7046
+ }
7047
+ return null;
7048
+ }
7049
+ };
7050
+
7051
+ // src/schema-query-builder.ts
7052
+ var SchemaQueryBuilder = class {
7053
+ constructor(fetch2, schemaName) {
7054
+ this.fetch = fetch2;
7055
+ this.schemaName = schemaName;
7056
+ }
7057
+ /**
7058
+ * Create a query builder for a table in this schema
7059
+ *
7060
+ * @param table - The table name (without schema prefix)
7061
+ * @returns A query builder instance for constructing and executing queries
7062
+ */
7063
+ from(table) {
7064
+ return new QueryBuilder(this.fetch, table, this.schemaName);
7065
+ }
6316
7066
  };
6317
7067
 
6318
7068
  // src/client.ts
@@ -6346,6 +7096,7 @@ var FluxbaseClient = class {
6346
7096
  timeout: options?.timeout,
6347
7097
  debug: options?.debug
6348
7098
  });
7099
+ this.fetch.setAnonKey(fluxbaseKey);
6349
7100
  this.auth = new FluxbaseAuth(
6350
7101
  this.fetch,
6351
7102
  options?.auth?.autoRefresh ?? true,
@@ -6393,6 +7144,37 @@ var FluxbaseClient = class {
6393
7144
  from(table) {
6394
7145
  return new QueryBuilder(this.fetch, table);
6395
7146
  }
7147
+ /**
7148
+ * Access a specific database schema
7149
+ *
7150
+ * Use this to query tables in non-public schemas.
7151
+ *
7152
+ * @param schemaName - The schema name (e.g., 'jobs', 'analytics')
7153
+ * @returns A schema query builder for constructing queries on that schema
7154
+ *
7155
+ * @example
7156
+ * ```typescript
7157
+ * // Query the jobs.execution_logs table
7158
+ * const { data } = await client
7159
+ * .schema('jobs')
7160
+ * .from('execution_logs')
7161
+ * .select('*')
7162
+ * .eq('job_id', jobId)
7163
+ * .execute()
7164
+ *
7165
+ * // Insert into a custom schema table
7166
+ * await client
7167
+ * .schema('analytics')
7168
+ * .from('events')
7169
+ * .insert({ event_type: 'click', data: {} })
7170
+ * .execute()
7171
+ * ```
7172
+ *
7173
+ * @category Database
7174
+ */
7175
+ schema(schemaName) {
7176
+ return new SchemaQueryBuilder(this.fetch, schemaName);
7177
+ }
6396
7178
  /**
6397
7179
  * Call a PostgreSQL function (Remote Procedure Call)
6398
7180
  *
@@ -6435,6 +7217,14 @@ var FluxbaseClient = class {
6435
7217
  originalSetAuthToken(token);
6436
7218
  this.realtime.setAuth(token);
6437
7219
  };
7220
+ this.realtime.setTokenRefreshCallback(async () => {
7221
+ const result = await this.auth.refreshSession();
7222
+ if (result.error || !result.data?.session) {
7223
+ console.error("[Fluxbase] Failed to refresh token for realtime:", result.error);
7224
+ return null;
7225
+ }
7226
+ return result.data.session.access_token;
7227
+ });
6438
7228
  }
6439
7229
  /**
6440
7230
  * Get the current authentication token
@@ -6519,12 +7309,29 @@ var FluxbaseClient = class {
6519
7309
  return this.fetch;
6520
7310
  }
6521
7311
  };
7312
+ function getEnvVar(name) {
7313
+ if (typeof process !== "undefined" && process.env) {
7314
+ return process.env[name];
7315
+ }
7316
+ if (typeof Deno !== "undefined" && Deno?.env) {
7317
+ return Deno.env.get(name);
7318
+ }
7319
+ return void 0;
7320
+ }
6522
7321
  function createClient(fluxbaseUrl, fluxbaseKey, options) {
6523
- return new FluxbaseClient(
6524
- fluxbaseUrl,
6525
- fluxbaseKey,
6526
- options
6527
- );
7322
+ const url = fluxbaseUrl || getEnvVar("FLUXBASE_URL") || getEnvVar("NEXT_PUBLIC_FLUXBASE_URL") || getEnvVar("VITE_FLUXBASE_URL");
7323
+ const key = fluxbaseKey || getEnvVar("FLUXBASE_ANON_KEY") || getEnvVar("FLUXBASE_SERVICE_TOKEN") || getEnvVar("FLUXBASE_JOB_TOKEN") || getEnvVar("NEXT_PUBLIC_FLUXBASE_ANON_KEY") || getEnvVar("VITE_FLUXBASE_ANON_KEY");
7324
+ if (!url) {
7325
+ throw new Error(
7326
+ "Fluxbase URL is required. Pass it as the first argument or set FLUXBASE_URL environment variable."
7327
+ );
7328
+ }
7329
+ if (!key) {
7330
+ throw new Error(
7331
+ "Fluxbase key is required. Pass it as the second argument or set FLUXBASE_ANON_KEY environment variable."
7332
+ );
7333
+ }
7334
+ return new FluxbaseClient(url, key, options);
6528
7335
  }
6529
7336
 
6530
7337
  exports.APIKeysManager = APIKeysManager;
@@ -6551,6 +7358,7 @@ exports.InvitationsManager = InvitationsManager;
6551
7358
  exports.OAuthProviderManager = OAuthProviderManager;
6552
7359
  exports.QueryBuilder = QueryBuilder;
6553
7360
  exports.RealtimeChannel = RealtimeChannel;
7361
+ exports.SchemaQueryBuilder = SchemaQueryBuilder;
6554
7362
  exports.SettingsClient = SettingsClient;
6555
7363
  exports.StorageBucket = StorageBucket;
6556
7364
  exports.SystemSettingsManager = SystemSettingsManager;