@rebasepro/client 0.5.0 → 0.6.0
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/admin.d.ts +2 -10
- package/dist/auth.d.ts +2 -1
- package/dist/index.d.ts +25 -17
- package/dist/index.es.js +2079 -2378
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2160 -2426
- package/dist/index.umd.js.map +1 -1
- package/dist/websocket.d.ts +9 -6
- package/package.json +13 -13
- package/src/admin.ts +3 -10
- package/src/auth.ts +9 -4
- package/src/collection.test.ts +11 -6
- package/src/functions.ts +2 -2
- package/src/index.ts +34 -22
- package/src/websocket.ts +121 -90
package/dist/index.es.js
CHANGED
|
@@ -1,2416 +1,2117 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { QueryBuilder } from "@rebasepro/common";
|
|
3
|
-
import { QueryBuilder as QueryBuilder2, and, cond, or } from "@rebasepro/common";
|
|
1
|
+
import { EntityReference, EntityRelation, GeoPoint, Vector } from "@rebasepro/types";
|
|
2
|
+
import { QueryBuilder, and, cond, or } from "@rebasepro/common";
|
|
4
3
|
import { toSnakeCase } from "@rebasepro/utils";
|
|
4
|
+
//#region src/reviver.ts
|
|
5
5
|
function rebaseReviver(_key, value) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
record.data
|
|
31
|
-
);
|
|
32
|
-
case "GeoPoint":
|
|
33
|
-
return new GeoPoint(record.latitude, record.longitude);
|
|
34
|
-
case "Vector":
|
|
35
|
-
return new Vector(record.value);
|
|
36
|
-
default:
|
|
37
|
-
return value;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return value;
|
|
6
|
+
if (value && typeof value === "object" && "__type" in value) {
|
|
7
|
+
const record = value;
|
|
8
|
+
switch (record.__type) {
|
|
9
|
+
case "date":
|
|
10
|
+
case "Date": {
|
|
11
|
+
if (typeof record.value !== "string") return value;
|
|
12
|
+
const date = new Date(record.value);
|
|
13
|
+
return isNaN(date.getTime()) ? null : date;
|
|
14
|
+
}
|
|
15
|
+
case "reference":
|
|
16
|
+
case "EntityReference": return new EntityReference({
|
|
17
|
+
id: String(record.id),
|
|
18
|
+
path: record.path,
|
|
19
|
+
driver: record.driver,
|
|
20
|
+
databaseId: record.databaseId
|
|
21
|
+
});
|
|
22
|
+
case "relation":
|
|
23
|
+
case "EntityRelation": return new EntityRelation(record.id, record.path, record.data);
|
|
24
|
+
case "GeoPoint": return new GeoPoint(record.latitude, record.longitude);
|
|
25
|
+
case "Vector": return new Vector(record.value);
|
|
26
|
+
default: return value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
41
30
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/transport.ts
|
|
33
|
+
var RebaseApiError = class extends Error {
|
|
34
|
+
status;
|
|
35
|
+
code;
|
|
36
|
+
details;
|
|
37
|
+
constructor(status, message, code, details) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "RebaseApiError";
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.code = code;
|
|
42
|
+
this.details = details;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Maps a short operator alias to the PostgREST-style short code.
|
|
47
|
+
*/
|
|
48
|
+
var OP_MAP = {
|
|
49
|
+
"==": "eq",
|
|
50
|
+
"!=": "neq",
|
|
51
|
+
">": "gt",
|
|
52
|
+
">=": "gte",
|
|
53
|
+
"<": "lt",
|
|
54
|
+
"<=": "lte",
|
|
55
|
+
"not-in": "nin",
|
|
56
|
+
"array-contains": "cs",
|
|
57
|
+
"array-contains-any": "csa"
|
|
64
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Normalise a single `WhereFieldValue` into the PostgREST query-string
|
|
61
|
+
* representation the backend expects.
|
|
62
|
+
*
|
|
63
|
+
* Supports:
|
|
64
|
+
* - `null` → `"eq.null"`
|
|
65
|
+
* - `true`/`false` → `"eq.true"` / `"eq.false"`
|
|
66
|
+
* - `42` → `"42"` (plain equality)
|
|
67
|
+
* - `"active"` → `"active"` (plain equality, backward-compat)
|
|
68
|
+
* - `"gte.18"` → `"gte.18"` (pass-through PostgREST string)
|
|
69
|
+
* - `[">=", 18]` → `"gte.18"` (tuple syntax)
|
|
70
|
+
* - `["in", [1,2]]` → `"in.(1,2)"` (tuple with array value)
|
|
71
|
+
* - `["!=", null]` → `"neq.null"`
|
|
72
|
+
*/
|
|
65
73
|
function normalizeWhereValue(value) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return String(value);
|
|
74
|
+
if (value === null) return "eq.null";
|
|
75
|
+
if (typeof value === "boolean") return `eq.${value}`;
|
|
76
|
+
if (typeof value === "number") return String(value);
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
const [rawOp, val] = (Array.isArray(value[0]) ? value : [value])[0] || [];
|
|
79
|
+
if (rawOp) {
|
|
80
|
+
const op = OP_MAP[rawOp] ?? rawOp;
|
|
81
|
+
if (val === null) return `${op}.null`;
|
|
82
|
+
if (Array.isArray(val)) return `${op}.(${val.join(",")})`;
|
|
83
|
+
return `${op}.${val}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return String(value);
|
|
80
87
|
}
|
|
81
|
-
function serializeLogicalCondition(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
return `${cond2.column}.${op}.${formattedValue}`;
|
|
94
|
-
}
|
|
88
|
+
function serializeLogicalCondition(cond) {
|
|
89
|
+
if ("type" in cond) {
|
|
90
|
+
const sub = cond.conditions.map(serializeLogicalCondition).join(",");
|
|
91
|
+
return `${cond.type}(${sub})`;
|
|
92
|
+
} else {
|
|
93
|
+
const op = OP_MAP[cond.operator] ?? cond.operator;
|
|
94
|
+
let formattedValue = cond.value;
|
|
95
|
+
if (Array.isArray(cond.value)) formattedValue = `(${cond.value.join(",")})`;
|
|
96
|
+
else if (cond.value === null) formattedValue = "null";
|
|
97
|
+
return `${cond.column}.${op}.${formattedValue}`;
|
|
98
|
+
}
|
|
95
99
|
}
|
|
96
100
|
function buildQueryString(params) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
for (const subVal of value) {
|
|
120
|
-
const normalized = normalizeWhereValue(subVal);
|
|
121
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
122
|
-
}
|
|
123
|
-
} else {
|
|
124
|
-
const normalized = normalizeWhereValue(value);
|
|
125
|
-
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return parts.length > 0 ? "?" + parts.join("&") : "";
|
|
101
|
+
if (!params) return "";
|
|
102
|
+
const parts = [];
|
|
103
|
+
if (params.limit != null) parts.push(`limit=${params.limit}`);
|
|
104
|
+
if (params.offset != null) parts.push(`offset=${params.offset}`);
|
|
105
|
+
if (params.page != null) parts.push(`page=${params.page}`);
|
|
106
|
+
if (params.orderBy) parts.push(`orderBy=${encodeURIComponent(params.orderBy)}`);
|
|
107
|
+
if (params.searchString) parts.push(`searchString=${encodeURIComponent(params.searchString)}`);
|
|
108
|
+
if (params.include && params.include.length > 0) parts.push(`include=${encodeURIComponent(params.include.join(","))}`);
|
|
109
|
+
if (params.logical) {
|
|
110
|
+
const root = params.logical;
|
|
111
|
+
const serialized = root.conditions.map(serializeLogicalCondition).join(",");
|
|
112
|
+
parts.push(`${root.type}=${encodeURIComponent(`(${serialized})`)}`);
|
|
113
|
+
}
|
|
114
|
+
if (params.where) for (const [field, value] of Object.entries(params.where)) if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) for (const subVal of value) {
|
|
115
|
+
const normalized = normalizeWhereValue(subVal);
|
|
116
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const normalized = normalizeWhereValue(value);
|
|
120
|
+
parts.push(`${encodeURIComponent(field)}=${encodeURIComponent(normalized)}`);
|
|
121
|
+
}
|
|
122
|
+
return parts.length > 0 ? "?" + parts.join("&") : "";
|
|
130
123
|
}
|
|
131
124
|
function createTransport(config) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
throw new RebaseApiError(
|
|
231
|
-
res.status,
|
|
232
|
-
String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`),
|
|
233
|
-
getErrorField(body, "code"),
|
|
234
|
-
getErrorField(body, "details")
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
return body;
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
request,
|
|
241
|
-
setToken(newToken) {
|
|
242
|
-
token = newToken || void 0;
|
|
243
|
-
},
|
|
244
|
-
setAuthTokenGetter(getter) {
|
|
245
|
-
tokenGetter = getter;
|
|
246
|
-
},
|
|
247
|
-
setOnUnauthorized(handler) {
|
|
248
|
-
onUnauthorizedHandler = handler;
|
|
249
|
-
},
|
|
250
|
-
get baseUrl() {
|
|
251
|
-
return config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "";
|
|
252
|
-
},
|
|
253
|
-
get apiPath() {
|
|
254
|
-
return apiPath;
|
|
255
|
-
},
|
|
256
|
-
get fetchFn() {
|
|
257
|
-
return fetchFn;
|
|
258
|
-
},
|
|
259
|
-
getHeaders: (init) => getHeaders(token, init),
|
|
260
|
-
resolveToken: async () => {
|
|
261
|
-
if (tokenGetter) {
|
|
262
|
-
try {
|
|
263
|
-
const fetched = await tokenGetter();
|
|
264
|
-
if (fetched !== null && fetched !== void 0) {
|
|
265
|
-
return fetched;
|
|
266
|
-
}
|
|
267
|
-
} catch (e) {
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return token || null;
|
|
271
|
-
}
|
|
272
|
-
};
|
|
125
|
+
const fetchFn = config.fetch || globalThis.fetch;
|
|
126
|
+
const apiPath = config.apiPath || "/api";
|
|
127
|
+
let token = config.token;
|
|
128
|
+
let tokenGetter;
|
|
129
|
+
let onUnauthorizedHandler = config.onUnauthorized;
|
|
130
|
+
function getHeaders(activeToken, init) {
|
|
131
|
+
return {
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
...activeToken ? { Authorization: `Bearer ${activeToken}` } : {},
|
|
134
|
+
...init?.headers || {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function request(path, init) {
|
|
138
|
+
const url = (config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "") + apiPath + path;
|
|
139
|
+
let activeToken = token;
|
|
140
|
+
if (tokenGetter) try {
|
|
141
|
+
const fetched = await tokenGetter();
|
|
142
|
+
if (fetched !== null && fetched !== void 0) activeToken = fetched;
|
|
143
|
+
} catch (e) {}
|
|
144
|
+
const headers = getHeaders(activeToken, init);
|
|
145
|
+
if (init?.body instanceof FormData) delete headers["Content-Type"];
|
|
146
|
+
const res = await fetchFn(url, {
|
|
147
|
+
...init,
|
|
148
|
+
headers
|
|
149
|
+
});
|
|
150
|
+
if (res.status === 204) return void 0;
|
|
151
|
+
const text = await res.text().catch(() => "");
|
|
152
|
+
let body = {};
|
|
153
|
+
if (text) try {
|
|
154
|
+
body = JSON.parse(text, rebaseReviver);
|
|
155
|
+
} catch (e) {}
|
|
156
|
+
const getErrorField = (obj, field) => {
|
|
157
|
+
const err = obj?.error;
|
|
158
|
+
if (err && typeof err === "object" && err !== null && field in err) return err[field];
|
|
159
|
+
return obj?.[field];
|
|
160
|
+
};
|
|
161
|
+
if (res.status === 401 && onUnauthorizedHandler) {
|
|
162
|
+
if (await onUnauthorizedHandler()) {
|
|
163
|
+
let retryToken = token;
|
|
164
|
+
if (tokenGetter) try {
|
|
165
|
+
const fetched = await tokenGetter();
|
|
166
|
+
if (fetched !== null && fetched !== void 0) retryToken = fetched;
|
|
167
|
+
} catch (e) {}
|
|
168
|
+
const retryHeaders = getHeaders(retryToken, init);
|
|
169
|
+
const retryRes = await fetchFn(url, {
|
|
170
|
+
...init,
|
|
171
|
+
headers: retryHeaders
|
|
172
|
+
});
|
|
173
|
+
if (retryRes.status === 204) return void 0;
|
|
174
|
+
const retryText = await retryRes.text().catch(() => "");
|
|
175
|
+
let retryBody = {};
|
|
176
|
+
if (retryText) try {
|
|
177
|
+
retryBody = JSON.parse(retryText, rebaseReviver);
|
|
178
|
+
} catch (e) {}
|
|
179
|
+
if (!retryRes.ok) {
|
|
180
|
+
let fallbackMessage = retryRes.statusText;
|
|
181
|
+
if (retryRes.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
|
|
182
|
+
throw new RebaseApiError(retryRes.status, String(getErrorField(retryBody, "message") || fallbackMessage || `Request failed with status ${retryRes.status}`), getErrorField(retryBody, "code"), getErrorField(retryBody, "details"));
|
|
183
|
+
}
|
|
184
|
+
return retryBody;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
let fallbackMessage = res.statusText;
|
|
189
|
+
if (res.status === 404 && !fallbackMessage) fallbackMessage = `Endpoint not found (${init?.method || "GET"} ${path}). This usually means the collection is not registered on the backend, or the frontend API URL configuration (e.g. VITE_API_URL) is missing or pointing to the wrong host.`;
|
|
190
|
+
throw new RebaseApiError(res.status, String(getErrorField(body, "message") || fallbackMessage || `Request failed with status ${res.status}`), getErrorField(body, "code"), getErrorField(body, "details"));
|
|
191
|
+
}
|
|
192
|
+
return body;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
request,
|
|
196
|
+
setToken(newToken) {
|
|
197
|
+
token = newToken || void 0;
|
|
198
|
+
},
|
|
199
|
+
setAuthTokenGetter(getter) {
|
|
200
|
+
tokenGetter = getter;
|
|
201
|
+
},
|
|
202
|
+
setOnUnauthorized(handler) {
|
|
203
|
+
onUnauthorizedHandler = handler;
|
|
204
|
+
},
|
|
205
|
+
get baseUrl() {
|
|
206
|
+
return config.baseUrl ? config.baseUrl.replace(/\/$/, "") : "";
|
|
207
|
+
},
|
|
208
|
+
get apiPath() {
|
|
209
|
+
return apiPath;
|
|
210
|
+
},
|
|
211
|
+
get fetchFn() {
|
|
212
|
+
return fetchFn;
|
|
213
|
+
},
|
|
214
|
+
getHeaders: (init) => getHeaders(token, init),
|
|
215
|
+
resolveToken: async () => {
|
|
216
|
+
if (tokenGetter) try {
|
|
217
|
+
const fetched = await tokenGetter();
|
|
218
|
+
if (fetched !== null && fetched !== void 0) return fetched;
|
|
219
|
+
} catch (e) {}
|
|
220
|
+
return token || null;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
273
223
|
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/auth.ts
|
|
274
226
|
function createMemoryStorage() {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
227
|
+
const store = {};
|
|
228
|
+
return {
|
|
229
|
+
getItem(key) {
|
|
230
|
+
return store[key] ?? null;
|
|
231
|
+
},
|
|
232
|
+
setItem(key, value) {
|
|
233
|
+
store[key] = value;
|
|
234
|
+
},
|
|
235
|
+
removeItem(key) {
|
|
236
|
+
delete store[key];
|
|
237
|
+
}
|
|
238
|
+
};
|
|
287
239
|
}
|
|
288
240
|
function detectStorage() {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return createMemoryStorage();
|
|
241
|
+
try {
|
|
242
|
+
if (typeof localStorage !== "undefined") {
|
|
243
|
+
localStorage.setItem("__rebase_test__", "1");
|
|
244
|
+
localStorage.removeItem("__rebase_test__");
|
|
245
|
+
return localStorage;
|
|
246
|
+
}
|
|
247
|
+
} catch (e) {}
|
|
248
|
+
return createMemoryStorage();
|
|
298
249
|
}
|
|
299
250
|
function createAuth(transport, options) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
signInWithSpotify,
|
|
719
|
-
signOut,
|
|
720
|
-
refreshSession,
|
|
721
|
-
getUser,
|
|
722
|
-
updateUser,
|
|
723
|
-
resetPasswordForEmail,
|
|
724
|
-
resetPassword,
|
|
725
|
-
changePassword,
|
|
726
|
-
sendVerificationEmail,
|
|
727
|
-
verifyEmail,
|
|
728
|
-
getSessions,
|
|
729
|
-
revokeSession,
|
|
730
|
-
revokeAllSessions,
|
|
731
|
-
getAuthConfig,
|
|
732
|
-
getSession,
|
|
733
|
-
onAuthStateChange
|
|
734
|
-
};
|
|
251
|
+
const opts = options || {};
|
|
252
|
+
const storage = opts.storage || detectStorage();
|
|
253
|
+
const authPath = opts.authPath || "/auth";
|
|
254
|
+
const autoRefresh = opts.autoRefresh !== false;
|
|
255
|
+
const persistSession = opts.persistSession !== false;
|
|
256
|
+
const STORAGE_KEY = "rebase_auth";
|
|
257
|
+
const REFRESH_BUFFER_MS = 12e4;
|
|
258
|
+
let currentSession = null;
|
|
259
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
260
|
+
let refreshTimeout = null;
|
|
261
|
+
function authUrl(endpoint) {
|
|
262
|
+
return transport.baseUrl + transport.apiPath + authPath + endpoint;
|
|
263
|
+
}
|
|
264
|
+
function getFetch() {
|
|
265
|
+
return transport.fetchFn || globalThis.fetch;
|
|
266
|
+
}
|
|
267
|
+
function throwApiError(status, body, statusText) {
|
|
268
|
+
throw new RebaseApiError(status, body?.error?.message || body?.message || statusText, body?.error?.code || body?.code, body?.error?.details || body?.details);
|
|
269
|
+
}
|
|
270
|
+
function emit(event, session) {
|
|
271
|
+
for (const fn of listeners) try {
|
|
272
|
+
fn(event, session);
|
|
273
|
+
} catch (e) {}
|
|
274
|
+
}
|
|
275
|
+
function saveSession(session) {
|
|
276
|
+
if (!persistSession) return;
|
|
277
|
+
try {
|
|
278
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(session));
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
}
|
|
281
|
+
function clearStoredSession() {
|
|
282
|
+
try {
|
|
283
|
+
storage.removeItem(STORAGE_KEY);
|
|
284
|
+
} catch (e) {}
|
|
285
|
+
}
|
|
286
|
+
function loadStoredSession() {
|
|
287
|
+
try {
|
|
288
|
+
const raw = storage.getItem(STORAGE_KEY);
|
|
289
|
+
if (raw) return JSON.parse(raw);
|
|
290
|
+
} catch (e) {}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
function scheduleRefresh(expiresAt) {
|
|
294
|
+
if (refreshTimeout) clearTimeout(refreshTimeout);
|
|
295
|
+
if (!autoRefresh) return;
|
|
296
|
+
const delay = expiresAt - REFRESH_BUFFER_MS - Date.now();
|
|
297
|
+
if (delay <= 0) {
|
|
298
|
+
refreshSession().catch(() => signOut());
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
refreshTimeout = setTimeout(async () => {
|
|
302
|
+
try {
|
|
303
|
+
await refreshSession();
|
|
304
|
+
} catch (e) {
|
|
305
|
+
signOut();
|
|
306
|
+
}
|
|
307
|
+
}, delay);
|
|
308
|
+
}
|
|
309
|
+
function handleAuthResponse(data, event) {
|
|
310
|
+
const session = {
|
|
311
|
+
accessToken: data.tokens.accessToken,
|
|
312
|
+
refreshToken: data.tokens.refreshToken,
|
|
313
|
+
expiresAt: data.tokens.accessTokenExpiresAt,
|
|
314
|
+
user: data.user
|
|
315
|
+
};
|
|
316
|
+
currentSession = session;
|
|
317
|
+
saveSession(session);
|
|
318
|
+
transport.setToken(session.accessToken);
|
|
319
|
+
scheduleRefresh(session.expiresAt);
|
|
320
|
+
emit(event || "SIGNED_IN", session);
|
|
321
|
+
return session;
|
|
322
|
+
}
|
|
323
|
+
async function signInWithEmail(email, password) {
|
|
324
|
+
const res = await getFetch()(authUrl("/login"), {
|
|
325
|
+
method: "POST",
|
|
326
|
+
headers: { "Content-Type": "application/json" },
|
|
327
|
+
body: JSON.stringify({
|
|
328
|
+
email,
|
|
329
|
+
password
|
|
330
|
+
})
|
|
331
|
+
});
|
|
332
|
+
const body = await res.json().catch(() => ({}));
|
|
333
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
334
|
+
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
335
|
+
return {
|
|
336
|
+
user: session.user,
|
|
337
|
+
accessToken: session.accessToken,
|
|
338
|
+
refreshToken: session.refreshToken
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
async function signUp(email, password, displayName) {
|
|
342
|
+
const fetchFn = getFetch();
|
|
343
|
+
const payload = {
|
|
344
|
+
email,
|
|
345
|
+
password
|
|
346
|
+
};
|
|
347
|
+
if (displayName !== void 0) payload.displayName = displayName;
|
|
348
|
+
const res = await fetchFn(authUrl("/register"), {
|
|
349
|
+
method: "POST",
|
|
350
|
+
headers: { "Content-Type": "application/json" },
|
|
351
|
+
body: JSON.stringify(payload)
|
|
352
|
+
});
|
|
353
|
+
const body = await res.json().catch(() => ({}));
|
|
354
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
355
|
+
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
356
|
+
return {
|
|
357
|
+
user: session.user,
|
|
358
|
+
accessToken: session.accessToken,
|
|
359
|
+
refreshToken: session.refreshToken
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Sign in with Google.
|
|
364
|
+
*
|
|
365
|
+
* Supports three invocation styles:
|
|
366
|
+
* - `signInWithGoogle({ idToken })` — ID-token flow (One Tap / Sign In button)
|
|
367
|
+
* - `signInWithGoogle({ accessToken })` — Access-token flow (popup)
|
|
368
|
+
* - `signInWithGoogle({ code, redirectUri })` — Authorization code flow (most secure)
|
|
369
|
+
*/
|
|
370
|
+
async function signInWithGoogle(payload) {
|
|
371
|
+
const res = await getFetch()(authUrl("/google"), {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { "Content-Type": "application/json" },
|
|
374
|
+
body: JSON.stringify(payload)
|
|
375
|
+
});
|
|
376
|
+
const responseBody = await res.json().catch(() => ({}));
|
|
377
|
+
if (!res.ok) throwApiError(res.status, responseBody, res.statusText);
|
|
378
|
+
const session = handleAuthResponse(responseBody, "SIGNED_IN");
|
|
379
|
+
return {
|
|
380
|
+
user: session.user,
|
|
381
|
+
accessToken: session.accessToken,
|
|
382
|
+
refreshToken: session.refreshToken
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
async function signInWithLinkedin(code, redirectUri) {
|
|
386
|
+
const res = await getFetch()(authUrl("/linkedin"), {
|
|
387
|
+
method: "POST",
|
|
388
|
+
headers: { "Content-Type": "application/json" },
|
|
389
|
+
body: JSON.stringify({
|
|
390
|
+
code,
|
|
391
|
+
redirectUri
|
|
392
|
+
})
|
|
393
|
+
});
|
|
394
|
+
const body = await res.json().catch(() => ({}));
|
|
395
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
396
|
+
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
397
|
+
return {
|
|
398
|
+
user: session.user,
|
|
399
|
+
accessToken: session.accessToken,
|
|
400
|
+
refreshToken: session.refreshToken
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Generic OAuth sign-in. Posts the given payload to `/auth/{providerId}`.
|
|
405
|
+
* Use this for any provider registered on the backend.
|
|
406
|
+
*/
|
|
407
|
+
async function signInWithOAuth(providerId, payload) {
|
|
408
|
+
const res = await getFetch()(authUrl(`/${providerId}`), {
|
|
409
|
+
method: "POST",
|
|
410
|
+
headers: { "Content-Type": "application/json" },
|
|
411
|
+
body: JSON.stringify(payload)
|
|
412
|
+
});
|
|
413
|
+
const body = await res.json().catch(() => ({}));
|
|
414
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
415
|
+
const session = handleAuthResponse(body, "SIGNED_IN");
|
|
416
|
+
return {
|
|
417
|
+
user: session.user,
|
|
418
|
+
accessToken: session.accessToken,
|
|
419
|
+
refreshToken: session.refreshToken
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
async function signInWithGitHub(code, redirectUri) {
|
|
423
|
+
return signInWithOAuth("github", {
|
|
424
|
+
code,
|
|
425
|
+
redirectUri
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
async function signInWithMicrosoft(code, redirectUri) {
|
|
429
|
+
return signInWithOAuth("microsoft", {
|
|
430
|
+
code,
|
|
431
|
+
redirectUri
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
async function signInWithApple(code, redirectUri, user) {
|
|
435
|
+
return signInWithOAuth("apple", {
|
|
436
|
+
code,
|
|
437
|
+
redirectUri,
|
|
438
|
+
user
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async function signInWithFacebook(code, redirectUri) {
|
|
442
|
+
return signInWithOAuth("facebook", {
|
|
443
|
+
code,
|
|
444
|
+
redirectUri
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function signInWithTwitter(code, redirectUri, codeVerifier) {
|
|
448
|
+
return signInWithOAuth("twitter", {
|
|
449
|
+
code,
|
|
450
|
+
redirectUri,
|
|
451
|
+
codeVerifier
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function signInWithDiscord(code, redirectUri) {
|
|
455
|
+
return signInWithOAuth("discord", {
|
|
456
|
+
code,
|
|
457
|
+
redirectUri
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
async function signInWithGitLab(code, redirectUri) {
|
|
461
|
+
return signInWithOAuth("gitlab", {
|
|
462
|
+
code,
|
|
463
|
+
redirectUri
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function signInWithBitbucket(code, redirectUri) {
|
|
467
|
+
return signInWithOAuth("bitbucket", {
|
|
468
|
+
code,
|
|
469
|
+
redirectUri
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async function signInWithSlack(code, redirectUri) {
|
|
473
|
+
return signInWithOAuth("slack", {
|
|
474
|
+
code,
|
|
475
|
+
redirectUri
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
async function signInWithSpotify(code, redirectUri) {
|
|
479
|
+
return signInWithOAuth("spotify", {
|
|
480
|
+
code,
|
|
481
|
+
redirectUri
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
async function signOut() {
|
|
485
|
+
const fetchFn = getFetch();
|
|
486
|
+
try {
|
|
487
|
+
if (currentSession?.refreshToken) await fetchFn(authUrl("/logout"), {
|
|
488
|
+
method: "POST",
|
|
489
|
+
headers: { "Content-Type": "application/json" },
|
|
490
|
+
body: JSON.stringify({ refreshToken: currentSession.refreshToken })
|
|
491
|
+
});
|
|
492
|
+
} catch (e) {}
|
|
493
|
+
currentSession = null;
|
|
494
|
+
clearStoredSession();
|
|
495
|
+
if (refreshTimeout) {
|
|
496
|
+
clearTimeout(refreshTimeout);
|
|
497
|
+
refreshTimeout = null;
|
|
498
|
+
}
|
|
499
|
+
transport.setToken(null);
|
|
500
|
+
emit("SIGNED_OUT", null);
|
|
501
|
+
}
|
|
502
|
+
async function refreshSession() {
|
|
503
|
+
if (!currentSession?.refreshToken) throw new Error("No active session to refresh");
|
|
504
|
+
const res = await getFetch()(authUrl("/refresh"), {
|
|
505
|
+
method: "POST",
|
|
506
|
+
headers: { "Content-Type": "application/json" },
|
|
507
|
+
body: JSON.stringify({ refreshToken: currentSession.refreshToken })
|
|
508
|
+
});
|
|
509
|
+
const body = await res.json().catch(() => ({}));
|
|
510
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
511
|
+
const session = {
|
|
512
|
+
accessToken: body.tokens.accessToken,
|
|
513
|
+
refreshToken: body.tokens.refreshToken,
|
|
514
|
+
expiresAt: body.tokens.accessTokenExpiresAt,
|
|
515
|
+
user: currentSession.user
|
|
516
|
+
};
|
|
517
|
+
currentSession = session;
|
|
518
|
+
saveSession(session);
|
|
519
|
+
transport.setToken(session.accessToken);
|
|
520
|
+
scheduleRefresh(session.expiresAt);
|
|
521
|
+
emit("TOKEN_REFRESHED", session);
|
|
522
|
+
return session;
|
|
523
|
+
}
|
|
524
|
+
async function getUser() {
|
|
525
|
+
return (await transport.request(authPath + "/me", { method: "GET" })).user;
|
|
526
|
+
}
|
|
527
|
+
async function updateUser(updates) {
|
|
528
|
+
const data = await transport.request(authPath + "/me", {
|
|
529
|
+
method: "PATCH",
|
|
530
|
+
body: JSON.stringify(updates)
|
|
531
|
+
});
|
|
532
|
+
if (currentSession) {
|
|
533
|
+
currentSession = {
|
|
534
|
+
...currentSession,
|
|
535
|
+
user: data.user
|
|
536
|
+
};
|
|
537
|
+
saveSession(currentSession);
|
|
538
|
+
emit("USER_UPDATED", currentSession);
|
|
539
|
+
}
|
|
540
|
+
return data.user;
|
|
541
|
+
}
|
|
542
|
+
async function resetPasswordForEmail(email) {
|
|
543
|
+
const res = await getFetch()(authUrl("/forgot-password"), {
|
|
544
|
+
method: "POST",
|
|
545
|
+
headers: { "Content-Type": "application/json" },
|
|
546
|
+
body: JSON.stringify({ email })
|
|
547
|
+
});
|
|
548
|
+
const body = await res.json().catch(() => ({}));
|
|
549
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
550
|
+
return body;
|
|
551
|
+
}
|
|
552
|
+
async function resetPassword(token, password) {
|
|
553
|
+
const res = await getFetch()(authUrl("/reset-password"), {
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: { "Content-Type": "application/json" },
|
|
556
|
+
body: JSON.stringify({
|
|
557
|
+
token,
|
|
558
|
+
password
|
|
559
|
+
})
|
|
560
|
+
});
|
|
561
|
+
const body = await res.json().catch(() => ({}));
|
|
562
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
563
|
+
return body;
|
|
564
|
+
}
|
|
565
|
+
async function changePassword(oldPassword, newPassword) {
|
|
566
|
+
return transport.request(authPath + "/change-password", {
|
|
567
|
+
method: "POST",
|
|
568
|
+
body: JSON.stringify({
|
|
569
|
+
oldPassword,
|
|
570
|
+
newPassword
|
|
571
|
+
})
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
async function sendVerificationEmail() {
|
|
575
|
+
return transport.request(authPath + "/send-verification", { method: "POST" });
|
|
576
|
+
}
|
|
577
|
+
async function verifyEmail(token) {
|
|
578
|
+
const res = await getFetch()(authUrl("/verify-email?token=" + encodeURIComponent(token)), {
|
|
579
|
+
method: "GET",
|
|
580
|
+
headers: { "Content-Type": "application/json" }
|
|
581
|
+
});
|
|
582
|
+
const body = await res.json().catch(() => ({}));
|
|
583
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
584
|
+
return body;
|
|
585
|
+
}
|
|
586
|
+
async function getSessions() {
|
|
587
|
+
return (await transport.request(authPath + "/sessions", { method: "GET" })).sessions;
|
|
588
|
+
}
|
|
589
|
+
async function revokeSession(sessionId) {
|
|
590
|
+
return transport.request(authPath + "/sessions/" + encodeURIComponent(sessionId), { method: "DELETE" });
|
|
591
|
+
}
|
|
592
|
+
async function revokeAllSessions() {
|
|
593
|
+
const result = await transport.request(authPath + "/sessions", { method: "DELETE" });
|
|
594
|
+
currentSession = null;
|
|
595
|
+
clearStoredSession();
|
|
596
|
+
if (refreshTimeout) {
|
|
597
|
+
clearTimeout(refreshTimeout);
|
|
598
|
+
refreshTimeout = null;
|
|
599
|
+
}
|
|
600
|
+
transport.setToken(null);
|
|
601
|
+
emit("SIGNED_OUT", null);
|
|
602
|
+
return result;
|
|
603
|
+
}
|
|
604
|
+
async function getAuthConfig() {
|
|
605
|
+
const res = await getFetch()(authUrl("/config"), {
|
|
606
|
+
method: "GET",
|
|
607
|
+
headers: { "Content-Type": "application/json" }
|
|
608
|
+
});
|
|
609
|
+
const body = await res.json().catch(() => ({}));
|
|
610
|
+
if (!res.ok) throwApiError(res.status, body, res.statusText);
|
|
611
|
+
return body;
|
|
612
|
+
}
|
|
613
|
+
function getSession() {
|
|
614
|
+
return currentSession;
|
|
615
|
+
}
|
|
616
|
+
function onAuthStateChange(callback) {
|
|
617
|
+
listeners.add(callback);
|
|
618
|
+
return () => listeners.delete(callback);
|
|
619
|
+
}
|
|
620
|
+
if (persistSession) {
|
|
621
|
+
const stored = loadStoredSession();
|
|
622
|
+
if (stored && stored.accessToken && stored.refreshToken) {
|
|
623
|
+
if (stored.expiresAt > Date.now()) {
|
|
624
|
+
currentSession = stored;
|
|
625
|
+
transport.setToken(stored.accessToken);
|
|
626
|
+
scheduleRefresh(stored.expiresAt);
|
|
627
|
+
} else if (stored.refreshToken) {
|
|
628
|
+
currentSession = stored;
|
|
629
|
+
refreshSession().catch(() => {
|
|
630
|
+
currentSession = null;
|
|
631
|
+
clearStoredSession();
|
|
632
|
+
transport.setToken(null);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
signInWithEmail,
|
|
639
|
+
signUp,
|
|
640
|
+
signInWithGoogle,
|
|
641
|
+
signInWithLinkedin,
|
|
642
|
+
signInWithOAuth,
|
|
643
|
+
signInWithGitHub,
|
|
644
|
+
signInWithMicrosoft,
|
|
645
|
+
signInWithApple,
|
|
646
|
+
signInWithFacebook,
|
|
647
|
+
signInWithTwitter,
|
|
648
|
+
signInWithDiscord,
|
|
649
|
+
signInWithGitLab,
|
|
650
|
+
signInWithBitbucket,
|
|
651
|
+
signInWithSlack,
|
|
652
|
+
signInWithSpotify,
|
|
653
|
+
signOut,
|
|
654
|
+
refreshSession,
|
|
655
|
+
getUser,
|
|
656
|
+
updateUser,
|
|
657
|
+
resetPasswordForEmail,
|
|
658
|
+
resetPassword,
|
|
659
|
+
changePassword,
|
|
660
|
+
sendVerificationEmail,
|
|
661
|
+
verifyEmail,
|
|
662
|
+
getSessions,
|
|
663
|
+
revokeSession,
|
|
664
|
+
revokeAllSessions,
|
|
665
|
+
getAuthConfig,
|
|
666
|
+
getSession,
|
|
667
|
+
onAuthStateChange
|
|
668
|
+
};
|
|
735
669
|
}
|
|
736
670
|
function createCookieStorage(options = {}) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
if (defaultOptions.sameSite) {
|
|
774
|
-
cookieStr += `; samesite=${defaultOptions.sameSite}`;
|
|
775
|
-
}
|
|
776
|
-
document.cookie = cookieStr;
|
|
777
|
-
},
|
|
778
|
-
removeItem(key) {
|
|
779
|
-
if (typeof document === "undefined") return;
|
|
780
|
-
let cookieStr = `${encodeURIComponent(key)}=; path=${defaultOptions.path || "/"}; max-age=-1`;
|
|
781
|
-
if (defaultOptions.domain) {
|
|
782
|
-
cookieStr += `; domain=${defaultOptions.domain}`;
|
|
783
|
-
}
|
|
784
|
-
document.cookie = cookieStr;
|
|
785
|
-
}
|
|
786
|
-
};
|
|
671
|
+
const defaultOptions = {
|
|
672
|
+
path: "/",
|
|
673
|
+
sameSite: "Lax",
|
|
674
|
+
...options
|
|
675
|
+
};
|
|
676
|
+
return {
|
|
677
|
+
getItem(key) {
|
|
678
|
+
if (typeof document === "undefined") return null;
|
|
679
|
+
const nameEQ = encodeURIComponent(key) + "=";
|
|
680
|
+
const ca = document.cookie.split(";");
|
|
681
|
+
for (let i = 0; i < ca.length; i++) {
|
|
682
|
+
let c = ca[i];
|
|
683
|
+
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
|
684
|
+
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
},
|
|
688
|
+
setItem(key, value) {
|
|
689
|
+
if (typeof document === "undefined") return;
|
|
690
|
+
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
691
|
+
if (defaultOptions.path) cookieStr += `; path=${defaultOptions.path}`;
|
|
692
|
+
if (defaultOptions.domain) cookieStr += `; domain=${defaultOptions.domain}`;
|
|
693
|
+
if (defaultOptions.maxAge !== void 0) cookieStr += `; max-age=${defaultOptions.maxAge}`;
|
|
694
|
+
else cookieStr += `; max-age=${365 * 24 * 60 * 60}`;
|
|
695
|
+
if (defaultOptions.secure) cookieStr += "; secure";
|
|
696
|
+
if (defaultOptions.sameSite) cookieStr += `; samesite=${defaultOptions.sameSite}`;
|
|
697
|
+
document.cookie = cookieStr;
|
|
698
|
+
},
|
|
699
|
+
removeItem(key) {
|
|
700
|
+
if (typeof document === "undefined") return;
|
|
701
|
+
let cookieStr = `${encodeURIComponent(key)}=; path=${defaultOptions.path || "/"}; max-age=-1`;
|
|
702
|
+
if (defaultOptions.domain) cookieStr += `; domain=${defaultOptions.domain}`;
|
|
703
|
+
document.cookie = cookieStr;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
787
706
|
}
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/admin.ts
|
|
788
709
|
function createAdmin(transport, options) {
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
listUsers,
|
|
834
|
-
listUsersPaginated,
|
|
835
|
-
getUser,
|
|
836
|
-
createUser,
|
|
837
|
-
updateUser,
|
|
838
|
-
deleteUser,
|
|
839
|
-
bootstrap
|
|
840
|
-
};
|
|
710
|
+
const adminPath = (options || {}).adminPath || "/admin";
|
|
711
|
+
async function listUsers() {
|
|
712
|
+
return transport.request(adminPath + "/users", { method: "GET" });
|
|
713
|
+
}
|
|
714
|
+
async function listUsersPaginated(options) {
|
|
715
|
+
const params = new URLSearchParams();
|
|
716
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
717
|
+
if (options?.offset !== void 0) params.set("offset", String(options.offset));
|
|
718
|
+
if (options?.search) params.set("search", options.search);
|
|
719
|
+
if (options?.orderBy) params.set("orderBy", options.orderBy);
|
|
720
|
+
if (options?.orderDir) params.set("orderDir", options.orderDir);
|
|
721
|
+
const qs = params.toString();
|
|
722
|
+
return transport.request(adminPath + "/users" + (qs ? "?" + qs : ""), { method: "GET" });
|
|
723
|
+
}
|
|
724
|
+
async function getUser(userId) {
|
|
725
|
+
return transport.request(adminPath + "/users/" + encodeURIComponent(userId), { method: "GET" });
|
|
726
|
+
}
|
|
727
|
+
async function createUser(data) {
|
|
728
|
+
return transport.request(adminPath + "/users", {
|
|
729
|
+
method: "POST",
|
|
730
|
+
body: JSON.stringify(data)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
async function updateUser(userId, data) {
|
|
734
|
+
return transport.request(adminPath + "/users/" + encodeURIComponent(userId), {
|
|
735
|
+
method: "PUT",
|
|
736
|
+
body: JSON.stringify(data)
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
async function deleteUser(userId) {
|
|
740
|
+
return transport.request(adminPath + "/users/" + encodeURIComponent(userId), { method: "DELETE" });
|
|
741
|
+
}
|
|
742
|
+
async function bootstrap() {
|
|
743
|
+
return transport.request(adminPath + "/bootstrap", { method: "POST" });
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
listUsers,
|
|
747
|
+
listUsersPaginated,
|
|
748
|
+
getUser,
|
|
749
|
+
createUser,
|
|
750
|
+
updateUser,
|
|
751
|
+
deleteUser,
|
|
752
|
+
bootstrap
|
|
753
|
+
};
|
|
841
754
|
}
|
|
755
|
+
//#endregion
|
|
756
|
+
//#region src/cron.ts
|
|
842
757
|
function createCron(transport, options) {
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
method: "PUT",
|
|
873
|
-
body: JSON.stringify({ enabled })
|
|
874
|
-
}
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
return {
|
|
878
|
-
listJobs,
|
|
879
|
-
getJob,
|
|
880
|
-
triggerJob,
|
|
881
|
-
getJobLogs,
|
|
882
|
-
toggleJob
|
|
883
|
-
};
|
|
758
|
+
const cronPath = options?.cronPath || "/cron";
|
|
759
|
+
async function listJobs() {
|
|
760
|
+
return transport.request(cronPath, { method: "GET" });
|
|
761
|
+
}
|
|
762
|
+
async function getJob(jobId) {
|
|
763
|
+
return transport.request(cronPath + "/" + encodeURIComponent(jobId), { method: "GET" });
|
|
764
|
+
}
|
|
765
|
+
async function triggerJob(jobId) {
|
|
766
|
+
return transport.request(cronPath + "/" + encodeURIComponent(jobId) + "/trigger", { method: "POST" });
|
|
767
|
+
}
|
|
768
|
+
async function getJobLogs(jobId, options) {
|
|
769
|
+
const params = new URLSearchParams();
|
|
770
|
+
if (options?.limit !== void 0) params.set("limit", String(options.limit));
|
|
771
|
+
const qs = params.toString();
|
|
772
|
+
return transport.request(cronPath + "/" + encodeURIComponent(jobId) + "/logs" + (qs ? "?" + qs : ""), { method: "GET" });
|
|
773
|
+
}
|
|
774
|
+
async function toggleJob(jobId, enabled) {
|
|
775
|
+
return transport.request(cronPath + "/" + encodeURIComponent(jobId), {
|
|
776
|
+
method: "PUT",
|
|
777
|
+
body: JSON.stringify({ enabled })
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
listJobs,
|
|
782
|
+
getJob,
|
|
783
|
+
triggerJob,
|
|
784
|
+
getJobLogs,
|
|
785
|
+
toggleJob
|
|
786
|
+
};
|
|
884
787
|
}
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/collection.ts
|
|
885
790
|
function parseWhereFilter(where) {
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) {
|
|
973
|
-
filters[key] = rawValue.map((r) => parseSingle(r, key));
|
|
974
|
-
} else {
|
|
975
|
-
filters[key] = parseSingle(rawValue, key);
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
return filters;
|
|
791
|
+
if (!where) return void 0;
|
|
792
|
+
const filters = {};
|
|
793
|
+
const OP_TO_FILTER = {
|
|
794
|
+
"eq": "==",
|
|
795
|
+
"neq": "!=",
|
|
796
|
+
"gt": ">",
|
|
797
|
+
"gte": ">=",
|
|
798
|
+
"lt": "<",
|
|
799
|
+
"lte": "<=",
|
|
800
|
+
"==": "==",
|
|
801
|
+
"!=": "!=",
|
|
802
|
+
">": ">",
|
|
803
|
+
">=": ">=",
|
|
804
|
+
"<": "<",
|
|
805
|
+
"<=": "<=",
|
|
806
|
+
"in": "in",
|
|
807
|
+
"nin": "not-in",
|
|
808
|
+
"not-in": "not-in",
|
|
809
|
+
"cs": "array-contains",
|
|
810
|
+
"csa": "array-contains-any",
|
|
811
|
+
"array-contains": "array-contains",
|
|
812
|
+
"array-contains-any": "array-contains-any"
|
|
813
|
+
};
|
|
814
|
+
const parseSingle = (rawValue, fieldKey) => {
|
|
815
|
+
if (rawValue === null) return ["==", null];
|
|
816
|
+
if (typeof rawValue === "boolean") return ["==", rawValue];
|
|
817
|
+
if (typeof rawValue === "number") return ["==", rawValue];
|
|
818
|
+
if (Array.isArray(rawValue) && rawValue.length === 2 && typeof rawValue[0] === "string") {
|
|
819
|
+
const [rawOp, val] = rawValue;
|
|
820
|
+
return [OP_TO_FILTER[rawOp] ?? "==", val];
|
|
821
|
+
}
|
|
822
|
+
const value = String(rawValue);
|
|
823
|
+
const dotIndex = value.indexOf(".");
|
|
824
|
+
if (dotIndex > 0) {
|
|
825
|
+
const opStr = value.substring(0, dotIndex);
|
|
826
|
+
const valStr = value.substring(dotIndex + 1);
|
|
827
|
+
let op = "==";
|
|
828
|
+
let val = valStr;
|
|
829
|
+
switch (opStr) {
|
|
830
|
+
case "eq":
|
|
831
|
+
op = "==";
|
|
832
|
+
break;
|
|
833
|
+
case "neq":
|
|
834
|
+
op = "!=";
|
|
835
|
+
break;
|
|
836
|
+
case "gt":
|
|
837
|
+
op = ">";
|
|
838
|
+
break;
|
|
839
|
+
case "gte":
|
|
840
|
+
op = ">=";
|
|
841
|
+
break;
|
|
842
|
+
case "lt":
|
|
843
|
+
op = "<";
|
|
844
|
+
break;
|
|
845
|
+
case "lte":
|
|
846
|
+
op = "<=";
|
|
847
|
+
break;
|
|
848
|
+
case "in":
|
|
849
|
+
op = "in";
|
|
850
|
+
val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
|
|
851
|
+
break;
|
|
852
|
+
case "nin":
|
|
853
|
+
op = "not-in";
|
|
854
|
+
val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
|
|
855
|
+
break;
|
|
856
|
+
case "cs":
|
|
857
|
+
op = "array-contains";
|
|
858
|
+
break;
|
|
859
|
+
case "csa":
|
|
860
|
+
op = "array-contains-any";
|
|
861
|
+
val = valStr.startsWith("(") && valStr.endsWith(")") ? valStr.slice(1, -1).split(",").map((v) => v.trim()) : valStr.split(",");
|
|
862
|
+
break;
|
|
863
|
+
default:
|
|
864
|
+
op = "==";
|
|
865
|
+
val = value;
|
|
866
|
+
}
|
|
867
|
+
if (val === "true") val = true;
|
|
868
|
+
else if (val === "false") val = false;
|
|
869
|
+
else if (val === "null") val = null;
|
|
870
|
+
else if (typeof val === "string" && /^[0-9]+(\.[0-9]+)?$/.test(val) && fieldKey !== "id" && !fieldKey.endsWith("_id")) val = Number(val);
|
|
871
|
+
return [op, val];
|
|
872
|
+
} else return ["==", value];
|
|
873
|
+
};
|
|
874
|
+
for (const [key, rawValue] of Object.entries(where)) if (Array.isArray(rawValue) && rawValue.length > 0 && Array.isArray(rawValue[0])) filters[key] = rawValue.map((r) => parseSingle(r, key));
|
|
875
|
+
else filters[key] = parseSingle(rawValue, key);
|
|
876
|
+
return filters;
|
|
979
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Wrap a flat row (returned by the REST API as `{ id, ...fields }`) into
|
|
880
|
+
* a proper `Entity<M>` structure expected by the core framework.
|
|
881
|
+
* The `id` is kept inside `values` as well, since collection properties
|
|
882
|
+
* may define an `isId` field that the form binds to `formex.values`.
|
|
883
|
+
*/
|
|
980
884
|
function rowToEntity(row, slug) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
885
|
+
return {
|
|
886
|
+
id: row.id,
|
|
887
|
+
path: slug,
|
|
888
|
+
values: row
|
|
889
|
+
};
|
|
986
890
|
}
|
|
987
891
|
function createCollectionClient(transport, slug, ws) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
1089
|
-
});
|
|
1090
|
-
},
|
|
1091
|
-
onError
|
|
1092
|
-
);
|
|
1093
|
-
};
|
|
1094
|
-
client.listenById = (id, onUpdate, onError) => {
|
|
1095
|
-
return ws.listenEntity(
|
|
1096
|
-
{
|
|
1097
|
-
path: slug,
|
|
1098
|
-
entityId: String(id)
|
|
1099
|
-
},
|
|
1100
|
-
(entity) => {
|
|
1101
|
-
if (entity) {
|
|
1102
|
-
onUpdate(entity);
|
|
1103
|
-
} else {
|
|
1104
|
-
onUpdate(void 0);
|
|
1105
|
-
}
|
|
1106
|
-
},
|
|
1107
|
-
onError
|
|
1108
|
-
);
|
|
1109
|
-
};
|
|
1110
|
-
}
|
|
1111
|
-
return client;
|
|
892
|
+
const basePath = `/data/${slug}`;
|
|
893
|
+
const client = {
|
|
894
|
+
async find(params) {
|
|
895
|
+
const qs = buildQueryString(params);
|
|
896
|
+
const raw = await transport.request(basePath + qs, { method: "GET" });
|
|
897
|
+
return {
|
|
898
|
+
data: (raw.data || []).map((row) => rowToEntity(row, slug)),
|
|
899
|
+
meta: raw.meta
|
|
900
|
+
};
|
|
901
|
+
},
|
|
902
|
+
async findById(id) {
|
|
903
|
+
try {
|
|
904
|
+
const raw = await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "GET" });
|
|
905
|
+
if (!raw) return void 0;
|
|
906
|
+
return rowToEntity(raw, slug);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
if (err instanceof RebaseApiError && err.status === 404) return;
|
|
909
|
+
throw err;
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
async create(data, id) {
|
|
913
|
+
const body = { ...data };
|
|
914
|
+
if (id !== void 0) body.id = id;
|
|
915
|
+
return rowToEntity(await transport.request(basePath, {
|
|
916
|
+
method: "POST",
|
|
917
|
+
body: JSON.stringify(body)
|
|
918
|
+
}), slug);
|
|
919
|
+
},
|
|
920
|
+
async update(id, data) {
|
|
921
|
+
return rowToEntity(await transport.request(`${basePath}/${encodeURIComponent(String(id))}`, {
|
|
922
|
+
method: "PUT",
|
|
923
|
+
body: JSON.stringify(data)
|
|
924
|
+
}), slug);
|
|
925
|
+
},
|
|
926
|
+
async delete(id) {
|
|
927
|
+
return transport.request(`${basePath}/${encodeURIComponent(String(id))}`, { method: "DELETE" });
|
|
928
|
+
},
|
|
929
|
+
async count(params) {
|
|
930
|
+
const qs = buildQueryString({
|
|
931
|
+
...params,
|
|
932
|
+
limit: void 0,
|
|
933
|
+
offset: void 0
|
|
934
|
+
});
|
|
935
|
+
return (await transport.request(basePath + "/count" + qs, { method: "GET" })).count ?? 0;
|
|
936
|
+
},
|
|
937
|
+
where(columnOrCondition, operator, value) {
|
|
938
|
+
const builder = new QueryBuilder(client);
|
|
939
|
+
if (typeof columnOrCondition === "object") return builder.where(columnOrCondition);
|
|
940
|
+
return builder.where(columnOrCondition, operator, value);
|
|
941
|
+
},
|
|
942
|
+
orderBy(column, ascending) {
|
|
943
|
+
return new QueryBuilder(client).orderBy(column, ascending);
|
|
944
|
+
},
|
|
945
|
+
limit(count) {
|
|
946
|
+
return new QueryBuilder(client).limit(count);
|
|
947
|
+
},
|
|
948
|
+
offset(count) {
|
|
949
|
+
return new QueryBuilder(client).offset(count);
|
|
950
|
+
},
|
|
951
|
+
search(searchString) {
|
|
952
|
+
return new QueryBuilder(client).search(searchString);
|
|
953
|
+
},
|
|
954
|
+
include(...relations) {
|
|
955
|
+
return new QueryBuilder(client).include(...relations);
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
if (ws) {
|
|
959
|
+
client.listen = (params, onUpdate, onError) => {
|
|
960
|
+
return ws.listenCollection({
|
|
961
|
+
path: slug,
|
|
962
|
+
filter: parseWhereFilter(params?.where),
|
|
963
|
+
limit: params?.limit,
|
|
964
|
+
startAfter: params?.offset ? String(params.offset) : void 0,
|
|
965
|
+
orderBy: params?.orderBy?.split(":")[0],
|
|
966
|
+
order: params?.orderBy?.split(":")[1],
|
|
967
|
+
searchString: params?.searchString
|
|
968
|
+
}, (entities) => {
|
|
969
|
+
const requestedLimit = params?.limit || 20;
|
|
970
|
+
onUpdate({
|
|
971
|
+
data: entities,
|
|
972
|
+
meta: {
|
|
973
|
+
total: entities.length,
|
|
974
|
+
limit: requestedLimit,
|
|
975
|
+
offset: params?.offset || 0,
|
|
976
|
+
hasMore: entities.length >= requestedLimit
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
}, onError);
|
|
980
|
+
};
|
|
981
|
+
client.listenById = (id, onUpdate, onError) => {
|
|
982
|
+
return ws.listenEntity({
|
|
983
|
+
path: slug,
|
|
984
|
+
entityId: String(id)
|
|
985
|
+
}, (entity) => {
|
|
986
|
+
if (entity) onUpdate(entity);
|
|
987
|
+
else onUpdate(void 0);
|
|
988
|
+
}, onError);
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
return client;
|
|
1112
992
|
}
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/functions.ts
|
|
995
|
+
/**
|
|
996
|
+
* Create a `FunctionsClient` backed by the given transport.
|
|
997
|
+
*
|
|
998
|
+
* The transport already handles:
|
|
999
|
+
* - Base URL resolution
|
|
1000
|
+
* - JWT injection via `Authorization: Bearer`
|
|
1001
|
+
* - 401 retry / `onUnauthorized` flow
|
|
1002
|
+
* - Consistent error throwing via `RebaseApiError`
|
|
1003
|
+
*
|
|
1004
|
+
* @internal
|
|
1005
|
+
*/
|
|
1113
1006
|
function createFunctionsClient(transport) {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (options?.headers) {
|
|
1124
|
-
init.headers = options.headers;
|
|
1125
|
-
}
|
|
1126
|
-
return transport.request(routePath, init);
|
|
1127
|
-
}
|
|
1128
|
-
};
|
|
1007
|
+
return { async invoke(name, payload, options) {
|
|
1008
|
+
const method = options?.method ?? "POST";
|
|
1009
|
+
const subPath = options?.path ? `/${options.path.replace(/^\//, "")}` : "";
|
|
1010
|
+
const routePath = `/functions/${encodeURIComponent(name)}${subPath}`;
|
|
1011
|
+
const init = { method };
|
|
1012
|
+
if (payload !== void 0 && method !== "GET") init.body = JSON.stringify(payload);
|
|
1013
|
+
if (options?.headers) init.headers = options.headers;
|
|
1014
|
+
return transport.request(routePath, init);
|
|
1015
|
+
} };
|
|
1129
1016
|
}
|
|
1130
|
-
|
|
1131
|
-
|
|
1017
|
+
//#endregion
|
|
1018
|
+
//#region src/storage.ts
|
|
1019
|
+
function createStorage(transport) {
|
|
1020
|
+
const urlsCache = /* @__PURE__ */ new Map();
|
|
1021
|
+
async function putObject({ file, key, metadata, bucket }) {
|
|
1022
|
+
const formData = new FormData();
|
|
1023
|
+
formData.append("file", file);
|
|
1024
|
+
if (key) formData.append("key", key);
|
|
1025
|
+
if (bucket) formData.append("bucket", bucket);
|
|
1026
|
+
if (metadata) {
|
|
1027
|
+
for (const [key, value] of Object.entries(metadata)) if (value !== void 0 && value !== null) formData.append(`metadata_${key}`, typeof value === "string" ? value : JSON.stringify(value));
|
|
1028
|
+
}
|
|
1029
|
+
return (await transport.request("/storage/upload", {
|
|
1030
|
+
method: "POST",
|
|
1031
|
+
body: formData,
|
|
1032
|
+
headers: {}
|
|
1033
|
+
})).data;
|
|
1034
|
+
}
|
|
1035
|
+
async function getSignedUrl(keyOrUrl, bucket) {
|
|
1036
|
+
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
1037
|
+
const cached = urlsCache.get(cacheKey);
|
|
1038
|
+
if (cached) return cached;
|
|
1039
|
+
let filePath = keyOrUrl;
|
|
1040
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
1041
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
|
|
1042
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") return {
|
|
1043
|
+
url: null,
|
|
1044
|
+
fileNotFound: true
|
|
1045
|
+
};
|
|
1046
|
+
try {
|
|
1047
|
+
const result = await transport.request(`/storage/metadata/${filePath}`);
|
|
1048
|
+
const activeToken = await transport.resolveToken();
|
|
1049
|
+
const tokenQuery = activeToken ? `?token=${activeToken}` : "";
|
|
1050
|
+
const downloadConfig = {
|
|
1051
|
+
url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
|
|
1052
|
+
metadata: result.data
|
|
1053
|
+
};
|
|
1054
|
+
urlsCache.set(cacheKey, downloadConfig);
|
|
1055
|
+
return downloadConfig;
|
|
1056
|
+
} catch (e) {
|
|
1057
|
+
if (e instanceof Error && "status" in e && e.status === 404) return {
|
|
1058
|
+
url: null,
|
|
1059
|
+
fileNotFound: true
|
|
1060
|
+
};
|
|
1061
|
+
throw e;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
async function getObject(key, bucket) {
|
|
1065
|
+
let filePath = key;
|
|
1066
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
1067
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
|
|
1068
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") return null;
|
|
1069
|
+
const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
|
|
1070
|
+
const response = await transport.fetchFn(url, { headers: transport.getHeaders ? transport.getHeaders() : {} });
|
|
1071
|
+
if (response.status === 404) return null;
|
|
1072
|
+
if (!response.ok) throw new Error("Failed to get file");
|
|
1073
|
+
const blob = await response.blob();
|
|
1074
|
+
const fileName = filePath.split("/").pop() || "file";
|
|
1075
|
+
return new File([blob], fileName, { type: blob.type });
|
|
1076
|
+
}
|
|
1077
|
+
async function deleteObject(key, bucket) {
|
|
1078
|
+
let filePath = key;
|
|
1079
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
1080
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) filePath = `${bucket}/${filePath}`;
|
|
1081
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") return;
|
|
1082
|
+
try {
|
|
1083
|
+
await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
|
|
1086
|
+
}
|
|
1087
|
+
urlsCache.delete(bucket ? `${bucket}/${key}` : key);
|
|
1088
|
+
}
|
|
1089
|
+
async function listObjects(prefix, options) {
|
|
1090
|
+
const params = new URLSearchParams();
|
|
1091
|
+
if (prefix) params.set("prefix", prefix);
|
|
1092
|
+
if (options?.bucket) params.set("bucket", options.bucket);
|
|
1093
|
+
if (options?.maxResults) params.set("maxResults", String(options.maxResults));
|
|
1094
|
+
if (options?.pageToken) params.set("pageToken", options.pageToken);
|
|
1095
|
+
return (await transport.request(`/storage/list?${params.toString()}`)).data;
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
putObject,
|
|
1099
|
+
getSignedUrl,
|
|
1100
|
+
getObject,
|
|
1101
|
+
deleteObject,
|
|
1102
|
+
listObjects
|
|
1103
|
+
};
|
|
1132
1104
|
}
|
|
1105
|
+
//#endregion
|
|
1106
|
+
//#region src/websocket.ts
|
|
1107
|
+
/**
|
|
1108
|
+
* Extract error message and code from a WebSocket message payload.
|
|
1109
|
+
* Handles both `{ error: string }` and `{ error: { message, code } }` shapes.
|
|
1110
|
+
*/
|
|
1133
1111
|
function extractMessageError(message) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
errorCode
|
|
1141
|
-
};
|
|
1142
|
-
}
|
|
1143
|
-
class ApiError extends Error {
|
|
1144
|
-
code;
|
|
1145
|
-
error;
|
|
1146
|
-
constructor(message, error, code) {
|
|
1147
|
-
super(message);
|
|
1148
|
-
this.name = "ApiError";
|
|
1149
|
-
this.code = code;
|
|
1150
|
-
this.error = error;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
class RebaseWebSocketClient {
|
|
1154
|
-
websocketUrl;
|
|
1155
|
-
ws = null;
|
|
1156
|
-
getAuthToken;
|
|
1157
|
-
subscriptions = /* @__PURE__ */ new Map();
|
|
1158
|
-
listeners = /* @__PURE__ */ new Map();
|
|
1159
|
-
on(event, cb) {
|
|
1160
|
-
if (!this.listeners.has(event)) {
|
|
1161
|
-
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1162
|
-
}
|
|
1163
|
-
this.listeners.get(event).add(cb);
|
|
1164
|
-
return () => this.listeners.get(event).delete(cb);
|
|
1165
|
-
}
|
|
1166
|
-
emit(event, ...args) {
|
|
1167
|
-
if (this.listeners.has(event)) {
|
|
1168
|
-
this.listeners.get(event).forEach((cb) => cb(...args));
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
// New: Subscription deduplication management with optimizations
|
|
1172
|
-
collectionSubscriptions = /* @__PURE__ */ new Map();
|
|
1173
|
-
entitySubscriptions = /* @__PURE__ */ new Map();
|
|
1174
|
-
// Maps to quickly find subscription by backend subscription ID
|
|
1175
|
-
backendToCollectionKey = /* @__PURE__ */ new Map();
|
|
1176
|
-
backendToEntityKey = /* @__PURE__ */ new Map();
|
|
1177
|
-
pendingRequests = /* @__PURE__ */ new Map();
|
|
1178
|
-
reconnectAttempts = 0;
|
|
1179
|
-
maxReconnectAttempts = 5;
|
|
1180
|
-
isConnected = false;
|
|
1181
|
-
messageQueue = [];
|
|
1182
|
-
reconnectTimeout = null;
|
|
1183
|
-
isAuthenticated = false;
|
|
1184
|
-
authPromise = null;
|
|
1185
|
-
WebSocketConstructor;
|
|
1186
|
-
onUnauthorized;
|
|
1187
|
-
refreshInProgress = null;
|
|
1188
|
-
constructor(config) {
|
|
1189
|
-
this.websocketUrl = config.websocketUrl;
|
|
1190
|
-
this.getAuthToken = config.getAuthToken;
|
|
1191
|
-
this.onUnauthorized = config.onUnauthorized;
|
|
1192
|
-
this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
1193
|
-
if (!this.WebSocketConstructor) {
|
|
1194
|
-
console.warn("WebSocket is not defined in this environment. Realtime subscriptions will not work unless you provide a WebSocket implementation in the config.");
|
|
1195
|
-
} else {
|
|
1196
|
-
this.initWebSocket();
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
/**
|
|
1200
|
-
* Authenticate the WebSocket connection
|
|
1201
|
-
*/
|
|
1202
|
-
async authenticate(token) {
|
|
1203
|
-
return new Promise((resolve, reject) => {
|
|
1204
|
-
const requestId = `auth_${Date.now()}`;
|
|
1205
|
-
const timeout = setTimeout(() => {
|
|
1206
|
-
this.pendingRequests.delete(requestId);
|
|
1207
|
-
this.authPromise = null;
|
|
1208
|
-
reject(new Error("Authentication timeout"));
|
|
1209
|
-
}, 3e4);
|
|
1210
|
-
this.pendingRequests.set(requestId, {
|
|
1211
|
-
resolve: () => {
|
|
1212
|
-
clearTimeout(timeout);
|
|
1213
|
-
this.isAuthenticated = true;
|
|
1214
|
-
resolve();
|
|
1215
|
-
},
|
|
1216
|
-
reject: (error) => {
|
|
1217
|
-
clearTimeout(timeout);
|
|
1218
|
-
reject(error);
|
|
1219
|
-
}
|
|
1220
|
-
});
|
|
1221
|
-
const message = {
|
|
1222
|
-
type: "AUTHENTICATE",
|
|
1223
|
-
requestId,
|
|
1224
|
-
payload: { token }
|
|
1225
|
-
};
|
|
1226
|
-
if (!this.isConnected || !this.ws) {
|
|
1227
|
-
this.messageQueue.unshift(message);
|
|
1228
|
-
} else {
|
|
1229
|
-
this.ws.send(JSON.stringify(message));
|
|
1230
|
-
}
|
|
1231
|
-
});
|
|
1232
|
-
}
|
|
1233
|
-
/**
|
|
1234
|
-
* Set the auth token getter function
|
|
1235
|
-
*/
|
|
1236
|
-
setAuthTokenGetter(getAuthToken) {
|
|
1237
|
-
this.getAuthToken = getAuthToken;
|
|
1238
|
-
if (this.isConnected && !this.isAuthenticated && !this.authPromise) {
|
|
1239
|
-
console.debug("WebSocket auto-authenticating after token getter set");
|
|
1240
|
-
this.getAuthToken().then((token) => {
|
|
1241
|
-
if (!this.ws) return;
|
|
1242
|
-
if (token) {
|
|
1243
|
-
this.authenticate(token).catch((e) => {
|
|
1244
|
-
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
}).catch((e) => {
|
|
1248
|
-
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
disconnect() {
|
|
1253
|
-
this.isAuthenticated = false;
|
|
1254
|
-
this.authPromise = null;
|
|
1255
|
-
if (this.reconnectTimeout) {
|
|
1256
|
-
clearTimeout(this.reconnectTimeout);
|
|
1257
|
-
this.reconnectTimeout = null;
|
|
1258
|
-
}
|
|
1259
|
-
if (this.ws) {
|
|
1260
|
-
this.ws.onclose = null;
|
|
1261
|
-
this.ws.onerror = null;
|
|
1262
|
-
this.ws.onopen = null;
|
|
1263
|
-
this.ws.onmessage = null;
|
|
1264
|
-
this.ws.close();
|
|
1265
|
-
this.ws = null;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
// Initialize WebSocket connection
|
|
1269
|
-
initWebSocket() {
|
|
1270
|
-
if (!this.WebSocketConstructor) return;
|
|
1271
|
-
if (this.ws?.readyState === this.WebSocketConstructor.OPEN) return;
|
|
1272
|
-
try {
|
|
1273
|
-
this.ws = new this.WebSocketConstructor(this.websocketUrl);
|
|
1274
|
-
this.ws.onopen = async () => {
|
|
1275
|
-
console.debug("Connected to PostgreSQL backend");
|
|
1276
|
-
const wasReconnect = this.reconnectAttempts > 0;
|
|
1277
|
-
this.isConnected = true;
|
|
1278
|
-
this.reconnectAttempts = 0;
|
|
1279
|
-
if (this.getAuthToken && !this.isAuthenticated) {
|
|
1280
|
-
try {
|
|
1281
|
-
const token = await this.getAuthToken();
|
|
1282
|
-
if (token) {
|
|
1283
|
-
await this.authenticate(token);
|
|
1284
|
-
console.debug("WebSocket auto-authenticated");
|
|
1285
|
-
}
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
console.debug("WebSocket connected without auth:", error?.message || error);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
1291
|
-
this.processMessageQueue();
|
|
1292
|
-
if (wasReconnect) {
|
|
1293
|
-
this.resubscribeAll();
|
|
1294
|
-
}
|
|
1295
|
-
};
|
|
1296
|
-
this.ws.onmessage = (event) => {
|
|
1297
|
-
try {
|
|
1298
|
-
const message = JSON.parse(event.data, rebaseReviver);
|
|
1299
|
-
this.handleWebSocketMessage(message);
|
|
1300
|
-
} catch (error) {
|
|
1301
|
-
console.error("Error parsing WebSocket message:", error);
|
|
1302
|
-
}
|
|
1303
|
-
};
|
|
1304
|
-
this.ws.onclose = () => {
|
|
1305
|
-
console.debug("Disconnected from PostgreSQL backend");
|
|
1306
|
-
this.isConnected = false;
|
|
1307
|
-
this.isAuthenticated = false;
|
|
1308
|
-
this.authPromise = null;
|
|
1309
|
-
this.emit("disconnect");
|
|
1310
|
-
for (const [reqId, request] of this.pendingRequests.entries()) {
|
|
1311
|
-
if (reqId.startsWith("auth_")) {
|
|
1312
|
-
request.reject(new Error("Connection closed during authentication"));
|
|
1313
|
-
} else if (request.message) {
|
|
1314
|
-
request.message._queuedResolve = request.resolve;
|
|
1315
|
-
request.message._queuedReject = request.reject;
|
|
1316
|
-
this.messageQueue.push(request.message);
|
|
1317
|
-
} else {
|
|
1318
|
-
request.reject(new ApiError("Connection closed", "Connection closed"));
|
|
1319
|
-
}
|
|
1320
|
-
this.pendingRequests.delete(reqId);
|
|
1321
|
-
}
|
|
1322
|
-
this.attemptReconnect();
|
|
1323
|
-
};
|
|
1324
|
-
this.ws.onerror = (error) => {
|
|
1325
|
-
console.error("WebSocket error:", error);
|
|
1326
|
-
this.isConnected = false;
|
|
1327
|
-
this.emit("error", error);
|
|
1328
|
-
};
|
|
1329
|
-
} catch (error) {
|
|
1330
|
-
console.error("Failed to initialize WebSocket:", error);
|
|
1331
|
-
this.attemptReconnect();
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
processMessageQueue() {
|
|
1335
|
-
while (this.messageQueue.length > 0 && this.isConnected) {
|
|
1336
|
-
const message = this.messageQueue.shift();
|
|
1337
|
-
if (message) this.sendMessage(message);
|
|
1338
|
-
}
|
|
1339
|
-
}
|
|
1340
|
-
attemptReconnect() {
|
|
1341
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1342
|
-
console.error("Max reconnection attempts reached");
|
|
1343
|
-
return;
|
|
1344
|
-
}
|
|
1345
|
-
this.reconnectAttempts++;
|
|
1346
|
-
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
1347
|
-
console.debug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
1348
|
-
if (this.reconnectTimeout) {
|
|
1349
|
-
clearTimeout(this.reconnectTimeout);
|
|
1350
|
-
}
|
|
1351
|
-
this.reconnectTimeout = setTimeout(() => {
|
|
1352
|
-
this.reconnectTimeout = null;
|
|
1353
|
-
this.initWebSocket();
|
|
1354
|
-
}, delay);
|
|
1355
|
-
}
|
|
1356
|
-
isAuthError(message) {
|
|
1357
|
-
if (message.type === "AUTH_ERROR") return true;
|
|
1358
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1359
|
-
if (errorCode === "UNAUTHORIZED" || errorCode === "JWT_EXPIRED" || errorCode === "AUTH_ERROR") return true;
|
|
1360
|
-
const lowerMessage = errorMessage.toLowerCase();
|
|
1361
|
-
return lowerMessage.includes("unauthorized") || lowerMessage.includes("token expired") || lowerMessage.includes("token is expired") || lowerMessage.includes("invalid token") || lowerMessage.includes("session expired") || lowerMessage.includes("auth error");
|
|
1362
|
-
}
|
|
1363
|
-
async handleAuthFailure() {
|
|
1364
|
-
if (this.refreshInProgress) {
|
|
1365
|
-
return this.refreshInProgress;
|
|
1366
|
-
}
|
|
1367
|
-
this.refreshInProgress = (async () => {
|
|
1368
|
-
this.isAuthenticated = false;
|
|
1369
|
-
this.authPromise = null;
|
|
1370
|
-
if (this.onUnauthorized) {
|
|
1371
|
-
try {
|
|
1372
|
-
const refreshed = await this.onUnauthorized();
|
|
1373
|
-
if (refreshed && this.getAuthToken) {
|
|
1374
|
-
const token = await this.getAuthToken();
|
|
1375
|
-
if (token) {
|
|
1376
|
-
await this.authenticate(token);
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
console.error("WebSocket auth refresh failed:", error);
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
return false;
|
|
1385
|
-
})();
|
|
1386
|
-
try {
|
|
1387
|
-
return await this.refreshInProgress;
|
|
1388
|
-
} finally {
|
|
1389
|
-
this.refreshInProgress = null;
|
|
1390
|
-
}
|
|
1391
|
-
}
|
|
1392
|
-
handleWebSocketMessage(message) {
|
|
1393
|
-
const {
|
|
1394
|
-
type,
|
|
1395
|
-
requestId,
|
|
1396
|
-
subscriptionId
|
|
1397
|
-
} = message;
|
|
1398
|
-
if (requestId && this.pendingRequests.has(requestId)) {
|
|
1399
|
-
const pendingReq = this.pendingRequests.get(requestId);
|
|
1400
|
-
if (type === "ERROR" || type === "AUTH_ERROR" || message.error) {
|
|
1401
|
-
if (this.isAuthError(message)) {
|
|
1402
|
-
this.pendingRequests.delete(requestId);
|
|
1403
|
-
this.handleAuthFailure().then((refreshed) => {
|
|
1404
|
-
if (refreshed && pendingReq.message) {
|
|
1405
|
-
this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
|
|
1406
|
-
} else {
|
|
1407
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1408
|
-
pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1409
|
-
}
|
|
1410
|
-
}).catch((err) => {
|
|
1411
|
-
pendingReq.reject(err);
|
|
1412
|
-
});
|
|
1413
|
-
} else {
|
|
1414
|
-
this.pendingRequests.delete(requestId);
|
|
1415
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1416
|
-
pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1417
|
-
}
|
|
1418
|
-
} else {
|
|
1419
|
-
this.pendingRequests.delete(requestId);
|
|
1420
|
-
pendingReq.resolve(message.payload || message);
|
|
1421
|
-
}
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
if (subscriptionId && type === "collection_update") {
|
|
1425
|
-
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1426
|
-
if (subscriptionKey) {
|
|
1427
|
-
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1428
|
-
if (collectionSub) {
|
|
1429
|
-
const incomingEntities = (message.entities || []).map((e) => rehydrateEntity(e));
|
|
1430
|
-
const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
|
|
1431
|
-
collectionSub.latestData = entities;
|
|
1432
|
-
collectionSub.lastUpdated = Date.now();
|
|
1433
|
-
collectionSub.isInitialDataReceived = true;
|
|
1434
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1435
|
-
try {
|
|
1436
|
-
callback.onUpdate(entities);
|
|
1437
|
-
} catch (error) {
|
|
1438
|
-
console.error("Error in collection subscription callback:", error);
|
|
1439
|
-
if (callback.onError) {
|
|
1440
|
-
callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
});
|
|
1444
|
-
return;
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
if (subscriptionId && type === "collection_entity_patch") {
|
|
1449
|
-
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1450
|
-
if (subscriptionKey) {
|
|
1451
|
-
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1452
|
-
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
1453
|
-
const patchEntity = message.entity ? rehydrateEntity(message.entity) : message.entity;
|
|
1454
|
-
const patchEntityId = message.entityId;
|
|
1455
|
-
let updated;
|
|
1456
|
-
if (patchEntity === null || patchEntity === void 0) {
|
|
1457
|
-
updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
|
|
1458
|
-
} else {
|
|
1459
|
-
const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchEntity.id));
|
|
1460
|
-
if (idx >= 0) {
|
|
1461
|
-
updated = [...collectionSub.latestData];
|
|
1462
|
-
updated[idx] = patchEntity;
|
|
1463
|
-
} else {
|
|
1464
|
-
updated = [patchEntity, ...collectionSub.latestData];
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
collectionSub.latestData = updated;
|
|
1468
|
-
collectionSub.lastUpdated = Date.now();
|
|
1469
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1470
|
-
try {
|
|
1471
|
-
callback.onUpdate(updated);
|
|
1472
|
-
} catch (error) {
|
|
1473
|
-
console.error("Error in collection patch callback:", error);
|
|
1474
|
-
if (callback.onError) {
|
|
1475
|
-
callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
});
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (subscriptionId && type === "entity_update") {
|
|
1484
|
-
const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
|
|
1485
|
-
if (subscriptionKey) {
|
|
1486
|
-
const entitySub = this.entitySubscriptions.get(subscriptionKey);
|
|
1487
|
-
if (entitySub) {
|
|
1488
|
-
const entity = message.entity ? rehydrateEntity(message.entity) : null;
|
|
1489
|
-
entitySub.latestData = entity;
|
|
1490
|
-
entitySub.lastUpdated = Date.now();
|
|
1491
|
-
entitySub.isInitialDataReceived = true;
|
|
1492
|
-
entitySub.callbacks.forEach((callback) => {
|
|
1493
|
-
try {
|
|
1494
|
-
callback.onUpdate(entity);
|
|
1495
|
-
} catch (error) {
|
|
1496
|
-
console.error("Error in entity subscription callback:", error);
|
|
1497
|
-
if (callback.onError) {
|
|
1498
|
-
callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
});
|
|
1502
|
-
return;
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
if (subscriptionId && (type === "ERROR" || message.error)) {
|
|
1507
|
-
const collectionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1508
|
-
if (collectionKey) {
|
|
1509
|
-
const collectionSub = this.collectionSubscriptions.get(collectionKey);
|
|
1510
|
-
if (collectionSub) {
|
|
1511
|
-
if (this.isAuthError(message)) {
|
|
1512
|
-
this.handleAuthFailure().then((refreshed) => {
|
|
1513
|
-
if (refreshed) {
|
|
1514
|
-
const oldBackendId = collectionSub.backendSubscriptionId;
|
|
1515
|
-
const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1516
|
-
collectionSub.backendSubscriptionId = newBackendId;
|
|
1517
|
-
this.backendToCollectionKey.delete(oldBackendId);
|
|
1518
|
-
this.backendToCollectionKey.set(newBackendId, collectionKey);
|
|
1519
|
-
this.sendMessage({
|
|
1520
|
-
type: "subscribe_collection",
|
|
1521
|
-
payload: {
|
|
1522
|
-
...collectionSub.props,
|
|
1523
|
-
subscriptionId: newBackendId
|
|
1524
|
-
}
|
|
1525
|
-
}).catch((error2) => {
|
|
1526
|
-
console.error("[WS] Failed to re-subscribe collection after auth refresh:", collectionKey, error2);
|
|
1527
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1528
|
-
if (callback.onError) callback.onError(error2);
|
|
1529
|
-
});
|
|
1530
|
-
});
|
|
1531
|
-
} else {
|
|
1532
|
-
const { errorMessage: errorMessage2, errorCode: errorCode2 } = extractMessageError(message);
|
|
1533
|
-
const error2 = new ApiError(errorMessage2, errorMessage2, errorCode2);
|
|
1534
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1535
|
-
if (callback.onError) callback.onError(error2);
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
}).catch((err) => {
|
|
1539
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1540
|
-
if (callback.onError) callback.onError(err);
|
|
1541
|
-
});
|
|
1542
|
-
});
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1546
|
-
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1547
|
-
collectionSub.callbacks.forEach((callback) => {
|
|
1548
|
-
if (callback.onError) {
|
|
1549
|
-
callback.onError(error);
|
|
1550
|
-
}
|
|
1551
|
-
});
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
const entityKey = this.backendToEntityKey.get(subscriptionId);
|
|
1556
|
-
if (entityKey) {
|
|
1557
|
-
const entitySub = this.entitySubscriptions.get(entityKey);
|
|
1558
|
-
if (entitySub) {
|
|
1559
|
-
if (this.isAuthError(message)) {
|
|
1560
|
-
this.handleAuthFailure().then((refreshed) => {
|
|
1561
|
-
if (refreshed) {
|
|
1562
|
-
const oldBackendId = entitySub.backendSubscriptionId;
|
|
1563
|
-
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1564
|
-
entitySub.backendSubscriptionId = newBackendId;
|
|
1565
|
-
this.backendToEntityKey.delete(oldBackendId);
|
|
1566
|
-
this.backendToEntityKey.set(newBackendId, entityKey);
|
|
1567
|
-
this.sendMessage({
|
|
1568
|
-
type: "subscribe_entity",
|
|
1569
|
-
payload: {
|
|
1570
|
-
...entitySub.props,
|
|
1571
|
-
subscriptionId: newBackendId
|
|
1572
|
-
}
|
|
1573
|
-
}).catch((error2) => {
|
|
1574
|
-
console.error("[WS] Failed to re-subscribe entity after auth refresh:", entityKey, error2);
|
|
1575
|
-
entitySub.callbacks.forEach((callback) => {
|
|
1576
|
-
if (callback.onError) callback.onError(error2);
|
|
1577
|
-
});
|
|
1578
|
-
});
|
|
1579
|
-
} else {
|
|
1580
|
-
const { errorMessage: errorMessage2, errorCode: errorCode2 } = extractMessageError(message);
|
|
1581
|
-
const error2 = new ApiError(errorMessage2, errorMessage2, errorCode2);
|
|
1582
|
-
entitySub.callbacks.forEach((callback) => {
|
|
1583
|
-
if (callback.onError) callback.onError(error2);
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
}).catch((err) => {
|
|
1587
|
-
entitySub.callbacks.forEach((callback) => {
|
|
1588
|
-
if (callback.onError) callback.onError(err);
|
|
1589
|
-
});
|
|
1590
|
-
});
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1594
|
-
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1595
|
-
entitySub.callbacks.forEach((callback) => {
|
|
1596
|
-
if (callback.onError) {
|
|
1597
|
-
callback.onError(error);
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
if (subscriptionId && this.subscriptions.has(subscriptionId)) {
|
|
1605
|
-
const callback = this.subscriptions.get(subscriptionId);
|
|
1606
|
-
if (!callback) {
|
|
1607
|
-
throw new Error(`Subscription callback not found for subscriptionId: ${subscriptionId}`);
|
|
1608
|
-
}
|
|
1609
|
-
if (message.type === "ERROR" || message.error) {
|
|
1610
|
-
if (callback.onError) {
|
|
1611
|
-
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1612
|
-
callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1613
|
-
}
|
|
1614
|
-
} else {
|
|
1615
|
-
callback.onUpdate(message);
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
}
|
|
1619
|
-
async ensureAuthenticated(retryCount = 3) {
|
|
1620
|
-
if (this.isAuthenticated || !this.getAuthToken) return;
|
|
1621
|
-
if (this.authPromise) {
|
|
1622
|
-
await this.authPromise;
|
|
1623
|
-
return;
|
|
1624
|
-
}
|
|
1625
|
-
let lastError = null;
|
|
1626
|
-
for (let attempt = 0; attempt < retryCount; attempt++) {
|
|
1627
|
-
try {
|
|
1628
|
-
const token = await this.getAuthToken();
|
|
1629
|
-
if (!token) throw new Error("user not logged in");
|
|
1630
|
-
this.authPromise = this.authenticate(token);
|
|
1631
|
-
await this.authPromise;
|
|
1632
|
-
this.authPromise = null;
|
|
1633
|
-
console.debug("WebSocket authenticated on demand");
|
|
1634
|
-
return;
|
|
1635
|
-
} catch (error) {
|
|
1636
|
-
this.authPromise = null;
|
|
1637
|
-
lastError = error;
|
|
1638
|
-
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1639
|
-
if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
|
|
1640
|
-
console.warn("WebSocket auth failed: user not logged in");
|
|
1641
|
-
throw error;
|
|
1642
|
-
}
|
|
1643
|
-
if (errMsg.includes("still loading")) {
|
|
1644
|
-
if (attempt < retryCount - 1) {
|
|
1645
|
-
const delay = Math.min(500 * (attempt + 1), 2e3);
|
|
1646
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1647
|
-
continue;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
if (attempt < retryCount - 1) {
|
|
1651
|
-
const delay = Math.min(1e3 * (attempt + 1), 3e3);
|
|
1652
|
-
console.debug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
1653
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
console.warn("WebSocket on-demand auth failed after retries:", lastError);
|
|
1658
|
-
throw lastError;
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* Force re-authentication (call after token refresh)
|
|
1662
|
-
*/
|
|
1663
|
-
async reauthenticate() {
|
|
1664
|
-
if (!this.getAuthToken) return;
|
|
1665
|
-
this.isAuthenticated = false;
|
|
1666
|
-
try {
|
|
1667
|
-
const token = await this.getAuthToken();
|
|
1668
|
-
await this.authenticate(token);
|
|
1669
|
-
console.debug("WebSocket reauthenticated successfully");
|
|
1670
|
-
} catch (error) {
|
|
1671
|
-
console.error("WebSocket reauthentication failed:", error);
|
|
1672
|
-
throw error;
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
sendMessage(message) {
|
|
1676
|
-
const queuedMsg = message;
|
|
1677
|
-
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) {
|
|
1678
|
-
return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
1679
|
-
}
|
|
1680
|
-
if (!this.isConnected || !this.ws) {
|
|
1681
|
-
return new Promise((resolve, reject) => {
|
|
1682
|
-
const queueable = message;
|
|
1683
|
-
queueable._queuedResolve = resolve;
|
|
1684
|
-
queueable._queuedReject = reject;
|
|
1685
|
-
this.messageQueue.push(message);
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
return new Promise((resolve, reject) => {
|
|
1689
|
-
this.doSendMessage(message, resolve, reject);
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
async doSendMessage(message, resolve, reject) {
|
|
1693
|
-
if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) {
|
|
1694
|
-
try {
|
|
1695
|
-
await this.ensureAuthenticated();
|
|
1696
|
-
} catch (error) {
|
|
1697
|
-
const errorMessage = error instanceof Error ? error.message : "Authentication required";
|
|
1698
|
-
reject(new ApiError(errorMessage, errorMessage));
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1703
|
-
message.requestId = requestId;
|
|
1704
|
-
if (!this.pendingRequests.has(requestId)) {
|
|
1705
|
-
this.pendingRequests.set(requestId, {
|
|
1706
|
-
resolve,
|
|
1707
|
-
reject,
|
|
1708
|
-
message
|
|
1709
|
-
});
|
|
1710
|
-
}
|
|
1711
|
-
try {
|
|
1712
|
-
this.ws.send(JSON.stringify(message));
|
|
1713
|
-
} catch (error) {
|
|
1714
|
-
this.pendingRequests.delete(requestId);
|
|
1715
|
-
reject(new ApiError("Failed to send message", error instanceof Error ? error.message : "Unknown error"));
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
// Data source methods
|
|
1719
|
-
async fetchCollection(props) {
|
|
1720
|
-
const response = await this.sendMessage({
|
|
1721
|
-
type: "FETCH_COLLECTION",
|
|
1722
|
-
payload: props
|
|
1723
|
-
});
|
|
1724
|
-
return (response.entities || []).map((e) => rehydrateEntity(e));
|
|
1725
|
-
}
|
|
1726
|
-
async fetchEntity(props) {
|
|
1727
|
-
const response = await this.sendMessage({
|
|
1728
|
-
type: "FETCH_ENTITY",
|
|
1729
|
-
payload: props
|
|
1730
|
-
});
|
|
1731
|
-
return response.entity ? rehydrateEntity(response.entity) : void 0;
|
|
1732
|
-
}
|
|
1733
|
-
async saveEntity(props) {
|
|
1734
|
-
const response = await this.sendMessage({
|
|
1735
|
-
type: "SAVE_ENTITY",
|
|
1736
|
-
payload: props
|
|
1737
|
-
});
|
|
1738
|
-
return rehydrateEntity(response.entity);
|
|
1739
|
-
}
|
|
1740
|
-
async deleteEntity(props) {
|
|
1741
|
-
await this.sendMessage({
|
|
1742
|
-
type: "DELETE_ENTITY",
|
|
1743
|
-
payload: props
|
|
1744
|
-
});
|
|
1745
|
-
}
|
|
1746
|
-
async executeSql(sql, options) {
|
|
1747
|
-
const response = await this.sendMessage({
|
|
1748
|
-
type: "EXECUTE_SQL",
|
|
1749
|
-
payload: {
|
|
1750
|
-
sql,
|
|
1751
|
-
options
|
|
1752
|
-
}
|
|
1753
|
-
});
|
|
1754
|
-
return response.result || [];
|
|
1755
|
-
}
|
|
1756
|
-
async fetchAvailableDatabases() {
|
|
1757
|
-
const response = await this.sendMessage({
|
|
1758
|
-
type: "FETCH_DATABASES",
|
|
1759
|
-
payload: {}
|
|
1760
|
-
});
|
|
1761
|
-
return response.databases || [];
|
|
1762
|
-
}
|
|
1763
|
-
async fetchAvailableRoles() {
|
|
1764
|
-
const response = await this.sendMessage({
|
|
1765
|
-
type: "FETCH_ROLES"
|
|
1766
|
-
});
|
|
1767
|
-
return response.roles || [];
|
|
1768
|
-
}
|
|
1769
|
-
async fetchCurrentDatabase() {
|
|
1770
|
-
const response = await this.sendMessage({
|
|
1771
|
-
type: "FETCH_CURRENT_DATABASE"
|
|
1772
|
-
});
|
|
1773
|
-
return response.database;
|
|
1774
|
-
}
|
|
1775
|
-
async checkUniqueField(path, name, value, entityId, collection) {
|
|
1776
|
-
const response = await this.sendMessage({
|
|
1777
|
-
type: "CHECK_UNIQUE_FIELD",
|
|
1778
|
-
payload: {
|
|
1779
|
-
path,
|
|
1780
|
-
name,
|
|
1781
|
-
value,
|
|
1782
|
-
entityId,
|
|
1783
|
-
collection
|
|
1784
|
-
}
|
|
1785
|
-
});
|
|
1786
|
-
return response.isUnique;
|
|
1787
|
-
}
|
|
1788
|
-
async countEntities(props) {
|
|
1789
|
-
const response = await this.sendMessage({
|
|
1790
|
-
type: "COUNT_ENTITIES",
|
|
1791
|
-
payload: props
|
|
1792
|
-
});
|
|
1793
|
-
return response.count;
|
|
1794
|
-
}
|
|
1795
|
-
async fetchUnmappedTables(mappedPaths) {
|
|
1796
|
-
const response = await this.sendMessage({
|
|
1797
|
-
type: "FETCH_UNMAPPED_TABLES",
|
|
1798
|
-
payload: { mappedPaths }
|
|
1799
|
-
});
|
|
1800
|
-
return response.tables || [];
|
|
1801
|
-
}
|
|
1802
|
-
async fetchTableMetadata(tableName) {
|
|
1803
|
-
const response = await this.sendMessage({
|
|
1804
|
-
type: "FETCH_TABLE_METADATA",
|
|
1805
|
-
payload: { tableName }
|
|
1806
|
-
});
|
|
1807
|
-
return response.metadata || {
|
|
1808
|
-
columns: [],
|
|
1809
|
-
foreignKeys: [],
|
|
1810
|
-
junctions: [],
|
|
1811
|
-
policies: []
|
|
1812
|
-
};
|
|
1813
|
-
}
|
|
1814
|
-
async createBranch(name, options) {
|
|
1815
|
-
const response = await this.sendMessage({
|
|
1816
|
-
type: "CREATE_BRANCH",
|
|
1817
|
-
payload: {
|
|
1818
|
-
name,
|
|
1819
|
-
options
|
|
1820
|
-
}
|
|
1821
|
-
});
|
|
1822
|
-
return response.branch;
|
|
1823
|
-
}
|
|
1824
|
-
async deleteBranch(name) {
|
|
1825
|
-
await this.sendMessage({
|
|
1826
|
-
type: "DELETE_BRANCH",
|
|
1827
|
-
payload: { name }
|
|
1828
|
-
});
|
|
1829
|
-
}
|
|
1830
|
-
async listBranches() {
|
|
1831
|
-
const response = await this.sendMessage({
|
|
1832
|
-
type: "LIST_BRANCHES",
|
|
1833
|
-
payload: {}
|
|
1834
|
-
});
|
|
1835
|
-
return response.branches || [];
|
|
1836
|
-
}
|
|
1837
|
-
/**
|
|
1838
|
-
* Recursively compare two values for structural equality.
|
|
1839
|
-
* Handles primitives, null, undefined, Date, RegExp, arrays, and plain objects.
|
|
1840
|
-
*/
|
|
1841
|
-
deepEqual(a, b) {
|
|
1842
|
-
if (a === b) return true;
|
|
1843
|
-
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
1844
|
-
if (typeof a !== typeof b) return false;
|
|
1845
|
-
if (typeof a !== "object") return false;
|
|
1846
|
-
if (a instanceof Date && b instanceof Date) {
|
|
1847
|
-
return a.getTime() === b.getTime();
|
|
1848
|
-
}
|
|
1849
|
-
if (a instanceof Date || b instanceof Date) return false;
|
|
1850
|
-
if (a instanceof RegExp && b instanceof RegExp) {
|
|
1851
|
-
return a.source === b.source && a.flags === b.flags;
|
|
1852
|
-
}
|
|
1853
|
-
if (a instanceof RegExp || b instanceof RegExp) return false;
|
|
1854
|
-
const aIsArray = Array.isArray(a);
|
|
1855
|
-
const bIsArray = Array.isArray(b);
|
|
1856
|
-
if (aIsArray !== bIsArray) return false;
|
|
1857
|
-
if (aIsArray && bIsArray) {
|
|
1858
|
-
if (a.length !== b.length) return false;
|
|
1859
|
-
for (let i = 0; i < a.length; i++) {
|
|
1860
|
-
if (!this.deepEqual(a[i], b[i])) return false;
|
|
1861
|
-
}
|
|
1862
|
-
return true;
|
|
1863
|
-
}
|
|
1864
|
-
const aObj = a;
|
|
1865
|
-
const bObj = b;
|
|
1866
|
-
const aKeys = Object.keys(aObj);
|
|
1867
|
-
const bKeys = Object.keys(bObj);
|
|
1868
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
1869
|
-
for (const key of aKeys) {
|
|
1870
|
-
if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
|
|
1871
|
-
if (!this.deepEqual(aObj[key], bObj[key])) return false;
|
|
1872
|
-
}
|
|
1873
|
-
return true;
|
|
1874
|
-
}
|
|
1875
|
-
normalizeForComparison(val) {
|
|
1876
|
-
if (!val) return val;
|
|
1877
|
-
if (Array.isArray(val)) {
|
|
1878
|
-
return val.map((item) => this.normalizeForComparison(item));
|
|
1879
|
-
}
|
|
1880
|
-
if (typeof val === "object") {
|
|
1881
|
-
if (val instanceof Date) return val;
|
|
1882
|
-
if (val instanceof RegExp) return val;
|
|
1883
|
-
const obj = val;
|
|
1884
|
-
if (obj.__type === "relation") {
|
|
1885
|
-
const { data, ...rest } = obj;
|
|
1886
|
-
return rest;
|
|
1887
|
-
}
|
|
1888
|
-
const result = {};
|
|
1889
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
1890
|
-
result[k] = this.normalizeForComparison(v);
|
|
1891
|
-
}
|
|
1892
|
-
return result;
|
|
1893
|
-
}
|
|
1894
|
-
return val;
|
|
1895
|
-
}
|
|
1896
|
-
/**
|
|
1897
|
-
* Merge incoming entities with cached data, preserving cached references
|
|
1898
|
-
* for entities whose values haven't changed. This avoids unnecessary
|
|
1899
|
-
* React re-renders when the server refetches all entities but most
|
|
1900
|
-
* haven't actually changed.
|
|
1901
|
-
*/
|
|
1902
|
-
mergeEntities(cached, incoming) {
|
|
1903
|
-
if (!cached || cached.length === 0) return incoming;
|
|
1904
|
-
const cachedById = /* @__PURE__ */ new Map();
|
|
1905
|
-
for (const entity of cached) {
|
|
1906
|
-
cachedById.set(entity.id, entity);
|
|
1907
|
-
}
|
|
1908
|
-
return incoming.map((incomingEntity) => {
|
|
1909
|
-
const cachedEntity = cachedById.get(incomingEntity.id);
|
|
1910
|
-
if (!cachedEntity) return incomingEntity;
|
|
1911
|
-
if (cachedEntity.path === incomingEntity.path) {
|
|
1912
|
-
const normCached = this.normalizeForComparison(cachedEntity.values);
|
|
1913
|
-
const normIncoming = this.normalizeForComparison(incomingEntity.values);
|
|
1914
|
-
if (this.deepEqual(normCached, normIncoming)) {
|
|
1915
|
-
return cachedEntity;
|
|
1916
|
-
} else {
|
|
1917
|
-
const mismatches = {};
|
|
1918
|
-
const allKeys = /* @__PURE__ */ new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
1919
|
-
for (const key of allKeys) {
|
|
1920
|
-
if (!this.deepEqual(normCached[key], normIncoming[key])) {
|
|
1921
|
-
mismatches[key] = {
|
|
1922
|
-
cached: normCached[key],
|
|
1923
|
-
incoming: normIncoming[key]
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:
|
|
1928
|
-
`, JSON.stringify(mismatches, null, 2));
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
return incomingEntity;
|
|
1932
|
-
});
|
|
1933
|
-
}
|
|
1934
|
-
// Subscription methods
|
|
1935
|
-
listenCollection(props, onUpdate, onError) {
|
|
1936
|
-
const subscriptionKey = this.createCollectionSubscriptionKey(props);
|
|
1937
|
-
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1938
|
-
const existingSubscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
1939
|
-
if (existingSubscription) {
|
|
1940
|
-
const callbackMap2 = existingSubscription.callbacks;
|
|
1941
|
-
callbackMap2.set(callbackId, {
|
|
1942
|
-
onUpdate,
|
|
1943
|
-
onError
|
|
1944
|
-
});
|
|
1945
|
-
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) {
|
|
1946
|
-
try {
|
|
1947
|
-
onUpdate(existingSubscription.latestData);
|
|
1948
|
-
} catch (error) {
|
|
1949
|
-
console.error("Error in collection subscription callback:", error);
|
|
1950
|
-
if (onError) {
|
|
1951
|
-
onError(error instanceof Error ? error : new Error(String(error)));
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
return () => {
|
|
1956
|
-
callbackMap2.delete(callbackId);
|
|
1957
|
-
if (callbackMap2.size === 0) {
|
|
1958
|
-
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1959
|
-
this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
|
|
1960
|
-
if (this.isConnected && this.ws) {
|
|
1961
|
-
this.sendMessage({
|
|
1962
|
-
type: "unsubscribe",
|
|
1963
|
-
payload: { subscriptionId: existingSubscription.backendSubscriptionId }
|
|
1964
|
-
}).catch(console.error);
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
};
|
|
1968
|
-
}
|
|
1969
|
-
const backendSubscriptionId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1970
|
-
const callbackMap = /* @__PURE__ */ new Map();
|
|
1971
|
-
callbackMap.set(callbackId, {
|
|
1972
|
-
onUpdate,
|
|
1973
|
-
onError
|
|
1974
|
-
});
|
|
1975
|
-
this.collectionSubscriptions.set(subscriptionKey, {
|
|
1976
|
-
backendSubscriptionId,
|
|
1977
|
-
callbacks: callbackMap,
|
|
1978
|
-
props
|
|
1979
|
-
});
|
|
1980
|
-
this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
|
|
1981
|
-
this.sendMessage({
|
|
1982
|
-
type: "subscribe_collection",
|
|
1983
|
-
payload: {
|
|
1984
|
-
...props,
|
|
1985
|
-
subscriptionId: backendSubscriptionId
|
|
1986
|
-
}
|
|
1987
|
-
}).catch((error) => {
|
|
1988
|
-
if (onError) onError(error);
|
|
1989
|
-
});
|
|
1990
|
-
return () => {
|
|
1991
|
-
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
1992
|
-
if (subscription) {
|
|
1993
|
-
const callbacks = subscription.callbacks;
|
|
1994
|
-
callbacks.delete(callbackId);
|
|
1995
|
-
if (callbacks.size === 0) {
|
|
1996
|
-
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1997
|
-
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
1998
|
-
if (this.isConnected && this.ws) {
|
|
1999
|
-
this.sendMessage({
|
|
2000
|
-
type: "unsubscribe",
|
|
2001
|
-
payload: { subscriptionId: subscription.backendSubscriptionId }
|
|
2002
|
-
}).catch(console.error);
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
};
|
|
2007
|
-
}
|
|
2008
|
-
listenEntity(props, onUpdate, onError) {
|
|
2009
|
-
const subscriptionKey = this.createEntitySubscriptionKey(props);
|
|
2010
|
-
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2011
|
-
const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
|
|
2012
|
-
if (existingSubscription) {
|
|
2013
|
-
const callbackMap2 = existingSubscription.callbacks;
|
|
2014
|
-
callbackMap2.set(callbackId, {
|
|
2015
|
-
onUpdate,
|
|
2016
|
-
onError
|
|
2017
|
-
});
|
|
2018
|
-
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) {
|
|
2019
|
-
try {
|
|
2020
|
-
onUpdate(existingSubscription.latestData);
|
|
2021
|
-
} catch (error) {
|
|
2022
|
-
console.error("Error in entity subscription callback:", error);
|
|
2023
|
-
if (onError) {
|
|
2024
|
-
onError(error instanceof Error ? error : new Error(String(error)));
|
|
2025
|
-
}
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
return () => {
|
|
2029
|
-
callbackMap2.delete(callbackId);
|
|
2030
|
-
if (callbackMap2.size === 0) {
|
|
2031
|
-
this.entitySubscriptions.delete(subscriptionKey);
|
|
2032
|
-
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
2033
|
-
if (this.isConnected && this.ws) {
|
|
2034
|
-
this.sendMessage({
|
|
2035
|
-
type: "unsubscribe",
|
|
2036
|
-
payload: { subscriptionId: existingSubscription.backendSubscriptionId }
|
|
2037
|
-
}).catch(console.error);
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
};
|
|
2041
|
-
}
|
|
2042
|
-
const backendSubscriptionId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2043
|
-
const callbackMap = /* @__PURE__ */ new Map();
|
|
2044
|
-
callbackMap.set(callbackId, {
|
|
2045
|
-
onUpdate,
|
|
2046
|
-
onError
|
|
2047
|
-
});
|
|
2048
|
-
this.entitySubscriptions.set(subscriptionKey, {
|
|
2049
|
-
backendSubscriptionId,
|
|
2050
|
-
callbacks: callbackMap,
|
|
2051
|
-
props
|
|
2052
|
-
});
|
|
2053
|
-
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
2054
|
-
this.sendMessage({
|
|
2055
|
-
type: "subscribe_entity",
|
|
2056
|
-
payload: {
|
|
2057
|
-
...props,
|
|
2058
|
-
subscriptionId: backendSubscriptionId
|
|
2059
|
-
}
|
|
2060
|
-
}).catch((error) => {
|
|
2061
|
-
if (onError) onError(error);
|
|
2062
|
-
});
|
|
2063
|
-
return () => {
|
|
2064
|
-
const subscription = this.entitySubscriptions.get(subscriptionKey);
|
|
2065
|
-
if (subscription) {
|
|
2066
|
-
const callbacks = subscription.callbacks;
|
|
2067
|
-
callbacks.delete(callbackId);
|
|
2068
|
-
if (callbacks.size === 0) {
|
|
2069
|
-
this.entitySubscriptions.delete(subscriptionKey);
|
|
2070
|
-
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
2071
|
-
if (this.isConnected && this.ws) {
|
|
2072
|
-
this.sendMessage({
|
|
2073
|
-
type: "unsubscribe",
|
|
2074
|
-
payload: { subscriptionId: subscription.backendSubscriptionId }
|
|
2075
|
-
}).catch(console.error);
|
|
2076
|
-
}
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
/**
|
|
2082
|
-
* Re-send all active subscriptions to the backend after a reconnect.
|
|
2083
|
-
* The server wipes subscription state when a client disconnects, so
|
|
2084
|
-
* we need to re-register everything to resume receiving updates.
|
|
2085
|
-
*/
|
|
2086
|
-
resubscribeAll() {
|
|
2087
|
-
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
|
|
2088
|
-
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
2089
|
-
const oldBackendId = sub.backendSubscriptionId;
|
|
2090
|
-
const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2091
|
-
sub.backendSubscriptionId = newBackendId;
|
|
2092
|
-
this.backendToCollectionKey.delete(oldBackendId);
|
|
2093
|
-
this.backendToCollectionKey.set(newBackendId, key);
|
|
2094
|
-
this.sendMessage({
|
|
2095
|
-
type: "subscribe_collection",
|
|
2096
|
-
payload: {
|
|
2097
|
-
...sub.props,
|
|
2098
|
-
subscriptionId: newBackendId
|
|
2099
|
-
}
|
|
2100
|
-
}).catch((error) => {
|
|
2101
|
-
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
2102
|
-
});
|
|
2103
|
-
}
|
|
2104
|
-
for (const [key, sub] of this.entitySubscriptions.entries()) {
|
|
2105
|
-
const oldBackendId = sub.backendSubscriptionId;
|
|
2106
|
-
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
2107
|
-
sub.backendSubscriptionId = newBackendId;
|
|
2108
|
-
this.backendToEntityKey.delete(oldBackendId);
|
|
2109
|
-
this.backendToEntityKey.set(newBackendId, key);
|
|
2110
|
-
this.sendMessage({
|
|
2111
|
-
type: "subscribe_entity",
|
|
2112
|
-
payload: {
|
|
2113
|
-
...sub.props,
|
|
2114
|
-
subscriptionId: newBackendId
|
|
2115
|
-
}
|
|
2116
|
-
}).catch((error) => {
|
|
2117
|
-
console.error("[WS] Failed to re-subscribe entity:", key, error);
|
|
2118
|
-
});
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
createCollectionSubscriptionKey(props) {
|
|
2122
|
-
const key = {
|
|
2123
|
-
path: props.path,
|
|
2124
|
-
filter: props.filter,
|
|
2125
|
-
limit: props.limit,
|
|
2126
|
-
startAfter: props.startAfter,
|
|
2127
|
-
orderBy: props.orderBy,
|
|
2128
|
-
order: props.order,
|
|
2129
|
-
searchString: props.searchString,
|
|
2130
|
-
collection: props.collection?.name
|
|
2131
|
-
};
|
|
2132
|
-
return JSON.stringify(key, (_, value) => {
|
|
2133
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
2134
|
-
return Object.keys(value).sort().reduce((sorted, k) => {
|
|
2135
|
-
sorted[k] = value[k];
|
|
2136
|
-
return sorted;
|
|
2137
|
-
}, {});
|
|
2138
|
-
}
|
|
2139
|
-
return value;
|
|
2140
|
-
});
|
|
2141
|
-
}
|
|
2142
|
-
createEntitySubscriptionKey(props) {
|
|
2143
|
-
return `${props.path}|${props.entityId}`;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
function createStorage(transport) {
|
|
2147
|
-
const urlsCache = /* @__PURE__ */ new Map();
|
|
2148
|
-
async function putObject({
|
|
2149
|
-
file,
|
|
2150
|
-
key,
|
|
2151
|
-
metadata,
|
|
2152
|
-
bucket
|
|
2153
|
-
}) {
|
|
2154
|
-
const formData = new FormData();
|
|
2155
|
-
formData.append("file", file);
|
|
2156
|
-
if (key) formData.append("key", key);
|
|
2157
|
-
if (bucket) formData.append("bucket", bucket);
|
|
2158
|
-
if (metadata) {
|
|
2159
|
-
for (const [key2, value] of Object.entries(metadata)) {
|
|
2160
|
-
if (value !== void 0 && value !== null) {
|
|
2161
|
-
formData.append(
|
|
2162
|
-
`metadata_${key2}`,
|
|
2163
|
-
typeof value === "string" ? value : JSON.stringify(value)
|
|
2164
|
-
);
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
const result = await transport.request("/storage/upload", {
|
|
2169
|
-
method: "POST",
|
|
2170
|
-
body: formData,
|
|
2171
|
-
headers: {
|
|
2172
|
-
// transport.request merges headers, so to prevent it setting application/json we can delete it
|
|
2173
|
-
// in transport if body is FormData, or we can explicitly set it to an empty string.
|
|
2174
|
-
// Let's rely on standard behaviour for now and adjust transport if it fails.
|
|
2175
|
-
}
|
|
2176
|
-
});
|
|
2177
|
-
return result.data;
|
|
2178
|
-
}
|
|
2179
|
-
async function getSignedUrl(keyOrUrl, bucket) {
|
|
2180
|
-
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
2181
|
-
const cached = urlsCache.get(cacheKey);
|
|
2182
|
-
if (cached) return cached;
|
|
2183
|
-
let filePath = keyOrUrl;
|
|
2184
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
2185
|
-
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
2186
|
-
}
|
|
2187
|
-
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
2188
|
-
filePath = `${bucket}/${filePath}`;
|
|
2189
|
-
}
|
|
2190
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
2191
|
-
return {
|
|
2192
|
-
url: null,
|
|
2193
|
-
fileNotFound: true
|
|
2194
|
-
};
|
|
2195
|
-
}
|
|
2196
|
-
try {
|
|
2197
|
-
const result = await transport.request(`/storage/metadata/${filePath}`);
|
|
2198
|
-
const activeToken = await transport.resolveToken();
|
|
2199
|
-
const tokenQuery = activeToken ? `?token=${activeToken}` : "";
|
|
2200
|
-
const downloadConfig = {
|
|
2201
|
-
url: `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}${tokenQuery}`,
|
|
2202
|
-
metadata: result.data
|
|
2203
|
-
};
|
|
2204
|
-
urlsCache.set(cacheKey, downloadConfig);
|
|
2205
|
-
return downloadConfig;
|
|
2206
|
-
} catch (e) {
|
|
2207
|
-
if (e instanceof Error && "status" in e && e.status === 404) {
|
|
2208
|
-
return {
|
|
2209
|
-
url: null,
|
|
2210
|
-
fileNotFound: true
|
|
2211
|
-
};
|
|
2212
|
-
}
|
|
2213
|
-
throw e;
|
|
2214
|
-
}
|
|
2215
|
-
}
|
|
2216
|
-
async function getObject(key, bucket) {
|
|
2217
|
-
let filePath = key;
|
|
2218
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
2219
|
-
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
2220
|
-
}
|
|
2221
|
-
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
2222
|
-
filePath = `${bucket}/${filePath}`;
|
|
2223
|
-
}
|
|
2224
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
2225
|
-
return null;
|
|
2226
|
-
}
|
|
2227
|
-
const url = `${transport.baseUrl}${transport.apiPath}/storage/file/${filePath}`;
|
|
2228
|
-
const response = await transport.fetchFn(url, {
|
|
2229
|
-
headers: transport.getHeaders ? transport.getHeaders() : {}
|
|
2230
|
-
});
|
|
2231
|
-
if (response.status === 404) return null;
|
|
2232
|
-
if (!response.ok) throw new Error("Failed to get file");
|
|
2233
|
-
const blob = await response.blob();
|
|
2234
|
-
const fileName = filePath.split("/").pop() || "file";
|
|
2235
|
-
return new File([blob], fileName, { type: blob.type });
|
|
2236
|
-
}
|
|
2237
|
-
async function deleteObject(key, bucket) {
|
|
2238
|
-
let filePath = key;
|
|
2239
|
-
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
2240
|
-
filePath = filePath.substring(filePath.indexOf("://") + 3);
|
|
2241
|
-
}
|
|
2242
|
-
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
2243
|
-
filePath = `${bucket}/${filePath}`;
|
|
2244
|
-
}
|
|
2245
|
-
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
try {
|
|
2249
|
-
await transport.request(`/storage/file/${filePath}`, { method: "DELETE" });
|
|
2250
|
-
} catch (e) {
|
|
2251
|
-
if (!(e instanceof Error && "status" in e && e.status === 404)) throw e;
|
|
2252
|
-
}
|
|
2253
|
-
urlsCache.delete(bucket ? `${bucket}/${key}` : key);
|
|
2254
|
-
}
|
|
2255
|
-
async function listObjects(prefix, options) {
|
|
2256
|
-
const params = new URLSearchParams();
|
|
2257
|
-
if (prefix) params.set("prefix", prefix);
|
|
2258
|
-
if (options?.bucket) params.set("bucket", options.bucket);
|
|
2259
|
-
if (options?.maxResults) params.set("maxResults", String(options.maxResults));
|
|
2260
|
-
if (options?.pageToken) params.set("pageToken", options.pageToken);
|
|
2261
|
-
const result = await transport.request(`/storage/list?${params.toString()}`);
|
|
2262
|
-
return result.data;
|
|
2263
|
-
}
|
|
2264
|
-
return {
|
|
2265
|
-
putObject,
|
|
2266
|
-
getSignedUrl,
|
|
2267
|
-
getObject,
|
|
2268
|
-
deleteObject,
|
|
2269
|
-
listObjects
|
|
2270
|
-
};
|
|
1112
|
+
const payload = message.payload;
|
|
1113
|
+
const errPayload = payload?.error;
|
|
1114
|
+
return {
|
|
1115
|
+
errorMessage: typeof errPayload === "object" ? errPayload.message : payload?.message || (typeof errPayload === "string" ? errPayload : void 0) || message.error || "Unknown error",
|
|
1116
|
+
errorCode: typeof errPayload === "object" ? errPayload.code : payload?.code
|
|
1117
|
+
};
|
|
2271
1118
|
}
|
|
1119
|
+
var ApiError = class extends Error {
|
|
1120
|
+
code;
|
|
1121
|
+
error;
|
|
1122
|
+
constructor(message, error, code) {
|
|
1123
|
+
super(message);
|
|
1124
|
+
this.name = "ApiError";
|
|
1125
|
+
this.code = code;
|
|
1126
|
+
this.error = error;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
var RebaseWebSocketClient = class {
|
|
1130
|
+
websocketUrl;
|
|
1131
|
+
ws = null;
|
|
1132
|
+
getAuthToken;
|
|
1133
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
1134
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1135
|
+
on(event, cb) {
|
|
1136
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
1137
|
+
this.listeners.get(event).add(cb);
|
|
1138
|
+
return () => this.listeners.get(event).delete(cb);
|
|
1139
|
+
}
|
|
1140
|
+
emit(event, ...args) {
|
|
1141
|
+
if (this.listeners.has(event)) this.listeners.get(event).forEach((cb) => cb(...args));
|
|
1142
|
+
}
|
|
1143
|
+
collectionSubscriptions = /* @__PURE__ */ new Map();
|
|
1144
|
+
entitySubscriptions = /* @__PURE__ */ new Map();
|
|
1145
|
+
backendToCollectionKey = /* @__PURE__ */ new Map();
|
|
1146
|
+
backendToEntityKey = /* @__PURE__ */ new Map();
|
|
1147
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1148
|
+
reconnectAttempts = 0;
|
|
1149
|
+
maxReconnectAttempts = 5;
|
|
1150
|
+
isConnected = false;
|
|
1151
|
+
messageQueue = [];
|
|
1152
|
+
requestTimeoutMs = 3e4;
|
|
1153
|
+
reconnectTimeout = null;
|
|
1154
|
+
isAuthenticated = false;
|
|
1155
|
+
authPromise = null;
|
|
1156
|
+
WebSocketConstructor;
|
|
1157
|
+
onUnauthorized;
|
|
1158
|
+
refreshInProgress = null;
|
|
1159
|
+
constructor(config) {
|
|
1160
|
+
this.websocketUrl = config.websocketUrl;
|
|
1161
|
+
this.getAuthToken = config.getAuthToken;
|
|
1162
|
+
this.onUnauthorized = config.onUnauthorized;
|
|
1163
|
+
this.WebSocketConstructor = config.WebSocket || (typeof WebSocket !== "undefined" ? WebSocket : void 0);
|
|
1164
|
+
if (!this.WebSocketConstructor) console.warn("WebSocket is not defined in this environment. Realtime subscriptions will not work unless you provide a WebSocket implementation in the config.");
|
|
1165
|
+
else this.initWebSocket();
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Authenticate the WebSocket connection
|
|
1169
|
+
*/
|
|
1170
|
+
async authenticate(token) {
|
|
1171
|
+
return new Promise((resolve, reject) => {
|
|
1172
|
+
const requestId = `auth_${Date.now()}`;
|
|
1173
|
+
const timeout = setTimeout(() => {
|
|
1174
|
+
this.pendingRequests.delete(requestId);
|
|
1175
|
+
this.authPromise = null;
|
|
1176
|
+
reject(/* @__PURE__ */ new Error("Authentication timeout"));
|
|
1177
|
+
}, 3e4);
|
|
1178
|
+
this.pendingRequests.set(requestId, {
|
|
1179
|
+
resolve: () => {
|
|
1180
|
+
clearTimeout(timeout);
|
|
1181
|
+
this.isAuthenticated = true;
|
|
1182
|
+
resolve();
|
|
1183
|
+
},
|
|
1184
|
+
reject: (error) => {
|
|
1185
|
+
clearTimeout(timeout);
|
|
1186
|
+
reject(error);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
const message = {
|
|
1190
|
+
type: "AUTHENTICATE",
|
|
1191
|
+
requestId,
|
|
1192
|
+
payload: { token }
|
|
1193
|
+
};
|
|
1194
|
+
if (!this.isConnected || !this.ws) this.messageQueue.unshift(message);
|
|
1195
|
+
else this.ws.send(JSON.stringify(message));
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Set the auth token getter function
|
|
1200
|
+
*/
|
|
1201
|
+
setAuthTokenGetter(getAuthToken) {
|
|
1202
|
+
this.getAuthToken = getAuthToken;
|
|
1203
|
+
if (this.isConnected && !this.isAuthenticated && !this.authPromise) {
|
|
1204
|
+
console.debug("WebSocket auto-authenticating after token getter set");
|
|
1205
|
+
this.getAuthToken().then((token) => {
|
|
1206
|
+
if (!this.ws) return;
|
|
1207
|
+
if (token) this.authenticate(token).catch((e) => {
|
|
1208
|
+
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
1209
|
+
});
|
|
1210
|
+
}).catch((e) => {
|
|
1211
|
+
if (this.ws) console.debug("WebSocket auto-auth skipped:", e?.message || e);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
disconnect() {
|
|
1216
|
+
this.isAuthenticated = false;
|
|
1217
|
+
this.authPromise = null;
|
|
1218
|
+
if (this.reconnectTimeout) {
|
|
1219
|
+
clearTimeout(this.reconnectTimeout);
|
|
1220
|
+
this.reconnectTimeout = null;
|
|
1221
|
+
}
|
|
1222
|
+
if (this.ws) {
|
|
1223
|
+
this.ws.onclose = null;
|
|
1224
|
+
this.ws.onerror = null;
|
|
1225
|
+
this.ws.onopen = null;
|
|
1226
|
+
this.ws.onmessage = null;
|
|
1227
|
+
this.ws.close();
|
|
1228
|
+
this.ws = null;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
initWebSocket() {
|
|
1232
|
+
if (!this.WebSocketConstructor) return;
|
|
1233
|
+
if (this.ws?.readyState === this.WebSocketConstructor.OPEN) return;
|
|
1234
|
+
if (this.ws) {
|
|
1235
|
+
this.ws.onclose = null;
|
|
1236
|
+
this.ws.close();
|
|
1237
|
+
this.ws = null;
|
|
1238
|
+
}
|
|
1239
|
+
try {
|
|
1240
|
+
this.ws = new this.WebSocketConstructor(this.websocketUrl);
|
|
1241
|
+
this.ws.onopen = async () => {
|
|
1242
|
+
console.debug("Connected to PostgreSQL backend");
|
|
1243
|
+
const wasReconnect = this.reconnectAttempts > 0;
|
|
1244
|
+
this.isConnected = true;
|
|
1245
|
+
this.reconnectAttempts = 0;
|
|
1246
|
+
if (this.getAuthToken && !this.isAuthenticated) try {
|
|
1247
|
+
const token = await this.getAuthToken();
|
|
1248
|
+
if (token) {
|
|
1249
|
+
await this.authenticate(token);
|
|
1250
|
+
console.debug("WebSocket auto-authenticated");
|
|
1251
|
+
}
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
console.debug("WebSocket connected without auth:", error?.message || error);
|
|
1254
|
+
}
|
|
1255
|
+
this.emit(wasReconnect ? "reconnect" : "connect");
|
|
1256
|
+
this.processMessageQueue();
|
|
1257
|
+
if (wasReconnect) this.resubscribeAll();
|
|
1258
|
+
};
|
|
1259
|
+
this.ws.onmessage = (event) => {
|
|
1260
|
+
try {
|
|
1261
|
+
const message = JSON.parse(event.data, rebaseReviver);
|
|
1262
|
+
this.handleWebSocketMessage(message);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
console.error("Error parsing WebSocket message:", error);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
this.ws.onclose = () => {
|
|
1268
|
+
console.debug("Disconnected from PostgreSQL backend");
|
|
1269
|
+
this.isConnected = false;
|
|
1270
|
+
this.isAuthenticated = false;
|
|
1271
|
+
this.authPromise = null;
|
|
1272
|
+
this.emit("disconnect");
|
|
1273
|
+
for (const [reqId, request] of this.pendingRequests.entries()) {
|
|
1274
|
+
if (reqId.startsWith("auth_")) request.reject(/* @__PURE__ */ new Error("Connection closed during authentication"));
|
|
1275
|
+
else if (request.message) {
|
|
1276
|
+
request.message._queuedResolve = request.resolve;
|
|
1277
|
+
request.message._queuedReject = request.reject;
|
|
1278
|
+
this.messageQueue.push(request.message);
|
|
1279
|
+
} else request.reject(new ApiError("Connection closed", "Connection closed"));
|
|
1280
|
+
this.pendingRequests.delete(reqId);
|
|
1281
|
+
}
|
|
1282
|
+
this.attemptReconnect();
|
|
1283
|
+
};
|
|
1284
|
+
this.ws.onerror = (error) => {
|
|
1285
|
+
console.error("WebSocket error:", error);
|
|
1286
|
+
this.isConnected = false;
|
|
1287
|
+
this.emit("error", error);
|
|
1288
|
+
};
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
console.error("Failed to initialize WebSocket:", error);
|
|
1291
|
+
this.attemptReconnect();
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
processMessageQueue() {
|
|
1295
|
+
while (this.messageQueue.length > 0 && this.isConnected) {
|
|
1296
|
+
const message = this.messageQueue.shift();
|
|
1297
|
+
if (message) this.sendMessage(message);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
attemptReconnect() {
|
|
1301
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
1302
|
+
console.error("Max reconnection attempts reached");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
this.reconnectAttempts++;
|
|
1306
|
+
const delay = Math.min(1e3 * Math.pow(2, this.reconnectAttempts), 3e4);
|
|
1307
|
+
console.debug(`Attempting to reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
1308
|
+
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
|
1309
|
+
this.reconnectTimeout = setTimeout(() => {
|
|
1310
|
+
this.reconnectTimeout = null;
|
|
1311
|
+
this.initWebSocket();
|
|
1312
|
+
}, delay);
|
|
1313
|
+
}
|
|
1314
|
+
isAuthError(message) {
|
|
1315
|
+
if (message.type === "AUTH_ERROR") return true;
|
|
1316
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1317
|
+
if (errorCode === "UNAUTHORIZED" || errorCode === "JWT_EXPIRED" || errorCode === "AUTH_ERROR") return true;
|
|
1318
|
+
const lowerMessage = errorMessage.toLowerCase();
|
|
1319
|
+
return lowerMessage.includes("unauthorized") || lowerMessage.includes("token expired") || lowerMessage.includes("token is expired") || lowerMessage.includes("invalid token") || lowerMessage.includes("session expired") || lowerMessage.includes("auth error");
|
|
1320
|
+
}
|
|
1321
|
+
async handleAuthFailure() {
|
|
1322
|
+
if (this.refreshInProgress) return this.refreshInProgress;
|
|
1323
|
+
this.refreshInProgress = (async () => {
|
|
1324
|
+
this.isAuthenticated = false;
|
|
1325
|
+
this.authPromise = null;
|
|
1326
|
+
if (this.onUnauthorized) try {
|
|
1327
|
+
if (await this.onUnauthorized() && this.getAuthToken) {
|
|
1328
|
+
const token = await this.getAuthToken();
|
|
1329
|
+
if (token) {
|
|
1330
|
+
await this.authenticate(token);
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.error("WebSocket auth refresh failed:", error);
|
|
1336
|
+
}
|
|
1337
|
+
return false;
|
|
1338
|
+
})();
|
|
1339
|
+
try {
|
|
1340
|
+
return await this.refreshInProgress;
|
|
1341
|
+
} finally {
|
|
1342
|
+
this.refreshInProgress = null;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
/**
|
|
1346
|
+
* Shared logic for re-subscribing a collection or entity subscription
|
|
1347
|
+
* after an auth error is resolved by refreshing credentials.
|
|
1348
|
+
*/
|
|
1349
|
+
resubscribeAfterAuthRefresh(message, subscription, subscriptionKey, idPrefix, backendKeyMap, messageType) {
|
|
1350
|
+
this.handleAuthFailure().then((refreshed) => {
|
|
1351
|
+
if (refreshed) {
|
|
1352
|
+
const oldBackendId = subscription.backendSubscriptionId;
|
|
1353
|
+
const newBackendId = `${idPrefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1354
|
+
subscription.backendSubscriptionId = newBackendId;
|
|
1355
|
+
backendKeyMap.delete(oldBackendId);
|
|
1356
|
+
backendKeyMap.set(newBackendId, subscriptionKey);
|
|
1357
|
+
this.sendMessage({
|
|
1358
|
+
type: messageType,
|
|
1359
|
+
payload: {
|
|
1360
|
+
...subscription.props,
|
|
1361
|
+
subscriptionId: newBackendId
|
|
1362
|
+
}
|
|
1363
|
+
}).catch((error) => {
|
|
1364
|
+
console.error(`[WS] Failed to re-subscribe ${idPrefix} after auth refresh:`, subscriptionKey, error);
|
|
1365
|
+
subscription.callbacks.forEach((callback) => {
|
|
1366
|
+
if (callback.onError) callback.onError(error);
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
} else {
|
|
1370
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1371
|
+
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1372
|
+
subscription.callbacks.forEach((callback) => {
|
|
1373
|
+
if (callback.onError) callback.onError(error);
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
}).catch((err) => {
|
|
1377
|
+
subscription.callbacks.forEach((callback) => {
|
|
1378
|
+
if (callback.onError) callback.onError(err);
|
|
1379
|
+
});
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
handleWebSocketMessage(message) {
|
|
1383
|
+
const { type, requestId, subscriptionId } = message;
|
|
1384
|
+
if (requestId && this.pendingRequests.has(requestId)) {
|
|
1385
|
+
const pendingReq = this.pendingRequests.get(requestId);
|
|
1386
|
+
if (type === "ERROR" || type === "AUTH_ERROR" || message.error) if (this.isAuthError(message)) {
|
|
1387
|
+
this.pendingRequests.delete(requestId);
|
|
1388
|
+
this.handleAuthFailure().then((refreshed) => {
|
|
1389
|
+
if (refreshed && pendingReq.message) this.doSendMessage(pendingReq.message, pendingReq.resolve, pendingReq.reject).catch(pendingReq.reject);
|
|
1390
|
+
else {
|
|
1391
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1392
|
+
pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1393
|
+
}
|
|
1394
|
+
}).catch((err) => {
|
|
1395
|
+
pendingReq.reject(err);
|
|
1396
|
+
});
|
|
1397
|
+
} else {
|
|
1398
|
+
this.pendingRequests.delete(requestId);
|
|
1399
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1400
|
+
pendingReq.reject(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1401
|
+
}
|
|
1402
|
+
else {
|
|
1403
|
+
this.pendingRequests.delete(requestId);
|
|
1404
|
+
pendingReq.resolve(message.payload || message);
|
|
1405
|
+
}
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
if (subscriptionId && type === "collection_update") {
|
|
1409
|
+
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1410
|
+
if (subscriptionKey) {
|
|
1411
|
+
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1412
|
+
if (collectionSub) {
|
|
1413
|
+
const incomingEntities = message.entities || [];
|
|
1414
|
+
const entities = this.mergeEntities(collectionSub.latestData, incomingEntities);
|
|
1415
|
+
collectionSub.latestData = entities;
|
|
1416
|
+
collectionSub.lastUpdated = Date.now();
|
|
1417
|
+
collectionSub.isInitialDataReceived = true;
|
|
1418
|
+
collectionSub.callbacks.forEach((callback) => {
|
|
1419
|
+
try {
|
|
1420
|
+
callback.onUpdate(entities);
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
console.error("Error in collection subscription callback:", error);
|
|
1423
|
+
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
if (subscriptionId && type === "collection_entity_patch") {
|
|
1431
|
+
const subscriptionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1432
|
+
if (subscriptionKey) {
|
|
1433
|
+
const collectionSub = this.collectionSubscriptions.get(subscriptionKey);
|
|
1434
|
+
if (collectionSub && collectionSub.isInitialDataReceived && collectionSub.latestData) {
|
|
1435
|
+
const patchEntity = message.entity ?? null;
|
|
1436
|
+
const patchEntityId = message.entityId;
|
|
1437
|
+
let updated;
|
|
1438
|
+
if (patchEntity === null || patchEntity === void 0) updated = collectionSub.latestData.filter((e) => String(e.id) !== String(patchEntityId));
|
|
1439
|
+
else {
|
|
1440
|
+
const idx = collectionSub.latestData.findIndex((e) => String(e.id) === String(patchEntity.id));
|
|
1441
|
+
if (idx >= 0) {
|
|
1442
|
+
updated = [...collectionSub.latestData];
|
|
1443
|
+
updated[idx] = patchEntity;
|
|
1444
|
+
} else updated = [patchEntity, ...collectionSub.latestData];
|
|
1445
|
+
}
|
|
1446
|
+
collectionSub.latestData = updated;
|
|
1447
|
+
collectionSub.lastUpdated = Date.now();
|
|
1448
|
+
collectionSub.callbacks.forEach((callback) => {
|
|
1449
|
+
try {
|
|
1450
|
+
callback.onUpdate(updated);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
console.error("Error in collection patch callback:", error);
|
|
1453
|
+
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (subscriptionId && type === "entity_update") {
|
|
1461
|
+
const subscriptionKey = this.backendToEntityKey.get(subscriptionId);
|
|
1462
|
+
if (subscriptionKey) {
|
|
1463
|
+
const entitySub = this.entitySubscriptions.get(subscriptionKey);
|
|
1464
|
+
if (entitySub) {
|
|
1465
|
+
const entity = message.entity ?? null;
|
|
1466
|
+
entitySub.latestData = entity;
|
|
1467
|
+
entitySub.lastUpdated = Date.now();
|
|
1468
|
+
entitySub.isInitialDataReceived = true;
|
|
1469
|
+
entitySub.callbacks.forEach((callback) => {
|
|
1470
|
+
try {
|
|
1471
|
+
callback.onUpdate(entity);
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
console.error("Error in entity subscription callback:", error);
|
|
1474
|
+
if (callback.onError) callback.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (subscriptionId && (type === "ERROR" || message.error)) {
|
|
1482
|
+
const collectionKey = this.backendToCollectionKey.get(subscriptionId);
|
|
1483
|
+
if (collectionKey) {
|
|
1484
|
+
const collectionSub = this.collectionSubscriptions.get(collectionKey);
|
|
1485
|
+
if (collectionSub) {
|
|
1486
|
+
if (this.isAuthError(message)) {
|
|
1487
|
+
this.resubscribeAfterAuthRefresh(message, collectionSub, collectionKey, "collection", this.backendToCollectionKey, "subscribe_collection");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1491
|
+
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1492
|
+
collectionSub.callbacks.forEach((callback) => {
|
|
1493
|
+
if (callback.onError) callback.onError(error);
|
|
1494
|
+
});
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
const entityKey = this.backendToEntityKey.get(subscriptionId);
|
|
1499
|
+
if (entityKey) {
|
|
1500
|
+
const entitySub = this.entitySubscriptions.get(entityKey);
|
|
1501
|
+
if (entitySub) {
|
|
1502
|
+
if (this.isAuthError(message)) {
|
|
1503
|
+
this.resubscribeAfterAuthRefresh(message, entitySub, entityKey, "entity", this.backendToEntityKey, "subscribe_entity");
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1507
|
+
const error = new ApiError(errorMessage, errorMessage, errorCode);
|
|
1508
|
+
entitySub.callbacks.forEach((callback) => {
|
|
1509
|
+
if (callback.onError) callback.onError(error);
|
|
1510
|
+
});
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
if (subscriptionId && this.subscriptions.has(subscriptionId)) {
|
|
1516
|
+
const callback = this.subscriptions.get(subscriptionId);
|
|
1517
|
+
if (!callback) throw new Error(`Subscription callback not found for subscriptionId: ${subscriptionId}`);
|
|
1518
|
+
if (message.type === "ERROR" || message.error) {
|
|
1519
|
+
if (callback.onError) {
|
|
1520
|
+
const { errorMessage, errorCode } = extractMessageError(message);
|
|
1521
|
+
callback.onError(new ApiError(errorMessage, errorMessage, errorCode));
|
|
1522
|
+
}
|
|
1523
|
+
} else callback.onUpdate(message);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
async ensureAuthenticated(retryCount = 3) {
|
|
1527
|
+
if (this.isAuthenticated || !this.getAuthToken) return;
|
|
1528
|
+
if (this.authPromise) {
|
|
1529
|
+
await this.authPromise;
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
let lastError = null;
|
|
1533
|
+
for (let attempt = 0; attempt < retryCount; attempt++) try {
|
|
1534
|
+
const token = await this.getAuthToken();
|
|
1535
|
+
if (!token) throw new Error("user not logged in");
|
|
1536
|
+
this.authPromise = this.authenticate(token);
|
|
1537
|
+
await this.authPromise;
|
|
1538
|
+
this.authPromise = null;
|
|
1539
|
+
console.debug("WebSocket authenticated on demand");
|
|
1540
|
+
return;
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
this.authPromise = null;
|
|
1543
|
+
lastError = error;
|
|
1544
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
1545
|
+
if (errMsg.includes("not logged in") || errMsg.includes("Session expired")) {
|
|
1546
|
+
console.warn("WebSocket auth failed: user not logged in");
|
|
1547
|
+
throw error;
|
|
1548
|
+
}
|
|
1549
|
+
if (errMsg.includes("still loading")) {
|
|
1550
|
+
if (attempt < retryCount - 1) {
|
|
1551
|
+
const delay = Math.min(500 * (attempt + 1), 2e3);
|
|
1552
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (attempt < retryCount - 1) {
|
|
1557
|
+
const delay = Math.min(1e3 * (attempt + 1), 3e3);
|
|
1558
|
+
console.debug(`WebSocket auth attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
|
|
1559
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
console.warn("WebSocket on-demand auth failed after retries:", lastError);
|
|
1563
|
+
throw lastError;
|
|
1564
|
+
}
|
|
1565
|
+
async reauthenticate() {
|
|
1566
|
+
if (!this.getAuthToken) return;
|
|
1567
|
+
this.isAuthenticated = false;
|
|
1568
|
+
try {
|
|
1569
|
+
const token = await this.getAuthToken();
|
|
1570
|
+
if (!token) throw new Error("user not logged in");
|
|
1571
|
+
await this.authenticate(token);
|
|
1572
|
+
console.debug("WebSocket reauthenticated successfully");
|
|
1573
|
+
} catch (error) {
|
|
1574
|
+
console.error("WebSocket reauthentication failed:", error);
|
|
1575
|
+
throw error;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
sendMessage(message) {
|
|
1579
|
+
const queuedMsg = message;
|
|
1580
|
+
if (queuedMsg._queuedResolve && queuedMsg._queuedReject) return this.doSendMessage(message, queuedMsg._queuedResolve, queuedMsg._queuedReject);
|
|
1581
|
+
if (!this.isConnected || !this.ws) return new Promise((resolve, reject) => {
|
|
1582
|
+
const queueable = message;
|
|
1583
|
+
queueable._queuedResolve = resolve;
|
|
1584
|
+
queueable._queuedReject = reject;
|
|
1585
|
+
this.messageQueue.push(message);
|
|
1586
|
+
});
|
|
1587
|
+
return new Promise((resolve, reject) => {
|
|
1588
|
+
this.doSendMessage(message, resolve, reject);
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
async doSendMessage(message, resolve, reject) {
|
|
1592
|
+
if (message.type !== "AUTHENTICATE" && this.getAuthToken && !this.isAuthenticated) try {
|
|
1593
|
+
await this.ensureAuthenticated();
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
const errorMessage = error instanceof Error ? error.message : "Authentication required";
|
|
1596
|
+
reject(new ApiError(errorMessage, errorMessage));
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const requestId = message.requestId || `req_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1600
|
+
message.requestId = requestId;
|
|
1601
|
+
const expectsResponse = ![
|
|
1602
|
+
"subscribe_collection",
|
|
1603
|
+
"subscribe_entity",
|
|
1604
|
+
"unsubscribe",
|
|
1605
|
+
"join_channel",
|
|
1606
|
+
"leave_channel",
|
|
1607
|
+
"broadcast",
|
|
1608
|
+
"presence_track",
|
|
1609
|
+
"presence_untrack",
|
|
1610
|
+
"presence_state"
|
|
1611
|
+
].includes(message.type);
|
|
1612
|
+
if (expectsResponse && !this.pendingRequests.has(requestId)) {
|
|
1613
|
+
const timeoutHandle = setTimeout(() => {
|
|
1614
|
+
if (this.pendingRequests.has(requestId)) {
|
|
1615
|
+
this.pendingRequests.delete(requestId);
|
|
1616
|
+
reject(new ApiError("Request timed out", "Request timed out"));
|
|
1617
|
+
}
|
|
1618
|
+
}, this.requestTimeoutMs);
|
|
1619
|
+
this.pendingRequests.set(requestId, {
|
|
1620
|
+
resolve: (value) => {
|
|
1621
|
+
clearTimeout(timeoutHandle);
|
|
1622
|
+
resolve(value);
|
|
1623
|
+
},
|
|
1624
|
+
reject: (error) => {
|
|
1625
|
+
clearTimeout(timeoutHandle);
|
|
1626
|
+
reject(error);
|
|
1627
|
+
},
|
|
1628
|
+
message
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
try {
|
|
1632
|
+
this.ws.send(JSON.stringify(message));
|
|
1633
|
+
if (!expectsResponse) resolve(void 0);
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
if (expectsResponse) this.pendingRequests.delete(requestId);
|
|
1636
|
+
reject(new ApiError("Failed to send message", error instanceof Error ? error.message : "Unknown error"));
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
async fetchCollection(props) {
|
|
1640
|
+
return (await this.sendMessage({
|
|
1641
|
+
type: "FETCH_COLLECTION",
|
|
1642
|
+
payload: props
|
|
1643
|
+
})).entities || [];
|
|
1644
|
+
}
|
|
1645
|
+
async fetchEntity(props) {
|
|
1646
|
+
return (await this.sendMessage({
|
|
1647
|
+
type: "FETCH_ENTITY",
|
|
1648
|
+
payload: props
|
|
1649
|
+
})).entity ?? void 0;
|
|
1650
|
+
}
|
|
1651
|
+
async saveEntity(props) {
|
|
1652
|
+
return (await this.sendMessage({
|
|
1653
|
+
type: "SAVE_ENTITY",
|
|
1654
|
+
payload: props
|
|
1655
|
+
})).entity;
|
|
1656
|
+
}
|
|
1657
|
+
async deleteEntity(props) {
|
|
1658
|
+
await this.sendMessage({
|
|
1659
|
+
type: "DELETE_ENTITY",
|
|
1660
|
+
payload: props
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
async executeSql(sql, options) {
|
|
1664
|
+
return (await this.sendMessage({
|
|
1665
|
+
type: "EXECUTE_SQL",
|
|
1666
|
+
payload: {
|
|
1667
|
+
sql,
|
|
1668
|
+
options
|
|
1669
|
+
}
|
|
1670
|
+
})).result || [];
|
|
1671
|
+
}
|
|
1672
|
+
async fetchAvailableDatabases() {
|
|
1673
|
+
return (await this.sendMessage({
|
|
1674
|
+
type: "FETCH_DATABASES",
|
|
1675
|
+
payload: {}
|
|
1676
|
+
})).databases || [];
|
|
1677
|
+
}
|
|
1678
|
+
async fetchAvailableRoles() {
|
|
1679
|
+
return (await this.sendMessage({ type: "FETCH_ROLES" })).roles || [];
|
|
1680
|
+
}
|
|
1681
|
+
async fetchCurrentDatabase() {
|
|
1682
|
+
return (await this.sendMessage({ type: "FETCH_CURRENT_DATABASE" })).database;
|
|
1683
|
+
}
|
|
1684
|
+
async checkUniqueField(path, name, value, entityId, collection) {
|
|
1685
|
+
return (await this.sendMessage({
|
|
1686
|
+
type: "CHECK_UNIQUE_FIELD",
|
|
1687
|
+
payload: {
|
|
1688
|
+
path,
|
|
1689
|
+
name,
|
|
1690
|
+
value,
|
|
1691
|
+
entityId,
|
|
1692
|
+
collection
|
|
1693
|
+
}
|
|
1694
|
+
})).isUnique;
|
|
1695
|
+
}
|
|
1696
|
+
async countEntities(props) {
|
|
1697
|
+
return (await this.sendMessage({
|
|
1698
|
+
type: "COUNT_ENTITIES",
|
|
1699
|
+
payload: props
|
|
1700
|
+
})).count;
|
|
1701
|
+
}
|
|
1702
|
+
async fetchUnmappedTables(mappedPaths) {
|
|
1703
|
+
return (await this.sendMessage({
|
|
1704
|
+
type: "FETCH_UNMAPPED_TABLES",
|
|
1705
|
+
payload: { mappedPaths }
|
|
1706
|
+
})).tables || [];
|
|
1707
|
+
}
|
|
1708
|
+
async fetchTableMetadata(tableName) {
|
|
1709
|
+
return (await this.sendMessage({
|
|
1710
|
+
type: "FETCH_TABLE_METADATA",
|
|
1711
|
+
payload: { tableName }
|
|
1712
|
+
})).metadata || {
|
|
1713
|
+
columns: [],
|
|
1714
|
+
foreignKeys: [],
|
|
1715
|
+
junctions: [],
|
|
1716
|
+
policies: []
|
|
1717
|
+
};
|
|
1718
|
+
}
|
|
1719
|
+
async createBranch(name, options) {
|
|
1720
|
+
return (await this.sendMessage({
|
|
1721
|
+
type: "CREATE_BRANCH",
|
|
1722
|
+
payload: {
|
|
1723
|
+
name,
|
|
1724
|
+
options
|
|
1725
|
+
}
|
|
1726
|
+
})).branch;
|
|
1727
|
+
}
|
|
1728
|
+
async deleteBranch(name) {
|
|
1729
|
+
await this.sendMessage({
|
|
1730
|
+
type: "DELETE_BRANCH",
|
|
1731
|
+
payload: { name }
|
|
1732
|
+
});
|
|
1733
|
+
}
|
|
1734
|
+
async listBranches() {
|
|
1735
|
+
return (await this.sendMessage({
|
|
1736
|
+
type: "LIST_BRANCHES",
|
|
1737
|
+
payload: {}
|
|
1738
|
+
})).branches || [];
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Recursively compare two values for structural equality.
|
|
1742
|
+
* Handles primitives, null, undefined, Date, RegExp, arrays, and plain objects.
|
|
1743
|
+
*/
|
|
1744
|
+
deepEqual(a, b) {
|
|
1745
|
+
if (a === b) return true;
|
|
1746
|
+
if (a === null || b === null || a === void 0 || b === void 0) return false;
|
|
1747
|
+
if (typeof a !== typeof b) return false;
|
|
1748
|
+
if (typeof a !== "object") return false;
|
|
1749
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
1750
|
+
if (a instanceof Date || b instanceof Date) return false;
|
|
1751
|
+
if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags;
|
|
1752
|
+
if (a instanceof RegExp || b instanceof RegExp) return false;
|
|
1753
|
+
const aIsArray = Array.isArray(a);
|
|
1754
|
+
const bIsArray = Array.isArray(b);
|
|
1755
|
+
if (aIsArray !== bIsArray) return false;
|
|
1756
|
+
if (aIsArray && bIsArray) {
|
|
1757
|
+
if (a.length !== b.length) return false;
|
|
1758
|
+
for (let i = 0; i < a.length; i++) if (!this.deepEqual(a[i], b[i])) return false;
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
const aObj = a;
|
|
1762
|
+
const bObj = b;
|
|
1763
|
+
const aKeys = Object.keys(aObj);
|
|
1764
|
+
const bKeys = Object.keys(bObj);
|
|
1765
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1766
|
+
for (const key of aKeys) {
|
|
1767
|
+
if (!Object.prototype.hasOwnProperty.call(bObj, key)) return false;
|
|
1768
|
+
if (!this.deepEqual(aObj[key], bObj[key])) return false;
|
|
1769
|
+
}
|
|
1770
|
+
return true;
|
|
1771
|
+
}
|
|
1772
|
+
normalizeForComparison(val) {
|
|
1773
|
+
if (!val) return val;
|
|
1774
|
+
if (Array.isArray(val)) return val.map((item) => this.normalizeForComparison(item));
|
|
1775
|
+
if (typeof val === "object") {
|
|
1776
|
+
if (val instanceof Date) return val;
|
|
1777
|
+
if (val instanceof RegExp) return val;
|
|
1778
|
+
const obj = val;
|
|
1779
|
+
if (obj.__type === "relation") {
|
|
1780
|
+
const { data, ...rest } = obj;
|
|
1781
|
+
return rest;
|
|
1782
|
+
}
|
|
1783
|
+
const result = {};
|
|
1784
|
+
for (const [k, v] of Object.entries(obj)) result[k] = this.normalizeForComparison(v);
|
|
1785
|
+
return result;
|
|
1786
|
+
}
|
|
1787
|
+
return val;
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Merge incoming entities with cached data, preserving cached references
|
|
1791
|
+
* for entities whose values haven't changed. This avoids unnecessary
|
|
1792
|
+
* React re-renders when the server refetches all entities but most
|
|
1793
|
+
* haven't actually changed.
|
|
1794
|
+
*/
|
|
1795
|
+
mergeEntities(cached, incoming) {
|
|
1796
|
+
if (!cached || cached.length === 0) return incoming;
|
|
1797
|
+
const cachedById = /* @__PURE__ */ new Map();
|
|
1798
|
+
for (const entity of cached) cachedById.set(entity.id, entity);
|
|
1799
|
+
return incoming.map((incomingEntity) => {
|
|
1800
|
+
const cachedEntity = cachedById.get(incomingEntity.id);
|
|
1801
|
+
if (!cachedEntity) return incomingEntity;
|
|
1802
|
+
if (cachedEntity.path === incomingEntity.path) {
|
|
1803
|
+
const normCached = this.normalizeForComparison(cachedEntity.values);
|
|
1804
|
+
const normIncoming = this.normalizeForComparison(incomingEntity.values);
|
|
1805
|
+
if (this.deepEqual(normCached, normIncoming)) return cachedEntity;
|
|
1806
|
+
else {
|
|
1807
|
+
const mismatches = {};
|
|
1808
|
+
const allKeys = new Set([...Object.keys(normCached), ...Object.keys(normIncoming)]);
|
|
1809
|
+
for (const key of allKeys) if (!this.deepEqual(normCached[key], normIncoming[key])) mismatches[key] = {
|
|
1810
|
+
cached: normCached[key],
|
|
1811
|
+
incoming: normIncoming[key]
|
|
1812
|
+
};
|
|
1813
|
+
console.debug(`[RebaseWS] Row ${incomingEntity.id} refetch mismatch:\n`, JSON.stringify(mismatches, null, 2));
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
return incomingEntity;
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
listenCollection(props, onUpdate, onError) {
|
|
1820
|
+
const subscriptionKey = this.createCollectionSubscriptionKey(props);
|
|
1821
|
+
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1822
|
+
const existingSubscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
1823
|
+
if (existingSubscription) {
|
|
1824
|
+
const callbackMap = existingSubscription.callbacks;
|
|
1825
|
+
callbackMap.set(callbackId, {
|
|
1826
|
+
onUpdate,
|
|
1827
|
+
onError
|
|
1828
|
+
});
|
|
1829
|
+
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
|
|
1830
|
+
onUpdate(existingSubscription.latestData);
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
console.error("Error in collection subscription callback:", error);
|
|
1833
|
+
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
1834
|
+
}
|
|
1835
|
+
return () => {
|
|
1836
|
+
callbackMap.delete(callbackId);
|
|
1837
|
+
if (callbackMap.size === 0) {
|
|
1838
|
+
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1839
|
+
this.backendToCollectionKey.delete(existingSubscription.backendSubscriptionId);
|
|
1840
|
+
if (this.isConnected && this.ws) this.sendMessage({
|
|
1841
|
+
type: "unsubscribe",
|
|
1842
|
+
payload: { subscriptionId: existingSubscription.backendSubscriptionId }
|
|
1843
|
+
}).catch(console.error);
|
|
1844
|
+
}
|
|
1845
|
+
};
|
|
1846
|
+
}
|
|
1847
|
+
const backendSubscriptionId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1848
|
+
const callbackMap = /* @__PURE__ */ new Map();
|
|
1849
|
+
callbackMap.set(callbackId, {
|
|
1850
|
+
onUpdate,
|
|
1851
|
+
onError
|
|
1852
|
+
});
|
|
1853
|
+
this.collectionSubscriptions.set(subscriptionKey, {
|
|
1854
|
+
backendSubscriptionId,
|
|
1855
|
+
callbacks: callbackMap,
|
|
1856
|
+
props
|
|
1857
|
+
});
|
|
1858
|
+
this.backendToCollectionKey.set(backendSubscriptionId, subscriptionKey);
|
|
1859
|
+
this.sendMessage({
|
|
1860
|
+
type: "subscribe_collection",
|
|
1861
|
+
payload: {
|
|
1862
|
+
...props,
|
|
1863
|
+
subscriptionId: backendSubscriptionId
|
|
1864
|
+
}
|
|
1865
|
+
}).catch((error) => {
|
|
1866
|
+
if (onError) onError(error);
|
|
1867
|
+
});
|
|
1868
|
+
return () => {
|
|
1869
|
+
const subscription = this.collectionSubscriptions.get(subscriptionKey);
|
|
1870
|
+
if (subscription) {
|
|
1871
|
+
const callbacks = subscription.callbacks;
|
|
1872
|
+
callbacks.delete(callbackId);
|
|
1873
|
+
if (callbacks.size === 0) {
|
|
1874
|
+
this.collectionSubscriptions.delete(subscriptionKey);
|
|
1875
|
+
this.backendToCollectionKey.delete(subscription.backendSubscriptionId);
|
|
1876
|
+
if (this.isConnected && this.ws) this.sendMessage({
|
|
1877
|
+
type: "unsubscribe",
|
|
1878
|
+
payload: { subscriptionId: subscription.backendSubscriptionId }
|
|
1879
|
+
}).catch(console.error);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
}
|
|
1884
|
+
listenEntity(props, onUpdate, onError) {
|
|
1885
|
+
const subscriptionKey = this.createEntitySubscriptionKey(props);
|
|
1886
|
+
const callbackId = `callback_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1887
|
+
const existingSubscription = this.entitySubscriptions.get(subscriptionKey);
|
|
1888
|
+
if (existingSubscription) {
|
|
1889
|
+
const callbackMap = existingSubscription.callbacks;
|
|
1890
|
+
callbackMap.set(callbackId, {
|
|
1891
|
+
onUpdate,
|
|
1892
|
+
onError
|
|
1893
|
+
});
|
|
1894
|
+
if (existingSubscription.latestData !== void 0 && existingSubscription.isInitialDataReceived) try {
|
|
1895
|
+
onUpdate(existingSubscription.latestData);
|
|
1896
|
+
} catch (error) {
|
|
1897
|
+
console.error("Error in entity subscription callback:", error);
|
|
1898
|
+
if (onError) onError(error instanceof Error ? error : new Error(String(error)));
|
|
1899
|
+
}
|
|
1900
|
+
return () => {
|
|
1901
|
+
callbackMap.delete(callbackId);
|
|
1902
|
+
if (callbackMap.size === 0) {
|
|
1903
|
+
this.entitySubscriptions.delete(subscriptionKey);
|
|
1904
|
+
this.backendToEntityKey.delete(existingSubscription.backendSubscriptionId);
|
|
1905
|
+
if (this.isConnected && this.ws) this.sendMessage({
|
|
1906
|
+
type: "unsubscribe",
|
|
1907
|
+
payload: { subscriptionId: existingSubscription.backendSubscriptionId }
|
|
1908
|
+
}).catch(console.error);
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
const backendSubscriptionId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1913
|
+
const callbackMap = /* @__PURE__ */ new Map();
|
|
1914
|
+
callbackMap.set(callbackId, {
|
|
1915
|
+
onUpdate,
|
|
1916
|
+
onError
|
|
1917
|
+
});
|
|
1918
|
+
this.entitySubscriptions.set(subscriptionKey, {
|
|
1919
|
+
backendSubscriptionId,
|
|
1920
|
+
callbacks: callbackMap,
|
|
1921
|
+
props
|
|
1922
|
+
});
|
|
1923
|
+
this.backendToEntityKey.set(backendSubscriptionId, subscriptionKey);
|
|
1924
|
+
this.sendMessage({
|
|
1925
|
+
type: "subscribe_entity",
|
|
1926
|
+
payload: {
|
|
1927
|
+
...props,
|
|
1928
|
+
subscriptionId: backendSubscriptionId
|
|
1929
|
+
}
|
|
1930
|
+
}).catch((error) => {
|
|
1931
|
+
if (onError) onError(error);
|
|
1932
|
+
});
|
|
1933
|
+
return () => {
|
|
1934
|
+
const subscription = this.entitySubscriptions.get(subscriptionKey);
|
|
1935
|
+
if (subscription) {
|
|
1936
|
+
const callbacks = subscription.callbacks;
|
|
1937
|
+
callbacks.delete(callbackId);
|
|
1938
|
+
if (callbacks.size === 0) {
|
|
1939
|
+
this.entitySubscriptions.delete(subscriptionKey);
|
|
1940
|
+
this.backendToEntityKey.delete(subscription.backendSubscriptionId);
|
|
1941
|
+
if (this.isConnected && this.ws) this.sendMessage({
|
|
1942
|
+
type: "unsubscribe",
|
|
1943
|
+
payload: { subscriptionId: subscription.backendSubscriptionId }
|
|
1944
|
+
}).catch(console.error);
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1950
|
+
* Re-send all active subscriptions to the backend after a reconnect.
|
|
1951
|
+
* The server wipes subscription state when a client disconnects, so
|
|
1952
|
+
* we need to re-register everything to resume receiving updates.
|
|
1953
|
+
*/
|
|
1954
|
+
resubscribeAll() {
|
|
1955
|
+
console.debug(`[WS] Re-subscribing: ${this.collectionSubscriptions.size} collection(s), ${this.entitySubscriptions.size} entity(ies)`);
|
|
1956
|
+
for (const [key, sub] of this.collectionSubscriptions.entries()) {
|
|
1957
|
+
const oldBackendId = sub.backendSubscriptionId;
|
|
1958
|
+
const newBackendId = `collection_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1959
|
+
sub.backendSubscriptionId = newBackendId;
|
|
1960
|
+
this.backendToCollectionKey.delete(oldBackendId);
|
|
1961
|
+
this.backendToCollectionKey.set(newBackendId, key);
|
|
1962
|
+
this.sendMessage({
|
|
1963
|
+
type: "subscribe_collection",
|
|
1964
|
+
payload: {
|
|
1965
|
+
...sub.props,
|
|
1966
|
+
subscriptionId: newBackendId
|
|
1967
|
+
}
|
|
1968
|
+
}).catch((error) => {
|
|
1969
|
+
console.error("[WS] Failed to re-subscribe collection:", key, error);
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
for (const [key, sub] of this.entitySubscriptions.entries()) {
|
|
1973
|
+
const oldBackendId = sub.backendSubscriptionId;
|
|
1974
|
+
const newBackendId = `entity_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
1975
|
+
sub.backendSubscriptionId = newBackendId;
|
|
1976
|
+
this.backendToEntityKey.delete(oldBackendId);
|
|
1977
|
+
this.backendToEntityKey.set(newBackendId, key);
|
|
1978
|
+
this.sendMessage({
|
|
1979
|
+
type: "subscribe_entity",
|
|
1980
|
+
payload: {
|
|
1981
|
+
...sub.props,
|
|
1982
|
+
subscriptionId: newBackendId
|
|
1983
|
+
}
|
|
1984
|
+
}).catch((error) => {
|
|
1985
|
+
console.error("[WS] Failed to re-subscribe entity:", key, error);
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
createCollectionSubscriptionKey(props) {
|
|
1990
|
+
const key = {
|
|
1991
|
+
path: props.path,
|
|
1992
|
+
filter: props.filter,
|
|
1993
|
+
limit: props.limit,
|
|
1994
|
+
startAfter: props.startAfter,
|
|
1995
|
+
orderBy: props.orderBy,
|
|
1996
|
+
order: props.order,
|
|
1997
|
+
searchString: props.searchString,
|
|
1998
|
+
collection: props.collection?.name
|
|
1999
|
+
};
|
|
2000
|
+
return JSON.stringify(key, (_, value) => {
|
|
2001
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return Object.keys(value).sort().reduce((sorted, k) => {
|
|
2002
|
+
sorted[k] = value[k];
|
|
2003
|
+
return sorted;
|
|
2004
|
+
}, {});
|
|
2005
|
+
return value;
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
createEntitySubscriptionKey(props) {
|
|
2009
|
+
return `${props.path}|${props.entityId}`;
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
//#endregion
|
|
2013
|
+
//#region src/index.ts
|
|
2014
|
+
/**
|
|
2015
|
+
* Derive a WebSocket URL from an HTTP base URL.
|
|
2016
|
+
* `http://` → `ws://`, `https://` → `wss://`.
|
|
2017
|
+
*/
|
|
2272
2018
|
function deriveWebSocketUrl(baseUrl) {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
}
|
|
2289
|
-
if (!baseUrl) return "";
|
|
2290
|
-
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) {
|
|
2291
|
-
return "";
|
|
2292
|
-
}
|
|
2293
|
-
return baseUrl.replace(/^https?:\/\//i, (match) => match.toLowerCase() === "https://" ? "wss://" : "ws://").replace(/\/$/, "");
|
|
2019
|
+
if (typeof window !== "undefined") {
|
|
2020
|
+
let absoluteUrl = "";
|
|
2021
|
+
if (!baseUrl) absoluteUrl = window.location.origin;
|
|
2022
|
+
else if (/^https?:\/\//i.test(baseUrl) || /^wss?:\/\//i.test(baseUrl)) absoluteUrl = baseUrl;
|
|
2023
|
+
else try {
|
|
2024
|
+
absoluteUrl = new URL(baseUrl, window.location.href).origin;
|
|
2025
|
+
} catch {
|
|
2026
|
+
absoluteUrl = window.location.origin;
|
|
2027
|
+
}
|
|
2028
|
+
const protocol = absoluteUrl.startsWith("https:") || absoluteUrl.startsWith("wss:") ? "wss:" : "ws:";
|
|
2029
|
+
return absoluteUrl.replace(/^https?:\/\//i, `${protocol}//`).replace(/^wss?:\/\//i, `${protocol}//`).replace(/\/$/, "");
|
|
2030
|
+
}
|
|
2031
|
+
if (!baseUrl) return "";
|
|
2032
|
+
if (!/^https?:\/\//i.test(baseUrl) && !/^wss?:\/\//i.test(baseUrl)) return "";
|
|
2033
|
+
return baseUrl.replace(/^https?:\/\//i, (match) => match.toLowerCase() === "https://" ? "wss://" : "ws://").replace(/\/$/, "");
|
|
2294
2034
|
}
|
|
2295
2035
|
function createRebaseClient(options) {
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
functions,
|
|
2374
|
-
storage,
|
|
2375
|
-
ws,
|
|
2376
|
-
setToken: transport.setToken,
|
|
2377
|
-
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
2378
|
-
setOnUnauthorized: transport.setOnUnauthorized,
|
|
2379
|
-
resolveToken: transport.resolveToken,
|
|
2380
|
-
baseUrl: transport.baseUrl,
|
|
2381
|
-
collection,
|
|
2382
|
-
call: async (endpoint, payload) => {
|
|
2383
|
-
const prefix = endpoint.startsWith("/") ? "" : "/";
|
|
2384
|
-
const res = await transport.request(`${prefix}${endpoint}`, {
|
|
2385
|
-
method: "POST",
|
|
2386
|
-
body: payload ? JSON.stringify(payload) : void 0
|
|
2387
|
-
});
|
|
2388
|
-
return res.data ?? res;
|
|
2389
|
-
},
|
|
2390
|
-
data: dataProxy,
|
|
2391
|
-
email: void 0
|
|
2392
|
-
};
|
|
2393
|
-
return target;
|
|
2036
|
+
const transport = createTransport(options);
|
|
2037
|
+
const auth = createAuth(transport, options.auth);
|
|
2038
|
+
const admin = createAdmin(transport, options.admin);
|
|
2039
|
+
const cron = createCron(transport, options.cron);
|
|
2040
|
+
const storage = createStorage(transport);
|
|
2041
|
+
const functions = createFunctionsClient(transport);
|
|
2042
|
+
const resolvedWsUrl = options.websocketUrl ?? deriveWebSocketUrl(options.baseUrl);
|
|
2043
|
+
let ws;
|
|
2044
|
+
if (resolvedWsUrl) {
|
|
2045
|
+
ws = new RebaseWebSocketClient({
|
|
2046
|
+
websocketUrl: resolvedWsUrl,
|
|
2047
|
+
getAuthToken: async () => {
|
|
2048
|
+
let session = auth.getSession();
|
|
2049
|
+
if (session && session.expiresAt <= Date.now() + 1e4) try {
|
|
2050
|
+
session = await auth.refreshSession();
|
|
2051
|
+
} catch (e) {}
|
|
2052
|
+
return session?.accessToken || options.token || "";
|
|
2053
|
+
},
|
|
2054
|
+
onUnauthorized: options.onUnauthorized || (async () => {
|
|
2055
|
+
try {
|
|
2056
|
+
await auth.refreshSession();
|
|
2057
|
+
return true;
|
|
2058
|
+
} catch (e) {
|
|
2059
|
+
return false;
|
|
2060
|
+
}
|
|
2061
|
+
})
|
|
2062
|
+
});
|
|
2063
|
+
auth.onAuthStateChange((event, session) => {
|
|
2064
|
+
if (!ws) return;
|
|
2065
|
+
if (event === "SIGNED_OUT") ws.disconnect();
|
|
2066
|
+
else if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
|
|
2067
|
+
if (session?.accessToken) ws.authenticate(session.accessToken).catch(console.warn);
|
|
2068
|
+
}
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
if (!options.onUnauthorized) transport.setOnUnauthorized(async () => {
|
|
2072
|
+
try {
|
|
2073
|
+
await auth.refreshSession();
|
|
2074
|
+
return true;
|
|
2075
|
+
} catch (e) {
|
|
2076
|
+
return false;
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
const collectionClients = /* @__PURE__ */ new Map();
|
|
2080
|
+
function collection(slug) {
|
|
2081
|
+
if (!collectionClients.has(slug)) collectionClients.set(slug, createCollectionClient(transport, slug, ws));
|
|
2082
|
+
return collectionClients.get(slug);
|
|
2083
|
+
}
|
|
2084
|
+
const dataProxy = new Proxy({ collection }, { get(_target, prop) {
|
|
2085
|
+
if (prop === "collection") return collection;
|
|
2086
|
+
if (typeof prop === "symbol") return void 0;
|
|
2087
|
+
if (typeof prop === "string" && prop !== "then" && prop !== "toJSON" && prop !== "$$typeof") return collection(toSnakeCase(prop));
|
|
2088
|
+
} });
|
|
2089
|
+
return {
|
|
2090
|
+
auth,
|
|
2091
|
+
admin,
|
|
2092
|
+
cron,
|
|
2093
|
+
functions,
|
|
2094
|
+
storage,
|
|
2095
|
+
ws,
|
|
2096
|
+
setToken: transport.setToken,
|
|
2097
|
+
setAuthTokenGetter: transport.setAuthTokenGetter,
|
|
2098
|
+
setOnUnauthorized: transport.setOnUnauthorized,
|
|
2099
|
+
resolveToken: transport.resolveToken,
|
|
2100
|
+
baseUrl: transport.baseUrl,
|
|
2101
|
+
collection,
|
|
2102
|
+
call: async (endpoint, payload) => {
|
|
2103
|
+
const prefix = endpoint.startsWith("/") ? "" : "/";
|
|
2104
|
+
const res = await transport.request(`${prefix}${endpoint}`, {
|
|
2105
|
+
method: "POST",
|
|
2106
|
+
body: payload ? JSON.stringify(payload) : void 0
|
|
2107
|
+
});
|
|
2108
|
+
return res.data ?? res;
|
|
2109
|
+
},
|
|
2110
|
+
data: dataProxy,
|
|
2111
|
+
email: void 0
|
|
2112
|
+
};
|
|
2394
2113
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
RebaseWebSocketClient,
|
|
2400
|
-
and,
|
|
2401
|
-
buildQueryString,
|
|
2402
|
-
cond,
|
|
2403
|
-
createAdmin,
|
|
2404
|
-
createAuth,
|
|
2405
|
-
createCollectionClient,
|
|
2406
|
-
createCookieStorage,
|
|
2407
|
-
createCron,
|
|
2408
|
-
createFunctionsClient,
|
|
2409
|
-
createMemoryStorage,
|
|
2410
|
-
createRebaseClient,
|
|
2411
|
-
createStorage,
|
|
2412
|
-
createTransport,
|
|
2413
|
-
or,
|
|
2414
|
-
rebaseReviver
|
|
2415
|
-
};
|
|
2416
|
-
//# sourceMappingURL=index.es.js.map
|
|
2114
|
+
//#endregion
|
|
2115
|
+
export { ApiError, QueryBuilder, RebaseApiError, RebaseWebSocketClient, and, buildQueryString, cond, createAdmin, createAuth, createCollectionClient, createCookieStorage, createCron, createFunctionsClient, createMemoryStorage, createRebaseClient, createStorage, createTransport, or, rebaseReviver };
|
|
2116
|
+
|
|
2117
|
+
//# sourceMappingURL=index.es.js.map
|