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