@fluxbase/sdk 0.0.1-rc.43 → 0.0.1-rc.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +865 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +362 -9
- package/dist/index.d.ts +362 -9
- package/dist/index.js +865 -58
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
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(
|
|
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 ||
|
|
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
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1545
|
-
*
|
|
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
|
|
2002
|
+
async downloadResumable(path, options) {
|
|
1548
2003
|
try {
|
|
1549
|
-
const
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
if (
|
|
1556
|
-
|
|
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
|
-
|
|
1559
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
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
|