@fluxbase/sdk 0.0.1-rc.43 → 0.0.1-rc.44
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 +854 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +358 -7
- package/dist/index.d.ts +358 -7
- package/dist/index.js +854 -56
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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(
|
|
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 ||
|
|
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
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -1542,23 +1912,228 @@ var StorageBucket = class {
|
|
|
1542
1912
|
xhr.send(formData);
|
|
1543
1913
|
});
|
|
1544
1914
|
}
|
|
1915
|
+
async download(path, options) {
|
|
1916
|
+
try {
|
|
1917
|
+
const controller = new AbortController();
|
|
1918
|
+
let timeoutId;
|
|
1919
|
+
if (options?.signal) {
|
|
1920
|
+
if (options.signal.aborted) {
|
|
1921
|
+
return { data: null, error: new Error("Download aborted") };
|
|
1922
|
+
}
|
|
1923
|
+
options.signal.addEventListener("abort", () => controller.abort(), {
|
|
1924
|
+
once: true
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
const timeout = options?.timeout ?? (options?.stream ? 0 : 3e4);
|
|
1928
|
+
if (timeout > 0) {
|
|
1929
|
+
timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
1930
|
+
}
|
|
1931
|
+
try {
|
|
1932
|
+
const response = await fetch(
|
|
1933
|
+
`${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`,
|
|
1934
|
+
{
|
|
1935
|
+
headers: this.fetch["defaultHeaders"],
|
|
1936
|
+
signal: controller.signal
|
|
1937
|
+
}
|
|
1938
|
+
);
|
|
1939
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1940
|
+
if (!response.ok) {
|
|
1941
|
+
throw new Error(`Failed to download file: ${response.statusText}`);
|
|
1942
|
+
}
|
|
1943
|
+
if (options?.stream) {
|
|
1944
|
+
if (!response.body) {
|
|
1945
|
+
throw new Error("Response body is not available for streaming");
|
|
1946
|
+
}
|
|
1947
|
+
const contentLength = response.headers.get("content-length");
|
|
1948
|
+
const size = contentLength ? parseInt(contentLength, 10) : null;
|
|
1949
|
+
return {
|
|
1950
|
+
data: { stream: response.body, size },
|
|
1951
|
+
error: null
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
const blob = await response.blob();
|
|
1955
|
+
return { data: blob, error: null };
|
|
1956
|
+
} catch (err) {
|
|
1957
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1958
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1959
|
+
if (options?.signal?.aborted) {
|
|
1960
|
+
return { data: null, error: new Error("Download aborted") };
|
|
1961
|
+
}
|
|
1962
|
+
return { data: null, error: new Error("Download timeout") };
|
|
1963
|
+
}
|
|
1964
|
+
throw err;
|
|
1965
|
+
}
|
|
1966
|
+
} catch (error) {
|
|
1967
|
+
return { data: null, error };
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1545
1970
|
/**
|
|
1546
|
-
* Download a file
|
|
1547
|
-
*
|
|
1971
|
+
* Download a file with resumable chunked downloads for large files.
|
|
1972
|
+
* Returns a ReadableStream that abstracts the chunking internally.
|
|
1973
|
+
*
|
|
1974
|
+
* Features:
|
|
1975
|
+
* - Downloads file in chunks using HTTP Range headers
|
|
1976
|
+
* - Automatically retries failed chunks with exponential backoff
|
|
1977
|
+
* - Reports progress via callback
|
|
1978
|
+
* - Falls back to regular streaming if Range not supported
|
|
1979
|
+
*
|
|
1980
|
+
* @param path - The file path within the bucket
|
|
1981
|
+
* @param options - Download options including chunk size, retries, and progress callback
|
|
1982
|
+
* @returns A ReadableStream and file size (consumer doesn't need to know about chunking)
|
|
1983
|
+
*
|
|
1984
|
+
* @example
|
|
1985
|
+
* ```typescript
|
|
1986
|
+
* const { data, error } = await storage.from('bucket').downloadResumable('large.json', {
|
|
1987
|
+
* chunkSize: 5 * 1024 * 1024, // 5MB chunks
|
|
1988
|
+
* maxRetries: 3,
|
|
1989
|
+
* onProgress: (progress) => console.log(`${progress.percentage}% complete`)
|
|
1990
|
+
* });
|
|
1991
|
+
* if (data) {
|
|
1992
|
+
* console.log(`File size: ${data.size} bytes`);
|
|
1993
|
+
* // Process data.stream...
|
|
1994
|
+
* }
|
|
1995
|
+
* ```
|
|
1548
1996
|
*/
|
|
1549
|
-
async
|
|
1997
|
+
async downloadResumable(path, options) {
|
|
1550
1998
|
try {
|
|
1551
|
-
const
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
if (
|
|
1558
|
-
|
|
1999
|
+
const chunkSize = options?.chunkSize ?? 5 * 1024 * 1024;
|
|
2000
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
2001
|
+
const retryDelayMs = options?.retryDelayMs ?? 1e3;
|
|
2002
|
+
const chunkTimeout = options?.chunkTimeout ?? 3e4;
|
|
2003
|
+
const url = `${this.fetch["baseUrl"]}/api/v1/storage/${this.bucketName}/${path}`;
|
|
2004
|
+
const headers = this.fetch["defaultHeaders"];
|
|
2005
|
+
if (options?.signal?.aborted) {
|
|
2006
|
+
return { data: null, error: new Error("Download aborted") };
|
|
2007
|
+
}
|
|
2008
|
+
const headResponse = await fetch(url, {
|
|
2009
|
+
method: "HEAD",
|
|
2010
|
+
headers,
|
|
2011
|
+
signal: options?.signal
|
|
2012
|
+
});
|
|
2013
|
+
if (!headResponse.ok) {
|
|
2014
|
+
throw new Error(`Failed to get file info: ${headResponse.statusText}`);
|
|
2015
|
+
}
|
|
2016
|
+
const contentLength = headResponse.headers.get("content-length");
|
|
2017
|
+
const acceptRanges = headResponse.headers.get("accept-ranges");
|
|
2018
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : null;
|
|
2019
|
+
if (acceptRanges !== "bytes") {
|
|
2020
|
+
const { data, error } = await this.download(path, {
|
|
2021
|
+
stream: true,
|
|
2022
|
+
timeout: 0,
|
|
2023
|
+
signal: options?.signal
|
|
2024
|
+
});
|
|
2025
|
+
if (error) return { data: null, error };
|
|
2026
|
+
return {
|
|
2027
|
+
data,
|
|
2028
|
+
error: null
|
|
2029
|
+
};
|
|
1559
2030
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
2031
|
+
let downloadedBytes = 0;
|
|
2032
|
+
let currentChunk = 0;
|
|
2033
|
+
const totalChunks = totalSize ? Math.ceil(totalSize / chunkSize) : null;
|
|
2034
|
+
let lastProgressTime = Date.now();
|
|
2035
|
+
let lastProgressBytes = 0;
|
|
2036
|
+
const stream = new ReadableStream({
|
|
2037
|
+
async pull(controller) {
|
|
2038
|
+
if (options?.signal?.aborted) {
|
|
2039
|
+
controller.error(new Error("Download aborted"));
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (totalSize !== null && downloadedBytes >= totalSize) {
|
|
2043
|
+
controller.close();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
const rangeStart = downloadedBytes;
|
|
2047
|
+
const rangeEnd = totalSize !== null ? Math.min(downloadedBytes + chunkSize - 1, totalSize - 1) : downloadedBytes + chunkSize - 1;
|
|
2048
|
+
let retryCount = 0;
|
|
2049
|
+
let chunk = null;
|
|
2050
|
+
while (retryCount <= maxRetries && chunk === null) {
|
|
2051
|
+
try {
|
|
2052
|
+
if (options?.signal?.aborted) {
|
|
2053
|
+
controller.error(new Error("Download aborted"));
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
const chunkController = new AbortController();
|
|
2057
|
+
const timeoutId = setTimeout(
|
|
2058
|
+
() => chunkController.abort(),
|
|
2059
|
+
chunkTimeout
|
|
2060
|
+
);
|
|
2061
|
+
if (options?.signal) {
|
|
2062
|
+
options.signal.addEventListener(
|
|
2063
|
+
"abort",
|
|
2064
|
+
() => chunkController.abort(),
|
|
2065
|
+
{ once: true }
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
const chunkResponse = await fetch(url, {
|
|
2069
|
+
headers: {
|
|
2070
|
+
...headers,
|
|
2071
|
+
Range: `bytes=${rangeStart}-${rangeEnd}`
|
|
2072
|
+
},
|
|
2073
|
+
signal: chunkController.signal
|
|
2074
|
+
});
|
|
2075
|
+
clearTimeout(timeoutId);
|
|
2076
|
+
if (!chunkResponse.ok && chunkResponse.status !== 206) {
|
|
2077
|
+
throw new Error(
|
|
2078
|
+
`Chunk download failed: ${chunkResponse.statusText}`
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
const arrayBuffer = await chunkResponse.arrayBuffer();
|
|
2082
|
+
chunk = new Uint8Array(arrayBuffer);
|
|
2083
|
+
if (totalSize === null && chunk.byteLength < chunkSize) {
|
|
2084
|
+
downloadedBytes += chunk.byteLength;
|
|
2085
|
+
currentChunk++;
|
|
2086
|
+
controller.enqueue(chunk);
|
|
2087
|
+
controller.close();
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
} catch (err) {
|
|
2091
|
+
if (options?.signal?.aborted) {
|
|
2092
|
+
controller.error(new Error("Download aborted"));
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
retryCount++;
|
|
2096
|
+
if (retryCount > maxRetries) {
|
|
2097
|
+
controller.error(
|
|
2098
|
+
new Error(
|
|
2099
|
+
`Failed to download chunk after ${maxRetries} retries`
|
|
2100
|
+
)
|
|
2101
|
+
);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const delay = retryDelayMs * Math.pow(2, retryCount - 1);
|
|
2105
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
if (chunk) {
|
|
2109
|
+
downloadedBytes += chunk.byteLength;
|
|
2110
|
+
currentChunk++;
|
|
2111
|
+
if (options?.onProgress) {
|
|
2112
|
+
const now = Date.now();
|
|
2113
|
+
const elapsed = (now - lastProgressTime) / 1e3;
|
|
2114
|
+
const bytesPerSecond = elapsed > 0 ? (downloadedBytes - lastProgressBytes) / elapsed : 0;
|
|
2115
|
+
lastProgressTime = now;
|
|
2116
|
+
lastProgressBytes = downloadedBytes;
|
|
2117
|
+
options.onProgress({
|
|
2118
|
+
loaded: downloadedBytes,
|
|
2119
|
+
total: totalSize,
|
|
2120
|
+
percentage: totalSize ? Math.round(downloadedBytes / totalSize * 100) : null,
|
|
2121
|
+
currentChunk,
|
|
2122
|
+
totalChunks,
|
|
2123
|
+
bytesPerSecond
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
controller.enqueue(chunk);
|
|
2127
|
+
if (totalSize !== null && downloadedBytes >= totalSize) {
|
|
2128
|
+
controller.close();
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
return {
|
|
2134
|
+
data: { stream, size: totalSize },
|
|
2135
|
+
error: null
|
|
2136
|
+
};
|
|
1562
2137
|
} catch (error) {
|
|
1563
2138
|
return { data: null, error };
|
|
1564
2139
|
}
|
|
@@ -4846,7 +5421,9 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
|
|
|
4846
5421
|
*/
|
|
4847
5422
|
async getJob(jobId) {
|
|
4848
5423
|
try {
|
|
4849
|
-
const data = await this.fetch.get(
|
|
5424
|
+
const data = await this.fetch.get(
|
|
5425
|
+
`/api/v1/admin/jobs/queue/${jobId}`
|
|
5426
|
+
);
|
|
4850
5427
|
return { data, error: null };
|
|
4851
5428
|
} catch (error) {
|
|
4852
5429
|
return { data: null, error };
|
|
@@ -5061,8 +5638,14 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
|
|
|
5061
5638
|
return fn;
|
|
5062
5639
|
}
|
|
5063
5640
|
const bundled = await _FluxbaseAdminJobs.bundleCode({
|
|
5641
|
+
// Apply global bundle options first
|
|
5642
|
+
...bundleOptions,
|
|
5643
|
+
// Then override with per-function values (these take priority)
|
|
5064
5644
|
code: fn.code,
|
|
5065
|
-
|
|
5645
|
+
// Use function's sourceDir for resolving relative imports
|
|
5646
|
+
baseDir: fn.sourceDir || bundleOptions?.baseDir,
|
|
5647
|
+
// Use function's nodePaths for additional module resolution
|
|
5648
|
+
nodePaths: fn.nodePaths || bundleOptions?.nodePaths
|
|
5066
5649
|
});
|
|
5067
5650
|
return {
|
|
5068
5651
|
...fn,
|
|
@@ -5122,23 +5705,74 @@ var FluxbaseAdminJobs = class _FluxbaseAdminJobs {
|
|
|
5122
5705
|
"esbuild is required for bundling. Install it with: npm install esbuild"
|
|
5123
5706
|
);
|
|
5124
5707
|
}
|
|
5125
|
-
const
|
|
5708
|
+
const externals = [...options.external ?? []];
|
|
5709
|
+
const alias = {};
|
|
5710
|
+
if (options.importMap) {
|
|
5711
|
+
for (const [key, value] of Object.entries(options.importMap)) {
|
|
5712
|
+
if (value.startsWith("npm:")) {
|
|
5713
|
+
externals.push(key);
|
|
5714
|
+
} else if (value.startsWith("https://") || value.startsWith("http://")) {
|
|
5715
|
+
externals.push(key);
|
|
5716
|
+
} else if (value.startsWith("/") || value.startsWith("./") || value.startsWith("../")) {
|
|
5717
|
+
alias[key] = value;
|
|
5718
|
+
} else {
|
|
5719
|
+
externals.push(key);
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
}
|
|
5723
|
+
const denoExternalPlugin = {
|
|
5724
|
+
name: "deno-external",
|
|
5725
|
+
setup(build) {
|
|
5726
|
+
build.onResolve({ filter: /^npm:/ }, (args) => ({
|
|
5727
|
+
path: args.path,
|
|
5728
|
+
external: true
|
|
5729
|
+
}));
|
|
5730
|
+
build.onResolve({ filter: /^https?:\/\// }, (args) => ({
|
|
5731
|
+
path: args.path,
|
|
5732
|
+
external: true
|
|
5733
|
+
}));
|
|
5734
|
+
build.onResolve({ filter: /^jsr:/ }, (args) => ({
|
|
5735
|
+
path: args.path,
|
|
5736
|
+
external: true
|
|
5737
|
+
}));
|
|
5738
|
+
}
|
|
5739
|
+
};
|
|
5740
|
+
const resolveDir = options.baseDir || process.cwd?.() || "/";
|
|
5741
|
+
const buildOptions = {
|
|
5126
5742
|
stdin: {
|
|
5127
5743
|
contents: options.code,
|
|
5128
5744
|
loader: "ts",
|
|
5129
|
-
resolveDir
|
|
5745
|
+
resolveDir
|
|
5130
5746
|
},
|
|
5747
|
+
// Set absWorkingDir for consistent path resolution
|
|
5748
|
+
absWorkingDir: resolveDir,
|
|
5131
5749
|
bundle: true,
|
|
5132
5750
|
write: false,
|
|
5133
5751
|
format: "esm",
|
|
5134
|
-
platform
|
|
5752
|
+
// Use 'node' platform for better node_modules resolution (Deno supports Node APIs)
|
|
5753
|
+
platform: "node",
|
|
5135
5754
|
target: "esnext",
|
|
5136
5755
|
minify: options.minify ?? false,
|
|
5137
5756
|
sourcemap: options.sourcemap ? "inline" : false,
|
|
5138
|
-
external:
|
|
5757
|
+
external: externals,
|
|
5758
|
+
plugins: [denoExternalPlugin],
|
|
5139
5759
|
// Preserve handler export
|
|
5140
|
-
treeShaking: true
|
|
5141
|
-
|
|
5760
|
+
treeShaking: true,
|
|
5761
|
+
// Resolve .ts, .js, .mjs extensions
|
|
5762
|
+
resolveExtensions: [".ts", ".tsx", ".js", ".mjs", ".json"],
|
|
5763
|
+
// ESM conditions for better module resolution
|
|
5764
|
+
conditions: ["import", "module"]
|
|
5765
|
+
};
|
|
5766
|
+
if (Object.keys(alias).length > 0) {
|
|
5767
|
+
buildOptions.alias = alias;
|
|
5768
|
+
}
|
|
5769
|
+
if (options.nodePaths && options.nodePaths.length > 0) {
|
|
5770
|
+
buildOptions.nodePaths = options.nodePaths;
|
|
5771
|
+
}
|
|
5772
|
+
if (options.define) {
|
|
5773
|
+
buildOptions.define = options.define;
|
|
5774
|
+
}
|
|
5775
|
+
const result = await esbuild.build(buildOptions);
|
|
5142
5776
|
const output = result.outputFiles?.[0];
|
|
5143
5777
|
if (!output) {
|
|
5144
5778
|
throw new Error("Bundling failed: no output generated");
|
|
@@ -5495,7 +6129,7 @@ var FluxbaseAdmin = class {
|
|
|
5495
6129
|
|
|
5496
6130
|
// src/query-builder.ts
|
|
5497
6131
|
var QueryBuilder = class {
|
|
5498
|
-
constructor(fetch2, table) {
|
|
6132
|
+
constructor(fetch2, table, schema) {
|
|
5499
6133
|
this.selectQuery = "*";
|
|
5500
6134
|
this.filters = [];
|
|
5501
6135
|
this.orFilters = [];
|
|
@@ -5504,17 +6138,33 @@ var QueryBuilder = class {
|
|
|
5504
6138
|
this.singleRow = false;
|
|
5505
6139
|
this.maybeSingleRow = false;
|
|
5506
6140
|
this.operationType = "select";
|
|
6141
|
+
this.headOnly = false;
|
|
5507
6142
|
this.fetch = fetch2;
|
|
5508
6143
|
this.table = table;
|
|
6144
|
+
this.schema = schema;
|
|
6145
|
+
}
|
|
6146
|
+
/**
|
|
6147
|
+
* Build the API path for this table, including schema if specified
|
|
6148
|
+
*/
|
|
6149
|
+
buildTablePath() {
|
|
6150
|
+
return this.schema ? `/api/v1/tables/${this.schema}/${this.table}` : `/api/v1/tables/${this.table}`;
|
|
5509
6151
|
}
|
|
5510
6152
|
/**
|
|
5511
6153
|
* Select columns to return
|
|
5512
6154
|
* @example select('*')
|
|
5513
6155
|
* @example select('id, name, email')
|
|
5514
6156
|
* @example select('id, name, posts(title, content)')
|
|
6157
|
+
* @example select('*', { count: 'exact' }) // Get exact count
|
|
6158
|
+
* @example select('*', { count: 'exact', head: true }) // Get count only (no data)
|
|
5515
6159
|
*/
|
|
5516
|
-
select(columns = "*") {
|
|
6160
|
+
select(columns = "*", options) {
|
|
5517
6161
|
this.selectQuery = columns;
|
|
6162
|
+
if (options?.count) {
|
|
6163
|
+
this.countType = options.count;
|
|
6164
|
+
}
|
|
6165
|
+
if (options?.head) {
|
|
6166
|
+
this.headOnly = true;
|
|
6167
|
+
}
|
|
5518
6168
|
return this;
|
|
5519
6169
|
}
|
|
5520
6170
|
/**
|
|
@@ -5544,7 +6194,7 @@ var QueryBuilder = class {
|
|
|
5544
6194
|
const headers = {
|
|
5545
6195
|
Prefer: preferValues.join(",")
|
|
5546
6196
|
};
|
|
5547
|
-
let path =
|
|
6197
|
+
let path = this.buildTablePath();
|
|
5548
6198
|
if (options?.onConflict) {
|
|
5549
6199
|
path += `?on_conflict=${encodeURIComponent(options.onConflict)}`;
|
|
5550
6200
|
}
|
|
@@ -6109,10 +6759,7 @@ var QueryBuilder = class {
|
|
|
6109
6759
|
throw new Error("Insert data is required for insert operation");
|
|
6110
6760
|
}
|
|
6111
6761
|
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
|
-
);
|
|
6762
|
+
const response = await this.fetch.post(this.buildTablePath(), body);
|
|
6116
6763
|
return {
|
|
6117
6764
|
data: response,
|
|
6118
6765
|
error: null,
|
|
@@ -6126,7 +6773,7 @@ var QueryBuilder = class {
|
|
|
6126
6773
|
throw new Error("Update data is required for update operation");
|
|
6127
6774
|
}
|
|
6128
6775
|
const queryString2 = this.buildQueryString();
|
|
6129
|
-
const path2 =
|
|
6776
|
+
const path2 = `${this.buildTablePath()}${queryString2}`;
|
|
6130
6777
|
const response = await this.fetch.patch(path2, this.updateData);
|
|
6131
6778
|
return {
|
|
6132
6779
|
data: response,
|
|
@@ -6138,7 +6785,7 @@ var QueryBuilder = class {
|
|
|
6138
6785
|
}
|
|
6139
6786
|
if (this.operationType === "delete") {
|
|
6140
6787
|
const queryString2 = this.buildQueryString();
|
|
6141
|
-
const path2 =
|
|
6788
|
+
const path2 = `${this.buildTablePath()}${queryString2}`;
|
|
6142
6789
|
await this.fetch.delete(path2);
|
|
6143
6790
|
return {
|
|
6144
6791
|
data: null,
|
|
@@ -6149,7 +6796,66 @@ var QueryBuilder = class {
|
|
|
6149
6796
|
};
|
|
6150
6797
|
}
|
|
6151
6798
|
const queryString = this.buildQueryString();
|
|
6152
|
-
const path =
|
|
6799
|
+
const path = `${this.buildTablePath()}${queryString}`;
|
|
6800
|
+
if (this.countType) {
|
|
6801
|
+
const response = await this.fetch.getWithHeaders(path);
|
|
6802
|
+
const serverCount = this.parseContentRangeCount(response.headers);
|
|
6803
|
+
const data2 = response.data;
|
|
6804
|
+
if (this.headOnly) {
|
|
6805
|
+
return {
|
|
6806
|
+
data: null,
|
|
6807
|
+
error: null,
|
|
6808
|
+
count: serverCount,
|
|
6809
|
+
status: response.status,
|
|
6810
|
+
statusText: "OK"
|
|
6811
|
+
};
|
|
6812
|
+
}
|
|
6813
|
+
if (this.singleRow) {
|
|
6814
|
+
if (Array.isArray(data2) && data2.length === 0) {
|
|
6815
|
+
return {
|
|
6816
|
+
data: null,
|
|
6817
|
+
error: { message: "No rows found", code: "PGRST116" },
|
|
6818
|
+
count: serverCount ?? 0,
|
|
6819
|
+
status: 404,
|
|
6820
|
+
statusText: "Not Found"
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
const singleData = Array.isArray(data2) ? data2[0] : data2;
|
|
6824
|
+
return {
|
|
6825
|
+
data: singleData,
|
|
6826
|
+
error: null,
|
|
6827
|
+
count: serverCount ?? 1,
|
|
6828
|
+
status: 200,
|
|
6829
|
+
statusText: "OK"
|
|
6830
|
+
};
|
|
6831
|
+
}
|
|
6832
|
+
if (this.maybeSingleRow) {
|
|
6833
|
+
if (Array.isArray(data2) && data2.length === 0) {
|
|
6834
|
+
return {
|
|
6835
|
+
data: null,
|
|
6836
|
+
error: null,
|
|
6837
|
+
count: serverCount ?? 0,
|
|
6838
|
+
status: 200,
|
|
6839
|
+
statusText: "OK"
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
const singleData = Array.isArray(data2) ? data2[0] : data2;
|
|
6843
|
+
return {
|
|
6844
|
+
data: singleData,
|
|
6845
|
+
error: null,
|
|
6846
|
+
count: serverCount ?? 1,
|
|
6847
|
+
status: 200,
|
|
6848
|
+
statusText: "OK"
|
|
6849
|
+
};
|
|
6850
|
+
}
|
|
6851
|
+
return {
|
|
6852
|
+
data: data2,
|
|
6853
|
+
error: null,
|
|
6854
|
+
count: serverCount ?? (Array.isArray(data2) ? data2.length : null),
|
|
6855
|
+
status: 200,
|
|
6856
|
+
statusText: "OK"
|
|
6857
|
+
};
|
|
6858
|
+
}
|
|
6153
6859
|
const data = await this.fetch.get(path);
|
|
6154
6860
|
if (this.singleRow) {
|
|
6155
6861
|
if (Array.isArray(data) && data.length === 0) {
|
|
@@ -6292,6 +6998,9 @@ var QueryBuilder = class {
|
|
|
6292
6998
|
if (this.offsetValue !== void 0) {
|
|
6293
6999
|
params.append("offset", String(this.offsetValue));
|
|
6294
7000
|
}
|
|
7001
|
+
if (this.countType) {
|
|
7002
|
+
params.append("count", this.countType);
|
|
7003
|
+
}
|
|
6295
7004
|
const queryString = params.toString();
|
|
6296
7005
|
return queryString ? `?${queryString}` : "";
|
|
6297
7006
|
}
|
|
@@ -6313,6 +7022,38 @@ var QueryBuilder = class {
|
|
|
6313
7022
|
}
|
|
6314
7023
|
return String(value);
|
|
6315
7024
|
}
|
|
7025
|
+
/**
|
|
7026
|
+
* Parse the Content-Range header to extract the total count
|
|
7027
|
+
* Header format: "0-999/50000" or "* /50000" (when no rows returned)
|
|
7028
|
+
*/
|
|
7029
|
+
parseContentRangeCount(headers) {
|
|
7030
|
+
const contentRange = headers.get("Content-Range");
|
|
7031
|
+
if (!contentRange) {
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
const match = contentRange.match(/\/(\d+)$/);
|
|
7035
|
+
if (match && match[1]) {
|
|
7036
|
+
return parseInt(match[1], 10);
|
|
7037
|
+
}
|
|
7038
|
+
return null;
|
|
7039
|
+
}
|
|
7040
|
+
};
|
|
7041
|
+
|
|
7042
|
+
// src/schema-query-builder.ts
|
|
7043
|
+
var SchemaQueryBuilder = class {
|
|
7044
|
+
constructor(fetch2, schemaName) {
|
|
7045
|
+
this.fetch = fetch2;
|
|
7046
|
+
this.schemaName = schemaName;
|
|
7047
|
+
}
|
|
7048
|
+
/**
|
|
7049
|
+
* Create a query builder for a table in this schema
|
|
7050
|
+
*
|
|
7051
|
+
* @param table - The table name (without schema prefix)
|
|
7052
|
+
* @returns A query builder instance for constructing and executing queries
|
|
7053
|
+
*/
|
|
7054
|
+
from(table) {
|
|
7055
|
+
return new QueryBuilder(this.fetch, table, this.schemaName);
|
|
7056
|
+
}
|
|
6316
7057
|
};
|
|
6317
7058
|
|
|
6318
7059
|
// src/client.ts
|
|
@@ -6346,6 +7087,7 @@ var FluxbaseClient = class {
|
|
|
6346
7087
|
timeout: options?.timeout,
|
|
6347
7088
|
debug: options?.debug
|
|
6348
7089
|
});
|
|
7090
|
+
this.fetch.setAnonKey(fluxbaseKey);
|
|
6349
7091
|
this.auth = new FluxbaseAuth(
|
|
6350
7092
|
this.fetch,
|
|
6351
7093
|
options?.auth?.autoRefresh ?? true,
|
|
@@ -6393,6 +7135,37 @@ var FluxbaseClient = class {
|
|
|
6393
7135
|
from(table) {
|
|
6394
7136
|
return new QueryBuilder(this.fetch, table);
|
|
6395
7137
|
}
|
|
7138
|
+
/**
|
|
7139
|
+
* Access a specific database schema
|
|
7140
|
+
*
|
|
7141
|
+
* Use this to query tables in non-public schemas.
|
|
7142
|
+
*
|
|
7143
|
+
* @param schemaName - The schema name (e.g., 'jobs', 'analytics')
|
|
7144
|
+
* @returns A schema query builder for constructing queries on that schema
|
|
7145
|
+
*
|
|
7146
|
+
* @example
|
|
7147
|
+
* ```typescript
|
|
7148
|
+
* // Query the jobs.execution_logs table
|
|
7149
|
+
* const { data } = await client
|
|
7150
|
+
* .schema('jobs')
|
|
7151
|
+
* .from('execution_logs')
|
|
7152
|
+
* .select('*')
|
|
7153
|
+
* .eq('job_id', jobId)
|
|
7154
|
+
* .execute()
|
|
7155
|
+
*
|
|
7156
|
+
* // Insert into a custom schema table
|
|
7157
|
+
* await client
|
|
7158
|
+
* .schema('analytics')
|
|
7159
|
+
* .from('events')
|
|
7160
|
+
* .insert({ event_type: 'click', data: {} })
|
|
7161
|
+
* .execute()
|
|
7162
|
+
* ```
|
|
7163
|
+
*
|
|
7164
|
+
* @category Database
|
|
7165
|
+
*/
|
|
7166
|
+
schema(schemaName) {
|
|
7167
|
+
return new SchemaQueryBuilder(this.fetch, schemaName);
|
|
7168
|
+
}
|
|
6396
7169
|
/**
|
|
6397
7170
|
* Call a PostgreSQL function (Remote Procedure Call)
|
|
6398
7171
|
*
|
|
@@ -6435,6 +7208,14 @@ var FluxbaseClient = class {
|
|
|
6435
7208
|
originalSetAuthToken(token);
|
|
6436
7209
|
this.realtime.setAuth(token);
|
|
6437
7210
|
};
|
|
7211
|
+
this.realtime.setTokenRefreshCallback(async () => {
|
|
7212
|
+
const result = await this.auth.refreshSession();
|
|
7213
|
+
if (result.error || !result.data?.session) {
|
|
7214
|
+
console.error("[Fluxbase] Failed to refresh token for realtime:", result.error);
|
|
7215
|
+
return null;
|
|
7216
|
+
}
|
|
7217
|
+
return result.data.session.access_token;
|
|
7218
|
+
});
|
|
6438
7219
|
}
|
|
6439
7220
|
/**
|
|
6440
7221
|
* Get the current authentication token
|
|
@@ -6519,12 +7300,29 @@ var FluxbaseClient = class {
|
|
|
6519
7300
|
return this.fetch;
|
|
6520
7301
|
}
|
|
6521
7302
|
};
|
|
7303
|
+
function getEnvVar(name) {
|
|
7304
|
+
if (typeof process !== "undefined" && process.env) {
|
|
7305
|
+
return process.env[name];
|
|
7306
|
+
}
|
|
7307
|
+
if (typeof Deno !== "undefined" && Deno?.env) {
|
|
7308
|
+
return Deno.env.get(name);
|
|
7309
|
+
}
|
|
7310
|
+
return void 0;
|
|
7311
|
+
}
|
|
6522
7312
|
function createClient(fluxbaseUrl, fluxbaseKey, options) {
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
7313
|
+
const url = fluxbaseUrl || getEnvVar("FLUXBASE_URL") || getEnvVar("NEXT_PUBLIC_FLUXBASE_URL") || getEnvVar("VITE_FLUXBASE_URL");
|
|
7314
|
+
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");
|
|
7315
|
+
if (!url) {
|
|
7316
|
+
throw new Error(
|
|
7317
|
+
"Fluxbase URL is required. Pass it as the first argument or set FLUXBASE_URL environment variable."
|
|
7318
|
+
);
|
|
7319
|
+
}
|
|
7320
|
+
if (!key) {
|
|
7321
|
+
throw new Error(
|
|
7322
|
+
"Fluxbase key is required. Pass it as the second argument or set FLUXBASE_ANON_KEY environment variable."
|
|
7323
|
+
);
|
|
7324
|
+
}
|
|
7325
|
+
return new FluxbaseClient(url, key, options);
|
|
6528
7326
|
}
|
|
6529
7327
|
|
|
6530
7328
|
exports.APIKeysManager = APIKeysManager;
|
|
@@ -6551,6 +7349,7 @@ exports.InvitationsManager = InvitationsManager;
|
|
|
6551
7349
|
exports.OAuthProviderManager = OAuthProviderManager;
|
|
6552
7350
|
exports.QueryBuilder = QueryBuilder;
|
|
6553
7351
|
exports.RealtimeChannel = RealtimeChannel;
|
|
7352
|
+
exports.SchemaQueryBuilder = SchemaQueryBuilder;
|
|
6554
7353
|
exports.SettingsClient = SettingsClient;
|
|
6555
7354
|
exports.StorageBucket = StorageBucket;
|
|
6556
7355
|
exports.SystemSettingsManager = SystemSettingsManager;
|