@edge-base/react-native 0.1.1
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/README.md +230 -0
- package/dist/index.cjs +3137 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1246 -0
- package/dist/index.d.ts +1246 -0
- package/dist/index.js +3118 -0
- package/dist/index.js.map +1 -0
- package/llms.txt +124 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3118 @@
|
|
|
1
|
+
import { EdgeBaseError, ApiPaths, ContextManager, HttpClient, DefaultDbApi, HttpClientAdapter, PublicHttpClientAdapter, StorageClient, FunctionsClient, DbRef } from '@edge-base/core';
|
|
2
|
+
import React, { useCallback, useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
function decodeJwtPayload(token) {
|
|
11
|
+
const parts = token.split(".");
|
|
12
|
+
if (parts.length !== 3) throw new EdgeBaseError(0, "Invalid JWT format");
|
|
13
|
+
const payload = parts[1];
|
|
14
|
+
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
15
|
+
const padded = base64 + "==".slice(0, (4 - base64.length % 4) % 4);
|
|
16
|
+
const binary = atob(padded);
|
|
17
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
18
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
19
|
+
}
|
|
20
|
+
function isTokenExpired(token, bufferSeconds = 30) {
|
|
21
|
+
try {
|
|
22
|
+
const payload = decodeJwtPayload(token);
|
|
23
|
+
const exp = payload.exp;
|
|
24
|
+
if (!exp) return true;
|
|
25
|
+
return Date.now() / 1e3 >= exp - bufferSeconds;
|
|
26
|
+
} catch {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function extractUser(token) {
|
|
31
|
+
try {
|
|
32
|
+
const payload = decodeJwtPayload(token);
|
|
33
|
+
return {
|
|
34
|
+
id: payload.sub,
|
|
35
|
+
email: payload.email,
|
|
36
|
+
displayName: payload.displayName,
|
|
37
|
+
avatarUrl: payload.avatarUrl,
|
|
38
|
+
role: payload.role,
|
|
39
|
+
isAnonymous: payload.isAnonymous,
|
|
40
|
+
emailVisibility: payload.emailVisibility,
|
|
41
|
+
custom: payload.custom
|
|
42
|
+
};
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
var REFRESH_TOKEN_KEY = "edgebase:refresh-token";
|
|
48
|
+
var PUSH_TOKEN_CACHE_KEY = "edgebase:push-token-cache";
|
|
49
|
+
var PUSH_DEVICE_ID_KEY = "edgebase:push-device-id";
|
|
50
|
+
var TokenManager = class {
|
|
51
|
+
constructor(baseUrl, storage) {
|
|
52
|
+
this.baseUrl = baseUrl;
|
|
53
|
+
this.storage = storage;
|
|
54
|
+
this.initPromise = this.restore();
|
|
55
|
+
}
|
|
56
|
+
accessToken = null;
|
|
57
|
+
refreshToken = null;
|
|
58
|
+
refreshPromise = null;
|
|
59
|
+
authStateListeners = [];
|
|
60
|
+
cachedUser = null;
|
|
61
|
+
storage;
|
|
62
|
+
initialized = false;
|
|
63
|
+
initPromise;
|
|
64
|
+
/** Wait for storage restore to complete */
|
|
65
|
+
async ready() {
|
|
66
|
+
return this.initPromise;
|
|
67
|
+
}
|
|
68
|
+
async restore() {
|
|
69
|
+
try {
|
|
70
|
+
const stored = await this.storage.getItem(REFRESH_TOKEN_KEY);
|
|
71
|
+
if (stored && !isTokenExpired(stored, 0)) {
|
|
72
|
+
this.refreshToken = stored;
|
|
73
|
+
this.cachedUser = extractUser(stored);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
this.initialized = true;
|
|
78
|
+
}
|
|
79
|
+
/** Get valid access token, refreshing if needed */
|
|
80
|
+
async getAccessToken(doRefresh) {
|
|
81
|
+
await this.initPromise;
|
|
82
|
+
if (this.accessToken && !isTokenExpired(this.accessToken)) {
|
|
83
|
+
return this.accessToken;
|
|
84
|
+
}
|
|
85
|
+
const refreshToken = this.refreshToken;
|
|
86
|
+
if (!refreshToken) return null;
|
|
87
|
+
if (this.refreshPromise) {
|
|
88
|
+
const result2 = await this.refreshPromise;
|
|
89
|
+
return result2.accessToken;
|
|
90
|
+
}
|
|
91
|
+
this.refreshPromise = doRefresh(refreshToken).then((tokens) => {
|
|
92
|
+
this.setTokens(tokens);
|
|
93
|
+
return tokens;
|
|
94
|
+
}).catch((err) => {
|
|
95
|
+
if (err instanceof EdgeBaseError && err.code === 401) {
|
|
96
|
+
this.clearTokens();
|
|
97
|
+
}
|
|
98
|
+
throw err;
|
|
99
|
+
}).finally(() => {
|
|
100
|
+
this.refreshPromise = null;
|
|
101
|
+
});
|
|
102
|
+
const result = await this.refreshPromise;
|
|
103
|
+
return result.accessToken;
|
|
104
|
+
}
|
|
105
|
+
/** Set tokens after successful auth (sync in-memory + async persist) */
|
|
106
|
+
setTokens(tokens) {
|
|
107
|
+
this.accessToken = tokens.accessToken;
|
|
108
|
+
this.refreshToken = tokens.refreshToken;
|
|
109
|
+
void this.storage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken);
|
|
110
|
+
this.updateUser(tokens.accessToken);
|
|
111
|
+
}
|
|
112
|
+
/** Get stored refresh token (sync from memory cache) */
|
|
113
|
+
getRefreshToken() {
|
|
114
|
+
return this.refreshToken;
|
|
115
|
+
}
|
|
116
|
+
/** Drop the current access token so the next request must refresh or fail fast. */
|
|
117
|
+
invalidateAccessToken() {
|
|
118
|
+
this.accessToken = null;
|
|
119
|
+
if (!this.refreshToken) {
|
|
120
|
+
this.cachedUser = null;
|
|
121
|
+
this.emitAuthStateChange(null);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** Read-only access to current access token (for websocket re-auth). */
|
|
125
|
+
get currentAccessToken() {
|
|
126
|
+
return this.accessToken;
|
|
127
|
+
}
|
|
128
|
+
/** Clear all tokens on sign-out */
|
|
129
|
+
clearTokens() {
|
|
130
|
+
this.accessToken = null;
|
|
131
|
+
this.refreshToken = null;
|
|
132
|
+
void this.storage.removeItem(REFRESH_TOKEN_KEY);
|
|
133
|
+
this.cachedUser = null;
|
|
134
|
+
this.emitAuthStateChange(null);
|
|
135
|
+
}
|
|
136
|
+
/** Get current user (from cached JWT payload) */
|
|
137
|
+
getCurrentUser() {
|
|
138
|
+
return this.cachedUser;
|
|
139
|
+
}
|
|
140
|
+
/** Subscribe to auth state changes. Fires immediately with current state. */
|
|
141
|
+
onAuthStateChange(handler) {
|
|
142
|
+
this.authStateListeners.push(handler);
|
|
143
|
+
handler(this.cachedUser);
|
|
144
|
+
return () => {
|
|
145
|
+
this.authStateListeners = this.authStateListeners.filter((h) => h !== handler);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
updateUser(accessToken) {
|
|
149
|
+
const user = extractUser(accessToken);
|
|
150
|
+
this.cachedUser = user;
|
|
151
|
+
this.emitAuthStateChange(user);
|
|
152
|
+
}
|
|
153
|
+
emitAuthStateChange(user) {
|
|
154
|
+
for (const listener of this.authStateListeners) {
|
|
155
|
+
listener(user);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** Clean up (no-op in RN, kept for API parity with web SDK) */
|
|
159
|
+
destroy() {
|
|
160
|
+
this.authStateListeners = [];
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/auth.ts
|
|
165
|
+
var AuthClient = class {
|
|
166
|
+
constructor(client, tokenManager, core, corePublic, linking) {
|
|
167
|
+
this.client = client;
|
|
168
|
+
this.tokenManager = tokenManager;
|
|
169
|
+
this.core = core;
|
|
170
|
+
this.corePublic = corePublic;
|
|
171
|
+
this.linking = linking;
|
|
172
|
+
this.baseUrl = client.getBaseUrl();
|
|
173
|
+
}
|
|
174
|
+
baseUrl;
|
|
175
|
+
/** Register a new user. Optionally include user metadata. */
|
|
176
|
+
async signUp(options) {
|
|
177
|
+
const body = {
|
|
178
|
+
email: options.email,
|
|
179
|
+
password: options.password
|
|
180
|
+
};
|
|
181
|
+
if (options.data) body.data = options.data;
|
|
182
|
+
if (options.captchaToken) body.captchaToken = options.captchaToken;
|
|
183
|
+
const result = await this.corePublic.authSignup(body);
|
|
184
|
+
this.tokenManager.setTokens({
|
|
185
|
+
accessToken: result.accessToken,
|
|
186
|
+
refreshToken: result.refreshToken
|
|
187
|
+
});
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
/** Sign in with email and password. Returns MfaRequiredResult if MFA is enabled. */
|
|
191
|
+
async signIn(options) {
|
|
192
|
+
const body = {
|
|
193
|
+
email: options.email,
|
|
194
|
+
password: options.password
|
|
195
|
+
};
|
|
196
|
+
if (options.captchaToken) body.captchaToken = options.captchaToken;
|
|
197
|
+
const result = await this.corePublic.authSignin(body);
|
|
198
|
+
if ("mfaRequired" in result && result.mfaRequired) {
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
const authResult = result;
|
|
202
|
+
this.tokenManager.setTokens({
|
|
203
|
+
accessToken: authResult.accessToken,
|
|
204
|
+
refreshToken: authResult.refreshToken
|
|
205
|
+
});
|
|
206
|
+
return authResult;
|
|
207
|
+
}
|
|
208
|
+
/** Sign out — revokes current session on server and clears local tokens. */
|
|
209
|
+
async signOut() {
|
|
210
|
+
try {
|
|
211
|
+
const refreshToken = await this.tokenManager.getRefreshToken();
|
|
212
|
+
if (refreshToken) {
|
|
213
|
+
await this.core.authSignout({ refreshToken });
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
this.tokenManager.clearTokens();
|
|
218
|
+
}
|
|
219
|
+
/** Refresh the current session using the stored refresh token. */
|
|
220
|
+
async refreshSession() {
|
|
221
|
+
const refreshToken = await this.tokenManager.getRefreshToken();
|
|
222
|
+
if (!refreshToken) {
|
|
223
|
+
throw new Error("No refresh token available.");
|
|
224
|
+
}
|
|
225
|
+
const result = await this.corePublic.authRefresh({ refreshToken });
|
|
226
|
+
this.tokenManager.setTokens({
|
|
227
|
+
accessToken: result.accessToken,
|
|
228
|
+
refreshToken: result.refreshToken
|
|
229
|
+
});
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Start OAuth sign-in flow.
|
|
234
|
+
* Opens the OAuth URL via Linking.openURL() and listens for the deep link callback.
|
|
235
|
+
* The app must be configured with a deep link scheme (e.g. myapp://auth/callback).
|
|
236
|
+
*
|
|
237
|
+
* @param provider - OAuth provider name (e.g. 'google', 'github')
|
|
238
|
+
* @param options.redirectUrl - Deep link URL to redirect back to after OAuth (required for RN)
|
|
239
|
+
* @param options.captchaToken - Optional captcha token
|
|
240
|
+
* @returns Promise that resolves with AuthResult when OAuth completes
|
|
241
|
+
*
|
|
242
|
+
* NOTE: Not delegated to Generated Core — this is URL construction + redirect, not a standard HTTP call.
|
|
243
|
+
*/
|
|
244
|
+
signInWithOAuth(providerOrOptions, options) {
|
|
245
|
+
const provider = typeof providerOrOptions === "string" ? providerOrOptions : providerOrOptions.provider;
|
|
246
|
+
const resolvedOptions = typeof providerOrOptions === "string" ? options : providerOrOptions;
|
|
247
|
+
let url = `${this.baseUrl}/api/auth/oauth/${encodeURIComponent(provider)}`;
|
|
248
|
+
if (resolvedOptions?.captchaToken) {
|
|
249
|
+
url += `?captcha_token=${encodeURIComponent(resolvedOptions.captchaToken)}`;
|
|
250
|
+
}
|
|
251
|
+
if (resolvedOptions?.redirectUrl) {
|
|
252
|
+
const sep = url.includes("?") ? "&" : "?";
|
|
253
|
+
url += `${sep}redirect_url=${encodeURIComponent(resolvedOptions.redirectUrl)}`;
|
|
254
|
+
}
|
|
255
|
+
if (this.linking) {
|
|
256
|
+
void this.linking.openURL(url);
|
|
257
|
+
}
|
|
258
|
+
return { url };
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Handle OAuth deep link callback.
|
|
262
|
+
* Call this when your app receives a deep link URL with auth tokens.
|
|
263
|
+
* Extract tokens from query params and store them.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* // In your navigation/linking config:
|
|
267
|
+
* Linking.addEventListener('url', ({ url }) => client.auth.handleOAuthCallback(url));
|
|
268
|
+
*/
|
|
269
|
+
async handleOAuthCallback(url) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = new URL(url);
|
|
272
|
+
const accessToken = parsed.searchParams.get("access_token");
|
|
273
|
+
const refreshToken = parsed.searchParams.get("refresh_token");
|
|
274
|
+
if (!accessToken || !refreshToken) return null;
|
|
275
|
+
this.tokenManager.setTokens({ accessToken, refreshToken });
|
|
276
|
+
return {
|
|
277
|
+
user: this.tokenManager.getCurrentUser(),
|
|
278
|
+
accessToken,
|
|
279
|
+
refreshToken
|
|
280
|
+
};
|
|
281
|
+
} catch {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
/** Sign in anonymously. */
|
|
286
|
+
async signInAnonymously(options) {
|
|
287
|
+
const body = options?.captchaToken ? { captchaToken: options.captchaToken } : void 0;
|
|
288
|
+
const result = await this.corePublic.authSigninAnonymous(body);
|
|
289
|
+
this.tokenManager.setTokens({
|
|
290
|
+
accessToken: result.accessToken,
|
|
291
|
+
refreshToken: result.refreshToken
|
|
292
|
+
});
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Send a magic link (passwordless login) email.
|
|
297
|
+
* If the email is not registered and autoCreate is enabled (server config), a new account is created.
|
|
298
|
+
*/
|
|
299
|
+
async signInWithMagicLink(options) {
|
|
300
|
+
const body = { email: options.email };
|
|
301
|
+
if (options.captchaToken) {
|
|
302
|
+
body.captchaToken = options.captchaToken;
|
|
303
|
+
}
|
|
304
|
+
await this.corePublic.authSigninMagicLink(body);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Verify a magic link token and sign in.
|
|
308
|
+
* Called after user clicks the link from their email.
|
|
309
|
+
*/
|
|
310
|
+
async verifyMagicLink(token) {
|
|
311
|
+
const result = await this.corePublic.authVerifyMagicLink({ token });
|
|
312
|
+
this.tokenManager.setTokens({
|
|
313
|
+
accessToken: result.accessToken,
|
|
314
|
+
refreshToken: result.refreshToken
|
|
315
|
+
});
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
// ─── Phone / SMS Auth ───
|
|
319
|
+
/**
|
|
320
|
+
* Send an SMS verification code to the given phone number.
|
|
321
|
+
* If the phone is not registered and autoCreate is enabled (server config), a new account is created on verify.
|
|
322
|
+
*/
|
|
323
|
+
async signInWithPhone(options) {
|
|
324
|
+
const body = { phone: options.phone };
|
|
325
|
+
if (options.captchaToken) {
|
|
326
|
+
body.captchaToken = options.captchaToken;
|
|
327
|
+
}
|
|
328
|
+
await this.corePublic.authSigninPhone(body);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Verify the SMS code and sign in.
|
|
332
|
+
* Called after user receives the code from signInWithPhone.
|
|
333
|
+
*/
|
|
334
|
+
async verifyPhone(options) {
|
|
335
|
+
const result = await this.corePublic.authVerifyPhone({
|
|
336
|
+
phone: options.phone,
|
|
337
|
+
code: options.code
|
|
338
|
+
});
|
|
339
|
+
this.tokenManager.setTokens({
|
|
340
|
+
accessToken: result.accessToken,
|
|
341
|
+
refreshToken: result.refreshToken
|
|
342
|
+
});
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
/** Link current account with a phone number. Sends an SMS code. */
|
|
346
|
+
async linkWithPhone(options) {
|
|
347
|
+
await this.core.authLinkPhone({ phone: options.phone });
|
|
348
|
+
}
|
|
349
|
+
/** Verify phone link code. Completes phone linking for the current account. */
|
|
350
|
+
async verifyLinkPhone(options) {
|
|
351
|
+
await this.core.authVerifyLinkPhone({
|
|
352
|
+
phone: options.phone,
|
|
353
|
+
code: options.code
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
/** Link anonymous account to email/password. */
|
|
357
|
+
async linkWithEmail(options) {
|
|
358
|
+
const result = await this.core.authLinkEmail({
|
|
359
|
+
email: options.email,
|
|
360
|
+
password: options.password
|
|
361
|
+
});
|
|
362
|
+
this.tokenManager.setTokens({
|
|
363
|
+
accessToken: result.accessToken,
|
|
364
|
+
refreshToken: result.refreshToken
|
|
365
|
+
});
|
|
366
|
+
return result;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Link anonymous account to OAuth provider. Returns URL to open in browser.
|
|
370
|
+
*
|
|
371
|
+
* NOTE: Not delegated — Generated Core's oauthLinkStart(provider) takes no body,
|
|
372
|
+
* but we need to pass { redirectUrl }.
|
|
373
|
+
*/
|
|
374
|
+
async linkWithOAuth(providerOrOptions, options) {
|
|
375
|
+
const provider = typeof providerOrOptions === "string" ? providerOrOptions : providerOrOptions.provider;
|
|
376
|
+
const resolvedOptions = typeof providerOrOptions === "string" ? options : providerOrOptions;
|
|
377
|
+
const redirectUrl = resolvedOptions?.redirectUrl ?? "";
|
|
378
|
+
const result = await this.client.post(
|
|
379
|
+
`/api/auth/oauth/link/${encodeURIComponent(provider)}`,
|
|
380
|
+
{ redirectUrl }
|
|
381
|
+
);
|
|
382
|
+
if (this.linking) {
|
|
383
|
+
void this.linking.openURL(result.redirectUrl);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
/** Subscribe to authentication state changes. */
|
|
388
|
+
onAuthStateChange(callback) {
|
|
389
|
+
return this.tokenManager.onAuthStateChange(callback);
|
|
390
|
+
}
|
|
391
|
+
/** Get current authenticated user (from cached JWT). */
|
|
392
|
+
get currentUser() {
|
|
393
|
+
return this.tokenManager.getCurrentUser();
|
|
394
|
+
}
|
|
395
|
+
/** List active sessions. */
|
|
396
|
+
async listSessions() {
|
|
397
|
+
const result = await this.core.authGetSessions();
|
|
398
|
+
return result.sessions;
|
|
399
|
+
}
|
|
400
|
+
/** Revoke a specific session. */
|
|
401
|
+
async revokeSession(sessionId) {
|
|
402
|
+
await this.core.authDeleteSession(sessionId);
|
|
403
|
+
}
|
|
404
|
+
/** Update current user's profile. */
|
|
405
|
+
async updateProfile(data) {
|
|
406
|
+
const result = await this.core.authUpdateProfile(data);
|
|
407
|
+
if (result.accessToken && result.refreshToken) {
|
|
408
|
+
this.tokenManager.setTokens({
|
|
409
|
+
accessToken: result.accessToken,
|
|
410
|
+
refreshToken: result.refreshToken
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
return this.tokenManager.getCurrentUser();
|
|
414
|
+
}
|
|
415
|
+
/** Verify email address with token. */
|
|
416
|
+
async verifyEmail(token) {
|
|
417
|
+
await this.corePublic.authVerifyEmail({ token });
|
|
418
|
+
}
|
|
419
|
+
/** Request a verification email for the current user. */
|
|
420
|
+
async requestEmailVerification(options) {
|
|
421
|
+
const body = {};
|
|
422
|
+
if (options?.redirectUrl) body.redirectUrl = options.redirectUrl;
|
|
423
|
+
await this.core.authRequestEmailVerification(body);
|
|
424
|
+
}
|
|
425
|
+
/** Verify a pending email change using the emailed token. */
|
|
426
|
+
async verifyEmailChange(token) {
|
|
427
|
+
await this.corePublic.authVerifyEmailChange({ token });
|
|
428
|
+
}
|
|
429
|
+
/** Request password reset email. */
|
|
430
|
+
async requestPasswordReset(email, options) {
|
|
431
|
+
const body = { email };
|
|
432
|
+
if (options?.captchaToken) body.captchaToken = options.captchaToken;
|
|
433
|
+
await this.corePublic.authRequestPasswordReset(body);
|
|
434
|
+
}
|
|
435
|
+
/** Reset password with token. */
|
|
436
|
+
async resetPassword(token, newPassword) {
|
|
437
|
+
await this.corePublic.authResetPassword({ token, newPassword });
|
|
438
|
+
}
|
|
439
|
+
/** Change password for authenticated user. */
|
|
440
|
+
async changePassword(options) {
|
|
441
|
+
const result = await this.core.authChangePassword({
|
|
442
|
+
currentPassword: options.currentPassword,
|
|
443
|
+
newPassword: options.newPassword
|
|
444
|
+
});
|
|
445
|
+
this.tokenManager.setTokens({
|
|
446
|
+
accessToken: result.accessToken,
|
|
447
|
+
refreshToken: result.refreshToken
|
|
448
|
+
});
|
|
449
|
+
return result;
|
|
450
|
+
}
|
|
451
|
+
/** Request an email change for the authenticated user. */
|
|
452
|
+
async changeEmail(options) {
|
|
453
|
+
const body = {
|
|
454
|
+
newEmail: options.newEmail,
|
|
455
|
+
password: options.password
|
|
456
|
+
};
|
|
457
|
+
if (options.redirectUrl) body.redirectUrl = options.redirectUrl;
|
|
458
|
+
await this.client.post("/api/auth/change-email", body);
|
|
459
|
+
}
|
|
460
|
+
/** List linked sign-in identities for the current user. */
|
|
461
|
+
async listIdentities() {
|
|
462
|
+
return this.client.get("/api/auth/identities");
|
|
463
|
+
}
|
|
464
|
+
/** Unlink a linked OAuth identity by its identity ID. */
|
|
465
|
+
async unlinkIdentity(identityId) {
|
|
466
|
+
return this.client.delete(`/api/auth/identities/${encodeURIComponent(identityId)}`);
|
|
467
|
+
}
|
|
468
|
+
/** Send an email OTP code for sign-in. */
|
|
469
|
+
async signInWithEmailOtp(options) {
|
|
470
|
+
await this.corePublic.authSigninEmailOtp({ email: options.email });
|
|
471
|
+
}
|
|
472
|
+
/** Verify an email OTP code and sign in. */
|
|
473
|
+
async verifyEmailOtp(options) {
|
|
474
|
+
const result = await this.corePublic.authVerifyEmailOtp({
|
|
475
|
+
email: options.email,
|
|
476
|
+
code: options.code
|
|
477
|
+
});
|
|
478
|
+
this.tokenManager.setTokens({
|
|
479
|
+
accessToken: result.accessToken,
|
|
480
|
+
refreshToken: result.refreshToken
|
|
481
|
+
});
|
|
482
|
+
return result;
|
|
483
|
+
}
|
|
484
|
+
// ─── Passkeys / WebAuthn REST layer ───
|
|
485
|
+
/** Generate WebAuthn registration options for the current authenticated user. */
|
|
486
|
+
async passkeysRegisterOptions() {
|
|
487
|
+
return this.core.authPasskeysRegisterOptions();
|
|
488
|
+
}
|
|
489
|
+
/** Verify and store a passkey registration response from the platform credential API. */
|
|
490
|
+
async passkeysRegister(response) {
|
|
491
|
+
return this.core.authPasskeysRegister({ response });
|
|
492
|
+
}
|
|
493
|
+
/** Generate WebAuthn authentication options. */
|
|
494
|
+
async passkeysAuthOptions(options) {
|
|
495
|
+
return this.corePublic.authPasskeysAuthOptions(options ?? {});
|
|
496
|
+
}
|
|
497
|
+
/** Verify a WebAuthn assertion and establish a session. */
|
|
498
|
+
async passkeysAuthenticate(response) {
|
|
499
|
+
const result = await this.corePublic.authPasskeysAuthenticate({ response });
|
|
500
|
+
this.tokenManager.setTokens({
|
|
501
|
+
accessToken: result.accessToken,
|
|
502
|
+
refreshToken: result.refreshToken
|
|
503
|
+
});
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
/** List registered passkeys for the current authenticated user. */
|
|
507
|
+
async passkeysList() {
|
|
508
|
+
return this.core.authPasskeysList();
|
|
509
|
+
}
|
|
510
|
+
/** Delete a registered passkey by credential ID. */
|
|
511
|
+
async passkeysDelete(credentialId) {
|
|
512
|
+
return this.core.authPasskeysDelete(credentialId);
|
|
513
|
+
}
|
|
514
|
+
// ─── MFA / TOTP ───
|
|
515
|
+
/** MFA sub-namespace for TOTP enrollment, verification, and management. */
|
|
516
|
+
get mfa() {
|
|
517
|
+
this.client;
|
|
518
|
+
const core = this.core;
|
|
519
|
+
const corePublic = this.corePublic;
|
|
520
|
+
const tokenManager = this.tokenManager;
|
|
521
|
+
return {
|
|
522
|
+
/** Enroll TOTP — returns secret, QR code URI, and recovery codes. */
|
|
523
|
+
async enrollTotp() {
|
|
524
|
+
return core.authMfaTotpEnroll();
|
|
525
|
+
},
|
|
526
|
+
/** Verify TOTP enrollment with factorId and a TOTP code. */
|
|
527
|
+
async verifyTotpEnrollment(factorId, code) {
|
|
528
|
+
return core.authMfaTotpVerify({ factorId, code });
|
|
529
|
+
},
|
|
530
|
+
/** Verify TOTP code during MFA challenge (after signIn returns mfaRequired). */
|
|
531
|
+
async verifyTotp(mfaTicket, code) {
|
|
532
|
+
const result = await corePublic.authMfaVerify({
|
|
533
|
+
mfaTicket,
|
|
534
|
+
code
|
|
535
|
+
});
|
|
536
|
+
tokenManager.setTokens({
|
|
537
|
+
accessToken: result.accessToken,
|
|
538
|
+
refreshToken: result.refreshToken
|
|
539
|
+
});
|
|
540
|
+
return result;
|
|
541
|
+
},
|
|
542
|
+
/** Use a recovery code during MFA challenge. */
|
|
543
|
+
async useRecoveryCode(mfaTicket, recoveryCode) {
|
|
544
|
+
const result = await corePublic.authMfaRecovery({
|
|
545
|
+
mfaTicket,
|
|
546
|
+
recoveryCode
|
|
547
|
+
});
|
|
548
|
+
tokenManager.setTokens({
|
|
549
|
+
accessToken: result.accessToken,
|
|
550
|
+
refreshToken: result.refreshToken
|
|
551
|
+
});
|
|
552
|
+
return result;
|
|
553
|
+
},
|
|
554
|
+
/**
|
|
555
|
+
* Disable TOTP for the current user. Requires password or TOTP code.
|
|
556
|
+
*/
|
|
557
|
+
async disableTotp(options) {
|
|
558
|
+
return core.authMfaTotpDelete(options ?? {});
|
|
559
|
+
},
|
|
560
|
+
/** List enrolled MFA factors for the current user. */
|
|
561
|
+
async listFactors() {
|
|
562
|
+
return core.authMfaFactors();
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
async function refreshAccessToken(baseUrl, refreshToken) {
|
|
568
|
+
let response;
|
|
569
|
+
try {
|
|
570
|
+
response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/auth/refresh`, {
|
|
571
|
+
method: "POST",
|
|
572
|
+
headers: { "Content-Type": "application/json" },
|
|
573
|
+
body: JSON.stringify({ refreshToken })
|
|
574
|
+
});
|
|
575
|
+
} catch (error) {
|
|
576
|
+
throw new EdgeBaseError(
|
|
577
|
+
0,
|
|
578
|
+
`Network error: ${error instanceof Error ? error.message : "Failed to refresh access token."}`
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
const body = await response.json().catch(() => null);
|
|
582
|
+
if (!response.ok) {
|
|
583
|
+
throw new EdgeBaseError(
|
|
584
|
+
response.status,
|
|
585
|
+
typeof body?.message === "string" ? body.message : "Failed to refresh access token."
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (!body?.accessToken || !body?.refreshToken) {
|
|
589
|
+
throw new EdgeBaseError(500, "Invalid auth refresh response.");
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
accessToken: body.accessToken,
|
|
593
|
+
refreshToken: body.refreshToken
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// src/database-live.ts
|
|
598
|
+
var DatabaseLiveClient = class {
|
|
599
|
+
constructor(baseUrl, tokenManager, options, contextManager) {
|
|
600
|
+
this.baseUrl = baseUrl;
|
|
601
|
+
this.tokenManager = tokenManager;
|
|
602
|
+
this.options = {
|
|
603
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
604
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
605
|
+
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3
|
|
606
|
+
};
|
|
607
|
+
if (contextManager) {
|
|
608
|
+
contextManager.onContextChange(() => this.handleContextChange());
|
|
609
|
+
}
|
|
610
|
+
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
611
|
+
this.handleAuthStateChange(user);
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
ws = null;
|
|
615
|
+
connectingPromise = null;
|
|
616
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
617
|
+
connectedChannels = /* @__PURE__ */ new Set();
|
|
618
|
+
channelFilters = /* @__PURE__ */ new Map();
|
|
619
|
+
channelOrFilters = /* @__PURE__ */ new Map();
|
|
620
|
+
errorHandlers = [];
|
|
621
|
+
reconnectAttempts = 0;
|
|
622
|
+
connected = false;
|
|
623
|
+
authenticated = false;
|
|
624
|
+
waitingForAuth = false;
|
|
625
|
+
authRecoveryPromise = null;
|
|
626
|
+
heartbeatTimer = null;
|
|
627
|
+
unsubAuthState = null;
|
|
628
|
+
options;
|
|
629
|
+
onSnapshot(channel, callback, clientFilters, serverFilters, serverOrFilters) {
|
|
630
|
+
const sub = {
|
|
631
|
+
channel,
|
|
632
|
+
handler: callback,
|
|
633
|
+
filters: clientFilters,
|
|
634
|
+
serverFilters,
|
|
635
|
+
serverOrFilters
|
|
636
|
+
};
|
|
637
|
+
if (!this.subscriptions.has(channel)) {
|
|
638
|
+
this.subscriptions.set(channel, []);
|
|
639
|
+
}
|
|
640
|
+
this.subscriptions.get(channel).push(sub);
|
|
641
|
+
if (sub.serverFilters && sub.serverFilters.length > 0) {
|
|
642
|
+
this.channelFilters.set(channel, sub.serverFilters);
|
|
643
|
+
}
|
|
644
|
+
if (sub.serverOrFilters && sub.serverOrFilters.length > 0) {
|
|
645
|
+
this.channelOrFilters.set(channel, sub.serverOrFilters);
|
|
646
|
+
}
|
|
647
|
+
this.connect(channel).catch(() => {
|
|
648
|
+
});
|
|
649
|
+
return () => {
|
|
650
|
+
const subs = this.subscriptions.get(channel);
|
|
651
|
+
if (!subs) return;
|
|
652
|
+
const idx = subs.indexOf(sub);
|
|
653
|
+
if (idx >= 0) subs.splice(idx, 1);
|
|
654
|
+
if (subs.length === 0) {
|
|
655
|
+
this.subscriptions.delete(channel);
|
|
656
|
+
this.channelFilters.delete(channel);
|
|
657
|
+
this.channelOrFilters.delete(channel);
|
|
658
|
+
this.sendUnsubscribe(channel);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
onError(handler) {
|
|
663
|
+
this.errorHandlers.push(handler);
|
|
664
|
+
return () => {
|
|
665
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
666
|
+
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
async connect(channel) {
|
|
670
|
+
this.connectedChannels.add(channel);
|
|
671
|
+
if (this.ws && this.connected) {
|
|
672
|
+
this.sendSubscribe(channel);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (!this.hasAuthContext()) {
|
|
676
|
+
this.waitingForAuth = true;
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
if (this.connectingPromise) {
|
|
680
|
+
return this.connectingPromise;
|
|
681
|
+
}
|
|
682
|
+
const connection = this.establishConnection(channel).finally(() => {
|
|
683
|
+
if (this.connectingPromise === connection) {
|
|
684
|
+
this.connectingPromise = null;
|
|
685
|
+
}
|
|
686
|
+
});
|
|
687
|
+
this.connectingPromise = connection;
|
|
688
|
+
return connection;
|
|
689
|
+
}
|
|
690
|
+
reconnect() {
|
|
691
|
+
if (this.connected || this.connectedChannels.size === 0) return;
|
|
692
|
+
const firstChannel = this.connectedChannels.values().next().value;
|
|
693
|
+
if (!firstChannel) return;
|
|
694
|
+
this.reconnectAttempts = 0;
|
|
695
|
+
this.options.autoReconnect = true;
|
|
696
|
+
this.connect(firstChannel).catch(() => {
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
disconnect() {
|
|
700
|
+
this.options.autoReconnect = false;
|
|
701
|
+
this.stopHeartbeat();
|
|
702
|
+
if (this.ws) {
|
|
703
|
+
this.ws.close(1e3, "Client disconnect");
|
|
704
|
+
this.ws = null;
|
|
705
|
+
}
|
|
706
|
+
this.connected = false;
|
|
707
|
+
this.authenticated = false;
|
|
708
|
+
this.connectingPromise = null;
|
|
709
|
+
this.connectedChannels.clear();
|
|
710
|
+
this.subscriptions.clear();
|
|
711
|
+
this.channelFilters.clear();
|
|
712
|
+
this.channelOrFilters.clear();
|
|
713
|
+
this.errorHandlers = [];
|
|
714
|
+
this.unsubAuthState?.();
|
|
715
|
+
this.unsubAuthState = null;
|
|
716
|
+
}
|
|
717
|
+
async establishConnection(channel) {
|
|
718
|
+
return new Promise((resolve, reject) => {
|
|
719
|
+
const ws = new WebSocket(this.buildWsUrl(channel));
|
|
720
|
+
this.ws = ws;
|
|
721
|
+
ws.onopen = () => {
|
|
722
|
+
this.connected = true;
|
|
723
|
+
this.reconnectAttempts = 0;
|
|
724
|
+
this.startHeartbeat();
|
|
725
|
+
this.authenticate().then(() => {
|
|
726
|
+
this.waitingForAuth = false;
|
|
727
|
+
resolve();
|
|
728
|
+
}).catch((error) => {
|
|
729
|
+
this.handleAuthenticationFailure(error);
|
|
730
|
+
reject(error);
|
|
731
|
+
});
|
|
732
|
+
};
|
|
733
|
+
ws.onmessage = (event) => {
|
|
734
|
+
this.handleMessage(event.data);
|
|
735
|
+
};
|
|
736
|
+
ws.onclose = () => {
|
|
737
|
+
this.connected = false;
|
|
738
|
+
this.authenticated = false;
|
|
739
|
+
this.ws = null;
|
|
740
|
+
this.stopHeartbeat();
|
|
741
|
+
if (this.options.autoReconnect && !this.waitingForAuth && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
742
|
+
this.scheduleReconnect(channel);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
ws.onerror = () => {
|
|
746
|
+
reject(new EdgeBaseError(500, "Database live WebSocket connection error"));
|
|
747
|
+
};
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
async authenticate() {
|
|
751
|
+
const token = await this.tokenManager.getAccessToken(
|
|
752
|
+
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
753
|
+
);
|
|
754
|
+
if (!token) throw new EdgeBaseError(401, "No access token available. Sign in first.");
|
|
755
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.1.0" });
|
|
756
|
+
return new Promise((resolve, reject) => {
|
|
757
|
+
const timeout = setTimeout(() => reject(new EdgeBaseError(401, "Auth timeout")), 1e4);
|
|
758
|
+
const original = this.ws?.onmessage;
|
|
759
|
+
if (!this.ws) return;
|
|
760
|
+
this.ws.onmessage = (event) => {
|
|
761
|
+
const msg = JSON.parse(event.data);
|
|
762
|
+
if (msg.type === "auth_success" || msg.type === "auth_refreshed") {
|
|
763
|
+
clearTimeout(timeout);
|
|
764
|
+
this.authenticated = true;
|
|
765
|
+
if (this.ws) this.ws.onmessage = original ?? null;
|
|
766
|
+
if (msg.type === "auth_refreshed") {
|
|
767
|
+
const revoked = msg.revokedChannels ?? [];
|
|
768
|
+
for (const channel of revoked) {
|
|
769
|
+
this.subscriptions.delete(channel);
|
|
770
|
+
this.channelFilters.delete(channel);
|
|
771
|
+
this.channelOrFilters.delete(channel);
|
|
772
|
+
this.connectedChannels.delete(channel);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
this.resubscribeAll();
|
|
776
|
+
resolve();
|
|
777
|
+
} else if (msg.type === "error") {
|
|
778
|
+
clearTimeout(timeout);
|
|
779
|
+
reject(new EdgeBaseError(401, msg.message));
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
handleMessage(raw) {
|
|
785
|
+
let msg;
|
|
786
|
+
try {
|
|
787
|
+
msg = JSON.parse(raw);
|
|
788
|
+
} catch {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const type = msg.type;
|
|
792
|
+
if (type === "db_change") {
|
|
793
|
+
const change = {
|
|
794
|
+
changeType: msg.changeType,
|
|
795
|
+
table: msg.table,
|
|
796
|
+
docId: msg.docId,
|
|
797
|
+
data: msg.data,
|
|
798
|
+
timestamp: msg.timestamp
|
|
799
|
+
};
|
|
800
|
+
const messageChannel = typeof msg.channel === "string" ? msg.channel : void 0;
|
|
801
|
+
for (const [channel, subs] of this.subscriptions.entries()) {
|
|
802
|
+
if (!matchesDatabaseLiveChannel(channel, change, messageChannel)) continue;
|
|
803
|
+
for (const sub of subs) {
|
|
804
|
+
if (sub.filters && change.data && !matchesClientFilter(change.data, sub.filters)) continue;
|
|
805
|
+
sub.handler(change);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (type === "batch_changes") {
|
|
811
|
+
const changes = msg.changes;
|
|
812
|
+
if (!Array.isArray(changes)) return;
|
|
813
|
+
for (const entry of changes) {
|
|
814
|
+
const change = {
|
|
815
|
+
changeType: entry.event,
|
|
816
|
+
table: msg.table ?? "",
|
|
817
|
+
docId: entry.docId,
|
|
818
|
+
data: entry.data,
|
|
819
|
+
timestamp: entry.timestamp
|
|
820
|
+
};
|
|
821
|
+
const messageChannel = typeof msg.channel === "string" ? msg.channel : void 0;
|
|
822
|
+
for (const [channel, subs] of this.subscriptions.entries()) {
|
|
823
|
+
if (!matchesDatabaseLiveChannel(channel, change, messageChannel)) continue;
|
|
824
|
+
for (const sub of subs) {
|
|
825
|
+
if (sub.filters && change.data && !matchesClientFilter(change.data, sub.filters)) continue;
|
|
826
|
+
sub.handler(change);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (type === "FILTER_RESYNC") {
|
|
833
|
+
this.resyncFilters();
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (type === "auth_refreshed") {
|
|
837
|
+
const revoked = msg.revokedChannels ?? [];
|
|
838
|
+
for (const channel of revoked) {
|
|
839
|
+
this.subscriptions.delete(channel);
|
|
840
|
+
this.channelFilters.delete(channel);
|
|
841
|
+
this.channelOrFilters.delete(channel);
|
|
842
|
+
this.connectedChannels.delete(channel);
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (type === "error") {
|
|
847
|
+
if (msg.code === "NOT_AUTHENTICATED" && this.hasAuthContext()) {
|
|
848
|
+
this.recoverAuthentication();
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
851
|
+
for (const handler of this.errorHandlers) {
|
|
852
|
+
handler({ code: msg.code, message: msg.message });
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
sendSubscribe(channel) {
|
|
857
|
+
if (!this.authenticated) return;
|
|
858
|
+
const filters = this.channelFilters.get(channel);
|
|
859
|
+
const orFilters = this.channelOrFilters.get(channel);
|
|
860
|
+
const msg = { type: "subscribe", channel };
|
|
861
|
+
if (filters && filters.length > 0) msg.filters = filters;
|
|
862
|
+
if (orFilters && orFilters.length > 0) msg.orFilters = orFilters;
|
|
863
|
+
this.sendRaw(msg);
|
|
864
|
+
}
|
|
865
|
+
sendUnsubscribe(channel) {
|
|
866
|
+
this.connectedChannels.delete(channel);
|
|
867
|
+
if (this.authenticated) this.sendRaw({ type: "unsubscribe", channel });
|
|
868
|
+
}
|
|
869
|
+
resubscribeAll() {
|
|
870
|
+
for (const channel of this.connectedChannels) this.sendSubscribe(channel);
|
|
871
|
+
}
|
|
872
|
+
refreshAuth() {
|
|
873
|
+
const token = this.tokenManager.currentAccessToken;
|
|
874
|
+
if (!token || !this.ws || !this.connected) return;
|
|
875
|
+
this.sendRaw({ type: "auth", token, sdkVersion: "0.1.0" });
|
|
876
|
+
}
|
|
877
|
+
handleAuthStateChange(user) {
|
|
878
|
+
if (user) {
|
|
879
|
+
if (this.ws && this.connected && this.authenticated) {
|
|
880
|
+
this.refreshAuth();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.waitingForAuth = false;
|
|
884
|
+
if (this.connectedChannels.size > 0 && (!this.ws || !this.connected)) {
|
|
885
|
+
const firstChannel = this.connectedChannels.values().next().value;
|
|
886
|
+
if (firstChannel) {
|
|
887
|
+
this.reconnectAttempts = 0;
|
|
888
|
+
this.connect(firstChannel).catch(() => {
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
this.waitingForAuth = this.connectedChannels.size > 0;
|
|
895
|
+
if (this.ws) {
|
|
896
|
+
const socket = this.ws;
|
|
897
|
+
this.stopHeartbeat();
|
|
898
|
+
this.ws = null;
|
|
899
|
+
this.connected = false;
|
|
900
|
+
this.authenticated = false;
|
|
901
|
+
try {
|
|
902
|
+
socket.close(1e3, "Signed out");
|
|
903
|
+
} catch {
|
|
904
|
+
}
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
this.connected = false;
|
|
908
|
+
this.authenticated = false;
|
|
909
|
+
}
|
|
910
|
+
handleAuthenticationFailure(error) {
|
|
911
|
+
const authError = error instanceof EdgeBaseError ? error : new EdgeBaseError(500, "Database live authentication failed.");
|
|
912
|
+
this.waitingForAuth = authError.code === 401 && this.connectedChannels.size > 0;
|
|
913
|
+
this.stopHeartbeat();
|
|
914
|
+
this.connected = false;
|
|
915
|
+
this.authenticated = false;
|
|
916
|
+
if (this.ws) {
|
|
917
|
+
const socket = this.ws;
|
|
918
|
+
this.ws = null;
|
|
919
|
+
try {
|
|
920
|
+
socket.close(4001, authError.message);
|
|
921
|
+
} catch {
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
resyncFilters() {
|
|
926
|
+
for (const [channel] of this.channelFilters) {
|
|
927
|
+
const filters = this.channelFilters.get(channel) ?? [];
|
|
928
|
+
const orFilters = this.channelOrFilters.get(channel) ?? [];
|
|
929
|
+
if (filters.length > 0 || orFilters.length > 0) {
|
|
930
|
+
const msg = { type: "subscribe", channel };
|
|
931
|
+
if (filters.length > 0) msg.filters = filters;
|
|
932
|
+
if (orFilters.length > 0) msg.orFilters = orFilters;
|
|
933
|
+
this.sendRaw(msg);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
scheduleReconnect(channel) {
|
|
938
|
+
const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
939
|
+
this.reconnectAttempts++;
|
|
940
|
+
setTimeout(() => {
|
|
941
|
+
this.connect(channel).catch(() => {
|
|
942
|
+
});
|
|
943
|
+
}, Math.min(delay, 3e4));
|
|
944
|
+
}
|
|
945
|
+
buildWsUrl(channel) {
|
|
946
|
+
const wsUrl = this.baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
947
|
+
return `${wsUrl}/api/db/subscribe?channel=${encodeURIComponent(channel)}`;
|
|
948
|
+
}
|
|
949
|
+
sendRaw(msg) {
|
|
950
|
+
if (this.ws && this.connected) this.ws.send(JSON.stringify(msg));
|
|
951
|
+
}
|
|
952
|
+
hasAuthContext() {
|
|
953
|
+
return Boolean(this.tokenManager.getCurrentUser() || this.tokenManager.getRefreshToken());
|
|
954
|
+
}
|
|
955
|
+
recoverAuthentication() {
|
|
956
|
+
if (this.authRecoveryPromise || !this.ws || !this.connected || !this.hasAuthContext()) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
this.authenticated = false;
|
|
960
|
+
this.waitingForAuth = true;
|
|
961
|
+
this.authRecoveryPromise = this.authenticate().then(() => {
|
|
962
|
+
this.waitingForAuth = false;
|
|
963
|
+
}).catch((error) => {
|
|
964
|
+
this.handleAuthenticationFailure(error);
|
|
965
|
+
for (const handler of this.errorHandlers) {
|
|
966
|
+
handler({
|
|
967
|
+
code: "NOT_AUTHENTICATED",
|
|
968
|
+
message: "Database live authentication was lost and recovery failed."
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
}).finally(() => {
|
|
972
|
+
this.authRecoveryPromise = null;
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
startHeartbeat() {
|
|
976
|
+
this.stopHeartbeat();
|
|
977
|
+
this.heartbeatTimer = setInterval(() => {
|
|
978
|
+
if (this.ws && this.connected) this.sendRaw({ type: "ping" });
|
|
979
|
+
}, 3e4);
|
|
980
|
+
}
|
|
981
|
+
stopHeartbeat() {
|
|
982
|
+
if (this.heartbeatTimer) {
|
|
983
|
+
clearInterval(this.heartbeatTimer);
|
|
984
|
+
this.heartbeatTimer = null;
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
handleContextChange() {
|
|
988
|
+
if (!this.ws || !this.connected) return;
|
|
989
|
+
if (this.connectedChannels.size === 0) return;
|
|
990
|
+
this.stopHeartbeat();
|
|
991
|
+
this.ws.close(1e3, "Context change");
|
|
992
|
+
this.ws = null;
|
|
993
|
+
this.connected = false;
|
|
994
|
+
this.authenticated = false;
|
|
995
|
+
this.reconnectAttempts = 0;
|
|
996
|
+
const firstChannel = this.connectedChannels.values().next().value;
|
|
997
|
+
if (firstChannel) this.connect(firstChannel).catch(() => {
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
};
|
|
1001
|
+
function matchesClientFilter(data, filters) {
|
|
1002
|
+
for (const [key, expected] of Object.entries(filters)) {
|
|
1003
|
+
if (data[key] !== expected) return false;
|
|
1004
|
+
}
|
|
1005
|
+
return true;
|
|
1006
|
+
}
|
|
1007
|
+
function matchesDatabaseLiveChannel(channel, change, messageChannel) {
|
|
1008
|
+
if (messageChannel) return channel === messageChannel;
|
|
1009
|
+
const parts = channel.split(":");
|
|
1010
|
+
if (parts[0] !== "dblive") return false;
|
|
1011
|
+
if (parts.length === 2) return parts[1] === change.table;
|
|
1012
|
+
if (parts.length === 3) return parts[2] === change.table;
|
|
1013
|
+
if (parts.length === 4) {
|
|
1014
|
+
if (parts[2] === change.table) return change.docId === parts[3];
|
|
1015
|
+
return parts[3] === change.table;
|
|
1016
|
+
}
|
|
1017
|
+
return parts[3] === change.table && change.docId === parts[4];
|
|
1018
|
+
}
|
|
1019
|
+
function deepSet(obj, path, value) {
|
|
1020
|
+
const parts = path.split(".");
|
|
1021
|
+
let current = obj;
|
|
1022
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1023
|
+
const key = parts[i];
|
|
1024
|
+
if (typeof current[key] !== "object" || current[key] === null) {
|
|
1025
|
+
current[key] = {};
|
|
1026
|
+
}
|
|
1027
|
+
current = current[key];
|
|
1028
|
+
}
|
|
1029
|
+
const lastKey = parts[parts.length - 1];
|
|
1030
|
+
if (value === null) {
|
|
1031
|
+
delete current[lastKey];
|
|
1032
|
+
} else {
|
|
1033
|
+
current[lastKey] = value;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function generateRequestId() {
|
|
1037
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
1038
|
+
return crypto.randomUUID();
|
|
1039
|
+
}
|
|
1040
|
+
return `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1041
|
+
}
|
|
1042
|
+
function cloneValue(value) {
|
|
1043
|
+
if (typeof structuredClone === "function") {
|
|
1044
|
+
return structuredClone(value);
|
|
1045
|
+
}
|
|
1046
|
+
return JSON.parse(JSON.stringify(value ?? null));
|
|
1047
|
+
}
|
|
1048
|
+
function cloneRecord(value) {
|
|
1049
|
+
return cloneValue(value);
|
|
1050
|
+
}
|
|
1051
|
+
var WS_CONNECTING = 0;
|
|
1052
|
+
var WS_OPEN = 1;
|
|
1053
|
+
var ROOM_EXPLICIT_LEAVE_CLOSE_CODE = 4005;
|
|
1054
|
+
var ROOM_EXPLICIT_LEAVE_REASON = "Client left room";
|
|
1055
|
+
var ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS = 40;
|
|
1056
|
+
function isSocketOpenOrConnecting(socket) {
|
|
1057
|
+
return !!socket && (socket.readyState === WS_OPEN || socket.readyState === WS_CONNECTING);
|
|
1058
|
+
}
|
|
1059
|
+
function closeSocketAfterLeave(socket, reason) {
|
|
1060
|
+
globalThis.setTimeout(() => {
|
|
1061
|
+
try {
|
|
1062
|
+
socket.close(ROOM_EXPLICIT_LEAVE_CLOSE_CODE, reason);
|
|
1063
|
+
} catch {
|
|
1064
|
+
}
|
|
1065
|
+
}, ROOM_EXPLICIT_LEAVE_CLOSE_DELAY_MS);
|
|
1066
|
+
}
|
|
1067
|
+
var RoomClient = class _RoomClient {
|
|
1068
|
+
baseUrl;
|
|
1069
|
+
tokenManager;
|
|
1070
|
+
options;
|
|
1071
|
+
/** Room namespace (e.g. 'game', 'chat') */
|
|
1072
|
+
namespace;
|
|
1073
|
+
/** Room instance ID within the namespace */
|
|
1074
|
+
roomId;
|
|
1075
|
+
// ─── State ───
|
|
1076
|
+
_sharedState = {};
|
|
1077
|
+
_sharedVersion = 0;
|
|
1078
|
+
_playerState = {};
|
|
1079
|
+
_playerVersion = 0;
|
|
1080
|
+
_members = [];
|
|
1081
|
+
_mediaMembers = [];
|
|
1082
|
+
// ─── Connection ───
|
|
1083
|
+
ws = null;
|
|
1084
|
+
reconnectAttempts = 0;
|
|
1085
|
+
connected = false;
|
|
1086
|
+
authenticated = false;
|
|
1087
|
+
joined = false;
|
|
1088
|
+
currentUserId = null;
|
|
1089
|
+
currentConnectionId = null;
|
|
1090
|
+
connectionState = "idle";
|
|
1091
|
+
reconnectInfo = null;
|
|
1092
|
+
connectingPromise = null;
|
|
1093
|
+
heartbeatTimer = null;
|
|
1094
|
+
intentionallyLeft = false;
|
|
1095
|
+
waitingForAuth = false;
|
|
1096
|
+
joinRequested = false;
|
|
1097
|
+
unsubAuthState = null;
|
|
1098
|
+
// ─── Pending send() requests (requestId → { resolve, reject, timeout }) ───
|
|
1099
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1100
|
+
pendingSignalRequests = /* @__PURE__ */ new Map();
|
|
1101
|
+
pendingAdminRequests = /* @__PURE__ */ new Map();
|
|
1102
|
+
pendingMemberStateRequests = /* @__PURE__ */ new Map();
|
|
1103
|
+
pendingMediaRequests = /* @__PURE__ */ new Map();
|
|
1104
|
+
// ─── Subscriptions ───
|
|
1105
|
+
sharedStateHandlers = [];
|
|
1106
|
+
playerStateHandlers = [];
|
|
1107
|
+
messageHandlers = /* @__PURE__ */ new Map();
|
|
1108
|
+
// messageType → handlers
|
|
1109
|
+
allMessageHandlers = [];
|
|
1110
|
+
errorHandlers = [];
|
|
1111
|
+
kickedHandlers = [];
|
|
1112
|
+
memberSyncHandlers = [];
|
|
1113
|
+
memberJoinHandlers = [];
|
|
1114
|
+
memberLeaveHandlers = [];
|
|
1115
|
+
memberStateHandlers = [];
|
|
1116
|
+
signalHandlers = /* @__PURE__ */ new Map();
|
|
1117
|
+
anySignalHandlers = [];
|
|
1118
|
+
mediaTrackHandlers = [];
|
|
1119
|
+
mediaTrackRemovedHandlers = [];
|
|
1120
|
+
mediaStateHandlers = [];
|
|
1121
|
+
mediaDeviceHandlers = [];
|
|
1122
|
+
reconnectHandlers = [];
|
|
1123
|
+
connectionStateHandlers = [];
|
|
1124
|
+
state = {
|
|
1125
|
+
getShared: () => this.getSharedState(),
|
|
1126
|
+
getMine: () => this.getPlayerState(),
|
|
1127
|
+
onSharedChange: (handler) => this.onSharedState(handler),
|
|
1128
|
+
onMineChange: (handler) => this.onPlayerState(handler),
|
|
1129
|
+
send: (actionType, payload) => this.send(actionType, payload)
|
|
1130
|
+
};
|
|
1131
|
+
meta = {
|
|
1132
|
+
get: () => this.getMetadata()
|
|
1133
|
+
};
|
|
1134
|
+
signals = {
|
|
1135
|
+
send: (event, payload, options) => this.sendSignal(event, payload, options),
|
|
1136
|
+
sendTo: (memberId, event, payload) => this.sendSignal(event, payload, { memberId }),
|
|
1137
|
+
on: (event, handler) => this.onSignal(event, handler),
|
|
1138
|
+
onAny: (handler) => this.onAnySignal(handler)
|
|
1139
|
+
};
|
|
1140
|
+
members = {
|
|
1141
|
+
list: () => cloneValue(this._members),
|
|
1142
|
+
onSync: (handler) => this.onMembersSync(handler),
|
|
1143
|
+
onJoin: (handler) => this.onMemberJoin(handler),
|
|
1144
|
+
onLeave: (handler) => this.onMemberLeave(handler),
|
|
1145
|
+
setState: (state) => this.sendMemberState(state),
|
|
1146
|
+
clearState: () => this.clearMemberState(),
|
|
1147
|
+
onStateChange: (handler) => this.onMemberStateChange(handler)
|
|
1148
|
+
};
|
|
1149
|
+
admin = {
|
|
1150
|
+
kick: (memberId) => this.sendAdmin("kick", memberId),
|
|
1151
|
+
mute: (memberId) => this.sendAdmin("mute", memberId),
|
|
1152
|
+
block: (memberId) => this.sendAdmin("block", memberId),
|
|
1153
|
+
setRole: (memberId, role) => this.sendAdmin("setRole", memberId, { role }),
|
|
1154
|
+
disableVideo: (memberId) => this.sendAdmin("disableVideo", memberId),
|
|
1155
|
+
stopScreenShare: (memberId) => this.sendAdmin("stopScreenShare", memberId)
|
|
1156
|
+
};
|
|
1157
|
+
media = {
|
|
1158
|
+
list: () => cloneValue(this._mediaMembers),
|
|
1159
|
+
audio: {
|
|
1160
|
+
enable: (payload) => this.sendMedia("publish", "audio", payload),
|
|
1161
|
+
disable: () => this.sendMedia("unpublish", "audio"),
|
|
1162
|
+
setMuted: (muted) => this.sendMedia("mute", "audio", { muted })
|
|
1163
|
+
},
|
|
1164
|
+
video: {
|
|
1165
|
+
enable: (payload) => this.sendMedia("publish", "video", payload),
|
|
1166
|
+
disable: () => this.sendMedia("unpublish", "video"),
|
|
1167
|
+
setMuted: (muted) => this.sendMedia("mute", "video", { muted })
|
|
1168
|
+
},
|
|
1169
|
+
screen: {
|
|
1170
|
+
start: (payload) => this.sendMedia("publish", "screen", payload),
|
|
1171
|
+
stop: () => this.sendMedia("unpublish", "screen")
|
|
1172
|
+
},
|
|
1173
|
+
devices: {
|
|
1174
|
+
switch: (payload) => this.switchMediaDevices(payload)
|
|
1175
|
+
},
|
|
1176
|
+
onTrack: (handler) => this.onMediaTrack(handler),
|
|
1177
|
+
onTrackRemoved: (handler) => this.onMediaTrackRemoved(handler),
|
|
1178
|
+
onStateChange: (handler) => this.onMediaStateChange(handler),
|
|
1179
|
+
onDeviceChange: (handler) => this.onMediaDeviceChange(handler)
|
|
1180
|
+
};
|
|
1181
|
+
session = {
|
|
1182
|
+
onError: (handler) => this.onError(handler),
|
|
1183
|
+
onKicked: (handler) => this.onKicked(handler),
|
|
1184
|
+
onReconnect: (handler) => this.onReconnect(handler),
|
|
1185
|
+
onConnectionStateChange: (handler) => this.onConnectionStateChange(handler)
|
|
1186
|
+
};
|
|
1187
|
+
constructor(baseUrl, namespace, roomId, tokenManager, options) {
|
|
1188
|
+
this.baseUrl = baseUrl;
|
|
1189
|
+
this.namespace = namespace;
|
|
1190
|
+
this.roomId = roomId;
|
|
1191
|
+
this.tokenManager = tokenManager;
|
|
1192
|
+
this.options = {
|
|
1193
|
+
autoReconnect: options?.autoReconnect ?? true,
|
|
1194
|
+
maxReconnectAttempts: options?.maxReconnectAttempts ?? 10,
|
|
1195
|
+
reconnectBaseDelay: options?.reconnectBaseDelay ?? 1e3,
|
|
1196
|
+
sendTimeout: options?.sendTimeout ?? 1e4
|
|
1197
|
+
};
|
|
1198
|
+
this.unsubAuthState = this.tokenManager.onAuthStateChange((user) => {
|
|
1199
|
+
this.handleAuthStateChange(user);
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
// ─── State Accessors ───
|
|
1203
|
+
/** Get current shared state (read-only snapshot) */
|
|
1204
|
+
getSharedState() {
|
|
1205
|
+
return cloneRecord(this._sharedState);
|
|
1206
|
+
}
|
|
1207
|
+
/** Get current player state (read-only snapshot) */
|
|
1208
|
+
getPlayerState() {
|
|
1209
|
+
return cloneRecord(this._playerState);
|
|
1210
|
+
}
|
|
1211
|
+
// ─── Metadata (HTTP, no WebSocket needed) ───
|
|
1212
|
+
/**
|
|
1213
|
+
* Get room metadata without joining (HTTP GET).
|
|
1214
|
+
* Returns developer-defined metadata set by room.setMetadata() on the server.
|
|
1215
|
+
*/
|
|
1216
|
+
async getMetadata() {
|
|
1217
|
+
return _RoomClient.getMetadata(this.baseUrl, this.namespace, this.roomId);
|
|
1218
|
+
}
|
|
1219
|
+
/**
|
|
1220
|
+
* Static: Get room metadata without creating a RoomClient instance.
|
|
1221
|
+
* Useful for lobby screens where you need room info before joining.
|
|
1222
|
+
*/
|
|
1223
|
+
static async getMetadata(baseUrl, namespace, roomId) {
|
|
1224
|
+
const url = `${baseUrl.replace(/\/$/, "")}/api/room/metadata?namespace=${encodeURIComponent(namespace)}&id=${encodeURIComponent(roomId)}`;
|
|
1225
|
+
const res = await fetch(url);
|
|
1226
|
+
if (!res.ok) {
|
|
1227
|
+
throw new EdgeBaseError(res.status, `Failed to get room metadata: ${res.statusText}`);
|
|
1228
|
+
}
|
|
1229
|
+
return res.json();
|
|
1230
|
+
}
|
|
1231
|
+
// ─── Connection Lifecycle ───
|
|
1232
|
+
/** Connect to the room, authenticate, and join */
|
|
1233
|
+
async join() {
|
|
1234
|
+
this.intentionallyLeft = false;
|
|
1235
|
+
this.joinRequested = true;
|
|
1236
|
+
if (isSocketOpenOrConnecting(this.ws)) {
|
|
1237
|
+
return this.connectingPromise ?? Promise.resolve();
|
|
1238
|
+
}
|
|
1239
|
+
this.setConnectionState(this.reconnectInfo ? "reconnecting" : "connecting");
|
|
1240
|
+
return this.ensureConnection();
|
|
1241
|
+
}
|
|
1242
|
+
/** Leave the room and disconnect. Cleans up all pending requests. */
|
|
1243
|
+
leave() {
|
|
1244
|
+
this.intentionallyLeft = true;
|
|
1245
|
+
this.joinRequested = false;
|
|
1246
|
+
this.waitingForAuth = false;
|
|
1247
|
+
this.stopHeartbeat();
|
|
1248
|
+
for (const [, pending] of this.pendingRequests) {
|
|
1249
|
+
clearTimeout(pending.timeout);
|
|
1250
|
+
pending.reject(new EdgeBaseError(499, "Room left"));
|
|
1251
|
+
}
|
|
1252
|
+
this.pendingRequests.clear();
|
|
1253
|
+
this.rejectPendingVoidRequests(this.pendingSignalRequests, new EdgeBaseError(499, "Room left"));
|
|
1254
|
+
this.rejectPendingVoidRequests(this.pendingAdminRequests, new EdgeBaseError(499, "Room left"));
|
|
1255
|
+
this.rejectPendingVoidRequests(this.pendingMemberStateRequests, new EdgeBaseError(499, "Room left"));
|
|
1256
|
+
this.rejectPendingVoidRequests(this.pendingMediaRequests, new EdgeBaseError(499, "Room left"));
|
|
1257
|
+
if (this.ws) {
|
|
1258
|
+
const socket = this.ws;
|
|
1259
|
+
this.sendRaw({ type: "leave" });
|
|
1260
|
+
closeSocketAfterLeave(socket, ROOM_EXPLICIT_LEAVE_REASON);
|
|
1261
|
+
this.ws = null;
|
|
1262
|
+
}
|
|
1263
|
+
this.connected = false;
|
|
1264
|
+
this.authenticated = false;
|
|
1265
|
+
this.joined = false;
|
|
1266
|
+
this.connectingPromise = null;
|
|
1267
|
+
this._sharedState = {};
|
|
1268
|
+
this._sharedVersion = 0;
|
|
1269
|
+
this._playerState = {};
|
|
1270
|
+
this._playerVersion = 0;
|
|
1271
|
+
this._members = [];
|
|
1272
|
+
this._mediaMembers = [];
|
|
1273
|
+
this.currentUserId = null;
|
|
1274
|
+
this.currentConnectionId = null;
|
|
1275
|
+
this.reconnectInfo = null;
|
|
1276
|
+
this.setConnectionState("disconnected");
|
|
1277
|
+
}
|
|
1278
|
+
// ─── Actions ───
|
|
1279
|
+
/**
|
|
1280
|
+
* Send an action to the server.
|
|
1281
|
+
* Returns a Promise that resolves with the action result from the server.
|
|
1282
|
+
*
|
|
1283
|
+
* @example
|
|
1284
|
+
* const result = await room.send('SET_SCORE', { score: 42 });
|
|
1285
|
+
*/
|
|
1286
|
+
async send(actionType, payload) {
|
|
1287
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1288
|
+
throw new EdgeBaseError(400, "Not connected to room");
|
|
1289
|
+
}
|
|
1290
|
+
const requestId = generateRequestId();
|
|
1291
|
+
return new Promise((resolve, reject) => {
|
|
1292
|
+
const timeout = setTimeout(() => {
|
|
1293
|
+
this.pendingRequests.delete(requestId);
|
|
1294
|
+
reject(new EdgeBaseError(408, `Action '${actionType}' timed out`));
|
|
1295
|
+
}, this.options.sendTimeout);
|
|
1296
|
+
this.pendingRequests.set(requestId, { resolve, reject, timeout });
|
|
1297
|
+
this.sendRaw({
|
|
1298
|
+
type: "send",
|
|
1299
|
+
actionType,
|
|
1300
|
+
payload: payload ?? {},
|
|
1301
|
+
requestId
|
|
1302
|
+
});
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
// ─── Subscriptions (v2 API) ───
|
|
1306
|
+
/**
|
|
1307
|
+
* Subscribe to shared state changes.
|
|
1308
|
+
* Called on full sync and on each shared_delta.
|
|
1309
|
+
*
|
|
1310
|
+
* @returns Subscription with unsubscribe()
|
|
1311
|
+
*/
|
|
1312
|
+
onSharedState(handler) {
|
|
1313
|
+
this.sharedStateHandlers.push(handler);
|
|
1314
|
+
return {
|
|
1315
|
+
unsubscribe: () => {
|
|
1316
|
+
const idx = this.sharedStateHandlers.indexOf(handler);
|
|
1317
|
+
if (idx >= 0) this.sharedStateHandlers.splice(idx, 1);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Subscribe to player state changes.
|
|
1323
|
+
* Called on full sync and on each player_delta.
|
|
1324
|
+
*
|
|
1325
|
+
* @returns Subscription with unsubscribe()
|
|
1326
|
+
*/
|
|
1327
|
+
onPlayerState(handler) {
|
|
1328
|
+
this.playerStateHandlers.push(handler);
|
|
1329
|
+
return {
|
|
1330
|
+
unsubscribe: () => {
|
|
1331
|
+
const idx = this.playerStateHandlers.indexOf(handler);
|
|
1332
|
+
if (idx >= 0) this.playerStateHandlers.splice(idx, 1);
|
|
1333
|
+
}
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* Subscribe to messages of a specific type sent by room.sendMessage().
|
|
1338
|
+
*
|
|
1339
|
+
* @example
|
|
1340
|
+
* room.onMessage('game_over', (data) => { console.log(data.winner); });
|
|
1341
|
+
*
|
|
1342
|
+
* @returns Subscription with unsubscribe()
|
|
1343
|
+
*/
|
|
1344
|
+
onMessage(messageType, handler) {
|
|
1345
|
+
if (!this.messageHandlers.has(messageType)) {
|
|
1346
|
+
this.messageHandlers.set(messageType, []);
|
|
1347
|
+
}
|
|
1348
|
+
this.messageHandlers.get(messageType).push(handler);
|
|
1349
|
+
return {
|
|
1350
|
+
unsubscribe: () => {
|
|
1351
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
1352
|
+
if (handlers) {
|
|
1353
|
+
const idx = handlers.indexOf(handler);
|
|
1354
|
+
if (idx >= 0) handlers.splice(idx, 1);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Subscribe to ALL messages regardless of type.
|
|
1361
|
+
*
|
|
1362
|
+
* @returns Subscription with unsubscribe()
|
|
1363
|
+
*/
|
|
1364
|
+
onAnyMessage(handler) {
|
|
1365
|
+
this.allMessageHandlers.push(handler);
|
|
1366
|
+
return {
|
|
1367
|
+
unsubscribe: () => {
|
|
1368
|
+
const idx = this.allMessageHandlers.indexOf(handler);
|
|
1369
|
+
if (idx >= 0) this.allMessageHandlers.splice(idx, 1);
|
|
1370
|
+
}
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
/** Subscribe to errors */
|
|
1374
|
+
onError(handler) {
|
|
1375
|
+
this.errorHandlers.push(handler);
|
|
1376
|
+
return {
|
|
1377
|
+
unsubscribe: () => {
|
|
1378
|
+
const idx = this.errorHandlers.indexOf(handler);
|
|
1379
|
+
if (idx >= 0) this.errorHandlers.splice(idx, 1);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
/** Subscribe to kick events */
|
|
1384
|
+
onKicked(handler) {
|
|
1385
|
+
this.kickedHandlers.push(handler);
|
|
1386
|
+
return {
|
|
1387
|
+
unsubscribe: () => {
|
|
1388
|
+
const idx = this.kickedHandlers.indexOf(handler);
|
|
1389
|
+
if (idx >= 0) this.kickedHandlers.splice(idx, 1);
|
|
1390
|
+
}
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
onSignal(event, handler) {
|
|
1394
|
+
if (!this.signalHandlers.has(event)) {
|
|
1395
|
+
this.signalHandlers.set(event, []);
|
|
1396
|
+
}
|
|
1397
|
+
this.signalHandlers.get(event).push(handler);
|
|
1398
|
+
return {
|
|
1399
|
+
unsubscribe: () => {
|
|
1400
|
+
const handlers = this.signalHandlers.get(event);
|
|
1401
|
+
if (!handlers) return;
|
|
1402
|
+
const index = handlers.indexOf(handler);
|
|
1403
|
+
if (index >= 0) handlers.splice(index, 1);
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
onAnySignal(handler) {
|
|
1408
|
+
this.anySignalHandlers.push(handler);
|
|
1409
|
+
return {
|
|
1410
|
+
unsubscribe: () => {
|
|
1411
|
+
const index = this.anySignalHandlers.indexOf(handler);
|
|
1412
|
+
if (index >= 0) this.anySignalHandlers.splice(index, 1);
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
onMembersSync(handler) {
|
|
1417
|
+
this.memberSyncHandlers.push(handler);
|
|
1418
|
+
return {
|
|
1419
|
+
unsubscribe: () => {
|
|
1420
|
+
const index = this.memberSyncHandlers.indexOf(handler);
|
|
1421
|
+
if (index >= 0) this.memberSyncHandlers.splice(index, 1);
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
onMemberJoin(handler) {
|
|
1426
|
+
this.memberJoinHandlers.push(handler);
|
|
1427
|
+
return {
|
|
1428
|
+
unsubscribe: () => {
|
|
1429
|
+
const index = this.memberJoinHandlers.indexOf(handler);
|
|
1430
|
+
if (index >= 0) this.memberJoinHandlers.splice(index, 1);
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
onMemberLeave(handler) {
|
|
1435
|
+
this.memberLeaveHandlers.push(handler);
|
|
1436
|
+
return {
|
|
1437
|
+
unsubscribe: () => {
|
|
1438
|
+
const index = this.memberLeaveHandlers.indexOf(handler);
|
|
1439
|
+
if (index >= 0) this.memberLeaveHandlers.splice(index, 1);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
onMemberStateChange(handler) {
|
|
1444
|
+
this.memberStateHandlers.push(handler);
|
|
1445
|
+
return {
|
|
1446
|
+
unsubscribe: () => {
|
|
1447
|
+
const index = this.memberStateHandlers.indexOf(handler);
|
|
1448
|
+
if (index >= 0) this.memberStateHandlers.splice(index, 1);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
onReconnect(handler) {
|
|
1453
|
+
this.reconnectHandlers.push(handler);
|
|
1454
|
+
return {
|
|
1455
|
+
unsubscribe: () => {
|
|
1456
|
+
const index = this.reconnectHandlers.indexOf(handler);
|
|
1457
|
+
if (index >= 0) this.reconnectHandlers.splice(index, 1);
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
onConnectionStateChange(handler) {
|
|
1462
|
+
this.connectionStateHandlers.push(handler);
|
|
1463
|
+
return {
|
|
1464
|
+
unsubscribe: () => {
|
|
1465
|
+
const index = this.connectionStateHandlers.indexOf(handler);
|
|
1466
|
+
if (index >= 0) this.connectionStateHandlers.splice(index, 1);
|
|
1467
|
+
}
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
onMediaTrack(handler) {
|
|
1471
|
+
this.mediaTrackHandlers.push(handler);
|
|
1472
|
+
return {
|
|
1473
|
+
unsubscribe: () => {
|
|
1474
|
+
const index = this.mediaTrackHandlers.indexOf(handler);
|
|
1475
|
+
if (index >= 0) this.mediaTrackHandlers.splice(index, 1);
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
onMediaTrackRemoved(handler) {
|
|
1480
|
+
this.mediaTrackRemovedHandlers.push(handler);
|
|
1481
|
+
return {
|
|
1482
|
+
unsubscribe: () => {
|
|
1483
|
+
const index = this.mediaTrackRemovedHandlers.indexOf(handler);
|
|
1484
|
+
if (index >= 0) this.mediaTrackRemovedHandlers.splice(index, 1);
|
|
1485
|
+
}
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
onMediaStateChange(handler) {
|
|
1489
|
+
this.mediaStateHandlers.push(handler);
|
|
1490
|
+
return {
|
|
1491
|
+
unsubscribe: () => {
|
|
1492
|
+
const index = this.mediaStateHandlers.indexOf(handler);
|
|
1493
|
+
if (index >= 0) this.mediaStateHandlers.splice(index, 1);
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
onMediaDeviceChange(handler) {
|
|
1498
|
+
this.mediaDeviceHandlers.push(handler);
|
|
1499
|
+
return {
|
|
1500
|
+
unsubscribe: () => {
|
|
1501
|
+
const index = this.mediaDeviceHandlers.indexOf(handler);
|
|
1502
|
+
if (index >= 0) this.mediaDeviceHandlers.splice(index, 1);
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async sendSignal(event, payload, options) {
|
|
1507
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1508
|
+
throw new EdgeBaseError(400, "Not connected to room");
|
|
1509
|
+
}
|
|
1510
|
+
const requestId = generateRequestId();
|
|
1511
|
+
return new Promise((resolve, reject) => {
|
|
1512
|
+
const timeout = setTimeout(() => {
|
|
1513
|
+
this.pendingSignalRequests.delete(requestId);
|
|
1514
|
+
reject(new EdgeBaseError(408, `Signal '${event}' timed out`));
|
|
1515
|
+
}, this.options.sendTimeout);
|
|
1516
|
+
this.pendingSignalRequests.set(requestId, { resolve, reject, timeout });
|
|
1517
|
+
this.sendRaw({
|
|
1518
|
+
type: "signal",
|
|
1519
|
+
event,
|
|
1520
|
+
payload: payload ?? {},
|
|
1521
|
+
includeSelf: options?.includeSelf === true,
|
|
1522
|
+
memberId: options?.memberId,
|
|
1523
|
+
requestId
|
|
1524
|
+
});
|
|
1525
|
+
});
|
|
1526
|
+
}
|
|
1527
|
+
async sendMemberState(state) {
|
|
1528
|
+
return this.sendMemberStateRequest({
|
|
1529
|
+
type: "member_state",
|
|
1530
|
+
state
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
async clearMemberState() {
|
|
1534
|
+
return this.sendMemberStateRequest({
|
|
1535
|
+
type: "member_state_clear"
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
async sendMemberStateRequest(payload) {
|
|
1539
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1540
|
+
throw new EdgeBaseError(400, "Not connected to room");
|
|
1541
|
+
}
|
|
1542
|
+
const requestId = generateRequestId();
|
|
1543
|
+
return new Promise((resolve, reject) => {
|
|
1544
|
+
const timeout = setTimeout(() => {
|
|
1545
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
1546
|
+
reject(new EdgeBaseError(408, "Member state update timed out"));
|
|
1547
|
+
}, this.options.sendTimeout);
|
|
1548
|
+
this.pendingMemberStateRequests.set(requestId, { resolve, reject, timeout });
|
|
1549
|
+
this.sendRaw({ ...payload, requestId });
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
async sendAdmin(operation, memberId, payload) {
|
|
1553
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1554
|
+
throw new EdgeBaseError(400, "Not connected to room");
|
|
1555
|
+
}
|
|
1556
|
+
const requestId = generateRequestId();
|
|
1557
|
+
return new Promise((resolve, reject) => {
|
|
1558
|
+
const timeout = setTimeout(() => {
|
|
1559
|
+
this.pendingAdminRequests.delete(requestId);
|
|
1560
|
+
reject(new EdgeBaseError(408, `Admin operation '${operation}' timed out`));
|
|
1561
|
+
}, this.options.sendTimeout);
|
|
1562
|
+
this.pendingAdminRequests.set(requestId, { resolve, reject, timeout });
|
|
1563
|
+
this.sendRaw({
|
|
1564
|
+
type: "admin",
|
|
1565
|
+
operation,
|
|
1566
|
+
memberId,
|
|
1567
|
+
payload: payload ?? {},
|
|
1568
|
+
requestId
|
|
1569
|
+
});
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
async sendMedia(operation, kind, payload) {
|
|
1573
|
+
if (!this.ws || !this.connected || !this.authenticated) {
|
|
1574
|
+
throw new EdgeBaseError(400, "Not connected to room");
|
|
1575
|
+
}
|
|
1576
|
+
const requestId = generateRequestId();
|
|
1577
|
+
return new Promise((resolve, reject) => {
|
|
1578
|
+
const timeout = setTimeout(() => {
|
|
1579
|
+
this.pendingMediaRequests.delete(requestId);
|
|
1580
|
+
reject(new EdgeBaseError(408, `Media operation '${operation}' timed out`));
|
|
1581
|
+
}, this.options.sendTimeout);
|
|
1582
|
+
this.pendingMediaRequests.set(requestId, { resolve, reject, timeout });
|
|
1583
|
+
this.sendRaw({
|
|
1584
|
+
type: "media",
|
|
1585
|
+
operation,
|
|
1586
|
+
kind,
|
|
1587
|
+
payload: payload ?? {},
|
|
1588
|
+
requestId
|
|
1589
|
+
});
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
async switchMediaDevices(payload) {
|
|
1593
|
+
const operations = [];
|
|
1594
|
+
if (payload.audioInputId) {
|
|
1595
|
+
operations.push(this.sendMedia("device", "audio", { deviceId: payload.audioInputId }));
|
|
1596
|
+
}
|
|
1597
|
+
if (payload.videoInputId) {
|
|
1598
|
+
operations.push(this.sendMedia("device", "video", { deviceId: payload.videoInputId }));
|
|
1599
|
+
}
|
|
1600
|
+
if (payload.screenInputId) {
|
|
1601
|
+
operations.push(this.sendMedia("device", "screen", { deviceId: payload.screenInputId }));
|
|
1602
|
+
}
|
|
1603
|
+
await Promise.all(operations);
|
|
1604
|
+
}
|
|
1605
|
+
// ─── Private: Connection ───
|
|
1606
|
+
async establishConnection() {
|
|
1607
|
+
return new Promise((resolve, reject) => {
|
|
1608
|
+
const wsUrl = this.buildWsUrl();
|
|
1609
|
+
const ws = new WebSocket(wsUrl);
|
|
1610
|
+
this.ws = ws;
|
|
1611
|
+
ws.onopen = () => {
|
|
1612
|
+
this.connected = true;
|
|
1613
|
+
this.reconnectAttempts = 0;
|
|
1614
|
+
this.startHeartbeat();
|
|
1615
|
+
this.authenticate().then(() => {
|
|
1616
|
+
this.waitingForAuth = false;
|
|
1617
|
+
resolve();
|
|
1618
|
+
}).catch((error) => {
|
|
1619
|
+
this.handleAuthenticationFailure(error);
|
|
1620
|
+
reject(error);
|
|
1621
|
+
});
|
|
1622
|
+
};
|
|
1623
|
+
ws.onmessage = (event) => {
|
|
1624
|
+
this.handleMessage(event.data);
|
|
1625
|
+
};
|
|
1626
|
+
ws.onclose = (event) => {
|
|
1627
|
+
this.connected = false;
|
|
1628
|
+
this.authenticated = false;
|
|
1629
|
+
this.joined = false;
|
|
1630
|
+
this.ws = null;
|
|
1631
|
+
this.stopHeartbeat();
|
|
1632
|
+
if (event.code === 4004 && this.connectionState !== "kicked") {
|
|
1633
|
+
this.handleKicked();
|
|
1634
|
+
}
|
|
1635
|
+
if (!this.intentionallyLeft && !this.waitingForAuth && this.options.autoReconnect && this.reconnectAttempts < this.options.maxReconnectAttempts) {
|
|
1636
|
+
this.scheduleReconnect();
|
|
1637
|
+
} else if (!this.intentionallyLeft && this.connectionState !== "kicked" && this.connectionState !== "auth_lost") {
|
|
1638
|
+
this.setConnectionState("disconnected");
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
ws.onerror = () => {
|
|
1642
|
+
reject(new EdgeBaseError(500, "Room WebSocket connection error"));
|
|
1643
|
+
};
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
ensureConnection() {
|
|
1647
|
+
if (this.connectingPromise) {
|
|
1648
|
+
return this.connectingPromise;
|
|
1649
|
+
}
|
|
1650
|
+
const nextPromise = this.establishConnection().finally(() => {
|
|
1651
|
+
if (this.connectingPromise === nextPromise) {
|
|
1652
|
+
this.connectingPromise = null;
|
|
1653
|
+
}
|
|
1654
|
+
});
|
|
1655
|
+
this.connectingPromise = nextPromise;
|
|
1656
|
+
return nextPromise;
|
|
1657
|
+
}
|
|
1658
|
+
async authenticate() {
|
|
1659
|
+
const token = await this.tokenManager.getAccessToken(
|
|
1660
|
+
(refreshToken) => refreshAccessToken(this.baseUrl, refreshToken)
|
|
1661
|
+
);
|
|
1662
|
+
if (!token) {
|
|
1663
|
+
throw new EdgeBaseError(401, "No access token available. Sign in first.");
|
|
1664
|
+
}
|
|
1665
|
+
return new Promise((resolve, reject) => {
|
|
1666
|
+
const timeout = setTimeout(() => {
|
|
1667
|
+
reject(new EdgeBaseError(401, "Room auth timeout"));
|
|
1668
|
+
}, 1e4);
|
|
1669
|
+
const originalOnMessage = this.ws?.onmessage;
|
|
1670
|
+
if (this.ws) {
|
|
1671
|
+
this.ws.onmessage = (event) => {
|
|
1672
|
+
const msg = JSON.parse(event.data);
|
|
1673
|
+
if (msg.type === "auth_success" || msg.type === "auth_refreshed") {
|
|
1674
|
+
clearTimeout(timeout);
|
|
1675
|
+
this.authenticated = true;
|
|
1676
|
+
this.currentUserId = typeof msg.userId === "string" ? msg.userId : this.currentUserId;
|
|
1677
|
+
this.currentConnectionId = typeof msg.connectionId === "string" ? msg.connectionId : this.currentConnectionId;
|
|
1678
|
+
if (this.ws) this.ws.onmessage = originalOnMessage ?? null;
|
|
1679
|
+
this.sendRaw({
|
|
1680
|
+
type: "join",
|
|
1681
|
+
lastSharedState: this._sharedState,
|
|
1682
|
+
lastSharedVersion: this._sharedVersion,
|
|
1683
|
+
lastPlayerState: this._playerState,
|
|
1684
|
+
lastPlayerVersion: this._playerVersion
|
|
1685
|
+
});
|
|
1686
|
+
this.joined = true;
|
|
1687
|
+
resolve();
|
|
1688
|
+
} else if (msg.type === "error") {
|
|
1689
|
+
clearTimeout(timeout);
|
|
1690
|
+
reject(new EdgeBaseError(401, msg.message));
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
this.sendRaw({ type: "auth", token });
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
// ─── Private: Message Handling ───
|
|
1698
|
+
handleMessage(raw) {
|
|
1699
|
+
let msg;
|
|
1700
|
+
try {
|
|
1701
|
+
msg = JSON.parse(raw);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const type = msg.type;
|
|
1706
|
+
switch (type) {
|
|
1707
|
+
case "auth_success":
|
|
1708
|
+
case "auth_refreshed":
|
|
1709
|
+
this.handleAuthAck(msg);
|
|
1710
|
+
break;
|
|
1711
|
+
case "sync":
|
|
1712
|
+
this.handleSync(msg);
|
|
1713
|
+
break;
|
|
1714
|
+
case "shared_delta":
|
|
1715
|
+
this.handleSharedDelta(msg);
|
|
1716
|
+
break;
|
|
1717
|
+
case "player_delta":
|
|
1718
|
+
this.handlePlayerDelta(msg);
|
|
1719
|
+
break;
|
|
1720
|
+
case "action_result":
|
|
1721
|
+
this.handleActionResult(msg);
|
|
1722
|
+
break;
|
|
1723
|
+
case "action_error":
|
|
1724
|
+
this.handleActionError(msg);
|
|
1725
|
+
break;
|
|
1726
|
+
case "message":
|
|
1727
|
+
this.handleServerMessage(msg);
|
|
1728
|
+
break;
|
|
1729
|
+
case "signal":
|
|
1730
|
+
this.handleSignalFrame(msg);
|
|
1731
|
+
break;
|
|
1732
|
+
case "signal_sent":
|
|
1733
|
+
this.handleSignalSent(msg);
|
|
1734
|
+
break;
|
|
1735
|
+
case "signal_error":
|
|
1736
|
+
this.handleSignalError(msg);
|
|
1737
|
+
break;
|
|
1738
|
+
case "members_sync":
|
|
1739
|
+
this.handleMembersSync(msg);
|
|
1740
|
+
break;
|
|
1741
|
+
case "media_sync":
|
|
1742
|
+
this.handleMediaSync(msg);
|
|
1743
|
+
break;
|
|
1744
|
+
case "member_join":
|
|
1745
|
+
this.handleMemberJoinFrame(msg);
|
|
1746
|
+
break;
|
|
1747
|
+
case "member_leave":
|
|
1748
|
+
this.handleMemberLeaveFrame(msg);
|
|
1749
|
+
break;
|
|
1750
|
+
case "member_state":
|
|
1751
|
+
this.handleMemberStateFrame(msg);
|
|
1752
|
+
break;
|
|
1753
|
+
case "member_state_error":
|
|
1754
|
+
this.handleMemberStateError(msg);
|
|
1755
|
+
break;
|
|
1756
|
+
case "media_track":
|
|
1757
|
+
this.handleMediaTrackFrame(msg);
|
|
1758
|
+
break;
|
|
1759
|
+
case "media_track_removed":
|
|
1760
|
+
this.handleMediaTrackRemovedFrame(msg);
|
|
1761
|
+
break;
|
|
1762
|
+
case "media_state":
|
|
1763
|
+
this.handleMediaStateFrame(msg);
|
|
1764
|
+
break;
|
|
1765
|
+
case "media_device":
|
|
1766
|
+
this.handleMediaDeviceFrame(msg);
|
|
1767
|
+
break;
|
|
1768
|
+
case "media_result":
|
|
1769
|
+
this.handleMediaResult(msg);
|
|
1770
|
+
break;
|
|
1771
|
+
case "media_error":
|
|
1772
|
+
this.handleMediaError(msg);
|
|
1773
|
+
break;
|
|
1774
|
+
case "admin_result":
|
|
1775
|
+
this.handleAdminResult(msg);
|
|
1776
|
+
break;
|
|
1777
|
+
case "admin_error":
|
|
1778
|
+
this.handleAdminError(msg);
|
|
1779
|
+
break;
|
|
1780
|
+
case "kicked":
|
|
1781
|
+
this.handleKicked();
|
|
1782
|
+
break;
|
|
1783
|
+
case "error":
|
|
1784
|
+
this.handleError(msg);
|
|
1785
|
+
break;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
handleSync(msg) {
|
|
1789
|
+
this._sharedState = msg.sharedState;
|
|
1790
|
+
this._sharedVersion = msg.sharedVersion;
|
|
1791
|
+
this._playerState = msg.playerState;
|
|
1792
|
+
this._playerVersion = msg.playerVersion;
|
|
1793
|
+
const reconnectInfo = this.reconnectInfo;
|
|
1794
|
+
this.reconnectInfo = null;
|
|
1795
|
+
this.setConnectionState("connected");
|
|
1796
|
+
const sharedSnapshot = cloneRecord(this._sharedState);
|
|
1797
|
+
const playerSnapshot = cloneRecord(this._playerState);
|
|
1798
|
+
for (const handler of this.sharedStateHandlers) {
|
|
1799
|
+
handler(sharedSnapshot, cloneRecord(sharedSnapshot));
|
|
1800
|
+
}
|
|
1801
|
+
for (const handler of this.playerStateHandlers) {
|
|
1802
|
+
handler(playerSnapshot, cloneRecord(playerSnapshot));
|
|
1803
|
+
}
|
|
1804
|
+
if (reconnectInfo) {
|
|
1805
|
+
for (const handler of this.reconnectHandlers) {
|
|
1806
|
+
handler(reconnectInfo);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
handleSharedDelta(msg) {
|
|
1811
|
+
const delta = msg.delta;
|
|
1812
|
+
this._sharedVersion = msg.version;
|
|
1813
|
+
for (const [path, value] of Object.entries(delta)) {
|
|
1814
|
+
deepSet(this._sharedState, path, value);
|
|
1815
|
+
}
|
|
1816
|
+
const sharedSnapshot = cloneRecord(this._sharedState);
|
|
1817
|
+
const deltaSnapshot = cloneRecord(delta);
|
|
1818
|
+
for (const handler of this.sharedStateHandlers) {
|
|
1819
|
+
handler(sharedSnapshot, deltaSnapshot);
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
handlePlayerDelta(msg) {
|
|
1823
|
+
const delta = msg.delta;
|
|
1824
|
+
this._playerVersion = msg.version;
|
|
1825
|
+
for (const [path, value] of Object.entries(delta)) {
|
|
1826
|
+
deepSet(this._playerState, path, value);
|
|
1827
|
+
}
|
|
1828
|
+
const playerSnapshot = cloneRecord(this._playerState);
|
|
1829
|
+
const deltaSnapshot = cloneRecord(delta);
|
|
1830
|
+
for (const handler of this.playerStateHandlers) {
|
|
1831
|
+
handler(playerSnapshot, deltaSnapshot);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
handleActionResult(msg) {
|
|
1835
|
+
const requestId = msg.requestId;
|
|
1836
|
+
const pending = this.pendingRequests.get(requestId);
|
|
1837
|
+
if (pending) {
|
|
1838
|
+
clearTimeout(pending.timeout);
|
|
1839
|
+
this.pendingRequests.delete(requestId);
|
|
1840
|
+
pending.resolve(msg.result);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
handleActionError(msg) {
|
|
1844
|
+
const requestId = msg.requestId;
|
|
1845
|
+
const pending = this.pendingRequests.get(requestId);
|
|
1846
|
+
if (pending) {
|
|
1847
|
+
clearTimeout(pending.timeout);
|
|
1848
|
+
this.pendingRequests.delete(requestId);
|
|
1849
|
+
pending.reject(new EdgeBaseError(400, msg.message));
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
handleAuthAck(msg) {
|
|
1853
|
+
this.currentUserId = typeof msg.userId === "string" ? msg.userId : this.currentUserId;
|
|
1854
|
+
this.currentConnectionId = typeof msg.connectionId === "string" ? msg.connectionId : this.currentConnectionId;
|
|
1855
|
+
}
|
|
1856
|
+
handleServerMessage(msg) {
|
|
1857
|
+
const messageType = msg.messageType;
|
|
1858
|
+
const data = msg.data;
|
|
1859
|
+
const handlers = this.messageHandlers.get(messageType);
|
|
1860
|
+
if (handlers) {
|
|
1861
|
+
for (const handler of handlers) handler(data);
|
|
1862
|
+
}
|
|
1863
|
+
for (const handler of this.allMessageHandlers) {
|
|
1864
|
+
handler(messageType, data);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
handleSignalFrame(msg) {
|
|
1868
|
+
const event = typeof msg.event === "string" ? msg.event : "";
|
|
1869
|
+
if (!event) return;
|
|
1870
|
+
const meta = this.normalizeSignalMeta(msg.meta);
|
|
1871
|
+
const payload = msg.payload;
|
|
1872
|
+
const handlers = this.signalHandlers.get(event);
|
|
1873
|
+
if (handlers) {
|
|
1874
|
+
for (const handler of handlers) handler(payload, meta);
|
|
1875
|
+
}
|
|
1876
|
+
for (const handler of this.anySignalHandlers) {
|
|
1877
|
+
handler(event, payload, meta);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
handleSignalSent(msg) {
|
|
1881
|
+
const requestId = msg.requestId;
|
|
1882
|
+
if (!requestId) return;
|
|
1883
|
+
const pending = this.pendingSignalRequests.get(requestId);
|
|
1884
|
+
if (!pending) return;
|
|
1885
|
+
clearTimeout(pending.timeout);
|
|
1886
|
+
this.pendingSignalRequests.delete(requestId);
|
|
1887
|
+
pending.resolve();
|
|
1888
|
+
}
|
|
1889
|
+
handleSignalError(msg) {
|
|
1890
|
+
const requestId = msg.requestId;
|
|
1891
|
+
if (!requestId) return;
|
|
1892
|
+
const pending = this.pendingSignalRequests.get(requestId);
|
|
1893
|
+
if (!pending) return;
|
|
1894
|
+
clearTimeout(pending.timeout);
|
|
1895
|
+
this.pendingSignalRequests.delete(requestId);
|
|
1896
|
+
pending.reject(new EdgeBaseError(400, msg.message || "Signal failed"));
|
|
1897
|
+
}
|
|
1898
|
+
handleMembersSync(msg) {
|
|
1899
|
+
const members = this.normalizeMembers(msg.members);
|
|
1900
|
+
this._members = members;
|
|
1901
|
+
for (const member of members) {
|
|
1902
|
+
this.syncMediaMemberInfo(member);
|
|
1903
|
+
}
|
|
1904
|
+
const snapshot = cloneValue(this._members);
|
|
1905
|
+
for (const handler of this.memberSyncHandlers) {
|
|
1906
|
+
handler(snapshot);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
handleMediaSync(msg) {
|
|
1910
|
+
this._mediaMembers = this.normalizeMediaMembers(msg.members);
|
|
1911
|
+
}
|
|
1912
|
+
handleMemberJoinFrame(msg) {
|
|
1913
|
+
const member = this.normalizeMember(msg.member);
|
|
1914
|
+
if (!member) return;
|
|
1915
|
+
this.upsertMember(member);
|
|
1916
|
+
this.syncMediaMemberInfo(member);
|
|
1917
|
+
const snapshot = cloneValue(member);
|
|
1918
|
+
for (const handler of this.memberJoinHandlers) {
|
|
1919
|
+
handler(snapshot);
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
handleMemberLeaveFrame(msg) {
|
|
1923
|
+
const member = this.normalizeMember(msg.member);
|
|
1924
|
+
if (!member) return;
|
|
1925
|
+
this.removeMember(member.memberId);
|
|
1926
|
+
this.removeMediaMember(member.memberId);
|
|
1927
|
+
const reason = this.normalizeLeaveReason(msg.reason);
|
|
1928
|
+
const snapshot = cloneValue(member);
|
|
1929
|
+
for (const handler of this.memberLeaveHandlers) {
|
|
1930
|
+
handler(snapshot, reason);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
handleMemberStateFrame(msg) {
|
|
1934
|
+
const member = this.normalizeMember(msg.member);
|
|
1935
|
+
const state = this.normalizeState(msg.state);
|
|
1936
|
+
if (!member) return;
|
|
1937
|
+
member.state = state;
|
|
1938
|
+
this.upsertMember(member);
|
|
1939
|
+
this.syncMediaMemberInfo(member);
|
|
1940
|
+
const requestId = msg.requestId;
|
|
1941
|
+
if (requestId && member.memberId === this.currentUserId) {
|
|
1942
|
+
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1943
|
+
if (pending) {
|
|
1944
|
+
clearTimeout(pending.timeout);
|
|
1945
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
1946
|
+
pending.resolve();
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
const memberSnapshot = cloneValue(member);
|
|
1950
|
+
const stateSnapshot = cloneRecord(state);
|
|
1951
|
+
for (const handler of this.memberStateHandlers) {
|
|
1952
|
+
handler(memberSnapshot, stateSnapshot);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
handleMemberStateError(msg) {
|
|
1956
|
+
const requestId = msg.requestId;
|
|
1957
|
+
if (!requestId) return;
|
|
1958
|
+
const pending = this.pendingMemberStateRequests.get(requestId);
|
|
1959
|
+
if (!pending) return;
|
|
1960
|
+
clearTimeout(pending.timeout);
|
|
1961
|
+
this.pendingMemberStateRequests.delete(requestId);
|
|
1962
|
+
pending.reject(new EdgeBaseError(400, msg.message || "Member state update failed"));
|
|
1963
|
+
}
|
|
1964
|
+
handleMediaTrackFrame(msg) {
|
|
1965
|
+
const member = this.normalizeMember(msg.member);
|
|
1966
|
+
const track = this.normalizeMediaTrack(msg.track);
|
|
1967
|
+
if (!member || !track) return;
|
|
1968
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1969
|
+
this.upsertMediaTrack(mediaMember, track);
|
|
1970
|
+
this.mergeMediaState(mediaMember, track.kind, {
|
|
1971
|
+
published: true,
|
|
1972
|
+
muted: track.muted,
|
|
1973
|
+
trackId: track.trackId,
|
|
1974
|
+
deviceId: track.deviceId,
|
|
1975
|
+
publishedAt: track.publishedAt,
|
|
1976
|
+
adminDisabled: track.adminDisabled
|
|
1977
|
+
});
|
|
1978
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1979
|
+
const trackSnapshot = cloneValue(track);
|
|
1980
|
+
for (const handler of this.mediaTrackHandlers) {
|
|
1981
|
+
handler(trackSnapshot, memberSnapshot);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
handleMediaTrackRemovedFrame(msg) {
|
|
1985
|
+
const member = this.normalizeMember(msg.member);
|
|
1986
|
+
const track = this.normalizeMediaTrack(msg.track);
|
|
1987
|
+
if (!member || !track) return;
|
|
1988
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
1989
|
+
this.removeMediaTrack(mediaMember, track);
|
|
1990
|
+
mediaMember.state = {
|
|
1991
|
+
...mediaMember.state,
|
|
1992
|
+
[track.kind]: {
|
|
1993
|
+
published: false,
|
|
1994
|
+
muted: false,
|
|
1995
|
+
adminDisabled: false
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
1999
|
+
const trackSnapshot = cloneValue(track);
|
|
2000
|
+
for (const handler of this.mediaTrackRemovedHandlers) {
|
|
2001
|
+
handler(trackSnapshot, memberSnapshot);
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
handleMediaStateFrame(msg) {
|
|
2005
|
+
const member = this.normalizeMember(msg.member);
|
|
2006
|
+
if (!member) return;
|
|
2007
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
2008
|
+
mediaMember.state = this.normalizeMediaState(msg.state);
|
|
2009
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
2010
|
+
const stateSnapshot = cloneValue(mediaMember.state);
|
|
2011
|
+
for (const handler of this.mediaStateHandlers) {
|
|
2012
|
+
handler(memberSnapshot, stateSnapshot);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
handleMediaDeviceFrame(msg) {
|
|
2016
|
+
const member = this.normalizeMember(msg.member);
|
|
2017
|
+
const kind = this.normalizeMediaKind(msg.kind);
|
|
2018
|
+
const deviceId = typeof msg.deviceId === "string" ? msg.deviceId : "";
|
|
2019
|
+
if (!member || !kind || !deviceId) return;
|
|
2020
|
+
const mediaMember = this.ensureMediaMember(member);
|
|
2021
|
+
this.mergeMediaState(mediaMember, kind, { deviceId });
|
|
2022
|
+
for (const track of mediaMember.tracks) {
|
|
2023
|
+
if (track.kind === kind) {
|
|
2024
|
+
track.deviceId = deviceId;
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
const memberSnapshot = cloneValue(mediaMember.member);
|
|
2028
|
+
const change = { kind, deviceId };
|
|
2029
|
+
for (const handler of this.mediaDeviceHandlers) {
|
|
2030
|
+
handler(memberSnapshot, change);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
handleMediaResult(msg) {
|
|
2034
|
+
const requestId = msg.requestId;
|
|
2035
|
+
if (!requestId) return;
|
|
2036
|
+
const pending = this.pendingMediaRequests.get(requestId);
|
|
2037
|
+
if (!pending) return;
|
|
2038
|
+
clearTimeout(pending.timeout);
|
|
2039
|
+
this.pendingMediaRequests.delete(requestId);
|
|
2040
|
+
pending.resolve();
|
|
2041
|
+
}
|
|
2042
|
+
handleMediaError(msg) {
|
|
2043
|
+
const requestId = msg.requestId;
|
|
2044
|
+
if (!requestId) return;
|
|
2045
|
+
const pending = this.pendingMediaRequests.get(requestId);
|
|
2046
|
+
if (!pending) return;
|
|
2047
|
+
clearTimeout(pending.timeout);
|
|
2048
|
+
this.pendingMediaRequests.delete(requestId);
|
|
2049
|
+
pending.reject(new EdgeBaseError(400, msg.message || "Media operation failed"));
|
|
2050
|
+
}
|
|
2051
|
+
handleAdminResult(msg) {
|
|
2052
|
+
const requestId = msg.requestId;
|
|
2053
|
+
if (!requestId) return;
|
|
2054
|
+
const pending = this.pendingAdminRequests.get(requestId);
|
|
2055
|
+
if (!pending) return;
|
|
2056
|
+
clearTimeout(pending.timeout);
|
|
2057
|
+
this.pendingAdminRequests.delete(requestId);
|
|
2058
|
+
pending.resolve();
|
|
2059
|
+
}
|
|
2060
|
+
handleAdminError(msg) {
|
|
2061
|
+
const requestId = msg.requestId;
|
|
2062
|
+
if (!requestId) return;
|
|
2063
|
+
const pending = this.pendingAdminRequests.get(requestId);
|
|
2064
|
+
if (!pending) return;
|
|
2065
|
+
clearTimeout(pending.timeout);
|
|
2066
|
+
this.pendingAdminRequests.delete(requestId);
|
|
2067
|
+
pending.reject(new EdgeBaseError(400, msg.message || "Admin operation failed"));
|
|
2068
|
+
}
|
|
2069
|
+
handleKicked() {
|
|
2070
|
+
for (const handler of this.kickedHandlers) handler();
|
|
2071
|
+
this.intentionallyLeft = true;
|
|
2072
|
+
this.reconnectInfo = null;
|
|
2073
|
+
this.setConnectionState("kicked");
|
|
2074
|
+
}
|
|
2075
|
+
handleError(msg) {
|
|
2076
|
+
for (const handler of this.errorHandlers) {
|
|
2077
|
+
handler({ code: msg.code, message: msg.message });
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
refreshAuth() {
|
|
2081
|
+
const token = this.tokenManager.currentAccessToken;
|
|
2082
|
+
if (!token || !this.ws || !this.connected) return;
|
|
2083
|
+
this.sendRaw({ type: "auth", token });
|
|
2084
|
+
}
|
|
2085
|
+
handleAuthStateChange(user) {
|
|
2086
|
+
if (user) {
|
|
2087
|
+
if (this.ws && this.connected && this.authenticated) {
|
|
2088
|
+
this.refreshAuth();
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
this.waitingForAuth = false;
|
|
2092
|
+
if (this.joinRequested && !this.connectingPromise && !isSocketOpenOrConnecting(this.ws)) {
|
|
2093
|
+
this.reconnectAttempts = 0;
|
|
2094
|
+
this.ensureConnection().catch(() => {
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
this.waitingForAuth = this.joinRequested;
|
|
2100
|
+
this.reconnectInfo = null;
|
|
2101
|
+
this.setConnectionState("auth_lost");
|
|
2102
|
+
if (this.ws) {
|
|
2103
|
+
const socket = this.ws;
|
|
2104
|
+
this.sendRaw({ type: "leave" });
|
|
2105
|
+
this.stopHeartbeat();
|
|
2106
|
+
this.ws = null;
|
|
2107
|
+
this.connected = false;
|
|
2108
|
+
this.authenticated = false;
|
|
2109
|
+
this.joined = false;
|
|
2110
|
+
this._mediaMembers = [];
|
|
2111
|
+
this.currentUserId = null;
|
|
2112
|
+
this.currentConnectionId = null;
|
|
2113
|
+
try {
|
|
2114
|
+
closeSocketAfterLeave(socket, "Signed out");
|
|
2115
|
+
} catch {
|
|
2116
|
+
}
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
this.connected = false;
|
|
2120
|
+
this.authenticated = false;
|
|
2121
|
+
this.joined = false;
|
|
2122
|
+
this._mediaMembers = [];
|
|
2123
|
+
}
|
|
2124
|
+
handleAuthenticationFailure(error) {
|
|
2125
|
+
const authError = error instanceof EdgeBaseError ? error : new EdgeBaseError(500, "Room authentication failed.");
|
|
2126
|
+
this.waitingForAuth = authError.code === 401 && this.joinRequested;
|
|
2127
|
+
this.stopHeartbeat();
|
|
2128
|
+
this.connected = false;
|
|
2129
|
+
this.authenticated = false;
|
|
2130
|
+
this.joined = false;
|
|
2131
|
+
this.connectingPromise = null;
|
|
2132
|
+
if (this.ws) {
|
|
2133
|
+
const socket = this.ws;
|
|
2134
|
+
this.ws = null;
|
|
2135
|
+
try {
|
|
2136
|
+
socket.close(4001, authError.message);
|
|
2137
|
+
} catch {
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
normalizeMembers(value) {
|
|
2142
|
+
if (!Array.isArray(value)) {
|
|
2143
|
+
return [];
|
|
2144
|
+
}
|
|
2145
|
+
return value.map((member) => this.normalizeMember(member)).filter((member) => !!member);
|
|
2146
|
+
}
|
|
2147
|
+
normalizeMediaMembers(value) {
|
|
2148
|
+
if (!Array.isArray(value)) {
|
|
2149
|
+
return [];
|
|
2150
|
+
}
|
|
2151
|
+
return value.map((member) => this.normalizeMediaMember(member)).filter((member) => !!member);
|
|
2152
|
+
}
|
|
2153
|
+
normalizeMember(value) {
|
|
2154
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
const member = value;
|
|
2158
|
+
if (typeof member.memberId !== "string" || typeof member.userId !== "string") {
|
|
2159
|
+
return null;
|
|
2160
|
+
}
|
|
2161
|
+
return {
|
|
2162
|
+
memberId: member.memberId,
|
|
2163
|
+
userId: member.userId,
|
|
2164
|
+
connectionId: typeof member.connectionId === "string" ? member.connectionId : void 0,
|
|
2165
|
+
connectionCount: typeof member.connectionCount === "number" ? member.connectionCount : void 0,
|
|
2166
|
+
role: typeof member.role === "string" ? member.role : void 0,
|
|
2167
|
+
state: this.normalizeState(member.state)
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
normalizeState(value) {
|
|
2171
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2172
|
+
return {};
|
|
2173
|
+
}
|
|
2174
|
+
return cloneRecord(value);
|
|
2175
|
+
}
|
|
2176
|
+
normalizeMediaMember(value) {
|
|
2177
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2178
|
+
return null;
|
|
2179
|
+
}
|
|
2180
|
+
const entry = value;
|
|
2181
|
+
const member = this.normalizeMember(entry.member);
|
|
2182
|
+
if (!member) {
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
return {
|
|
2186
|
+
member,
|
|
2187
|
+
state: this.normalizeMediaState(entry.state),
|
|
2188
|
+
tracks: this.normalizeMediaTracks(entry.tracks)
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
normalizeMediaState(value) {
|
|
2192
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2193
|
+
return {};
|
|
2194
|
+
}
|
|
2195
|
+
const state = value;
|
|
2196
|
+
return {
|
|
2197
|
+
audio: this.normalizeMediaKindState(state.audio),
|
|
2198
|
+
video: this.normalizeMediaKindState(state.video),
|
|
2199
|
+
screen: this.normalizeMediaKindState(state.screen)
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
normalizeMediaKindState(value) {
|
|
2203
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2204
|
+
return void 0;
|
|
2205
|
+
}
|
|
2206
|
+
const state = value;
|
|
2207
|
+
return {
|
|
2208
|
+
published: state.published === true,
|
|
2209
|
+
muted: state.muted === true,
|
|
2210
|
+
trackId: typeof state.trackId === "string" ? state.trackId : void 0,
|
|
2211
|
+
deviceId: typeof state.deviceId === "string" ? state.deviceId : void 0,
|
|
2212
|
+
publishedAt: typeof state.publishedAt === "number" ? state.publishedAt : void 0,
|
|
2213
|
+
adminDisabled: state.adminDisabled === true
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
normalizeMediaTracks(value) {
|
|
2217
|
+
if (!Array.isArray(value)) {
|
|
2218
|
+
return [];
|
|
2219
|
+
}
|
|
2220
|
+
return value.map((track) => this.normalizeMediaTrack(track)).filter((track) => !!track);
|
|
2221
|
+
}
|
|
2222
|
+
normalizeMediaTrack(value) {
|
|
2223
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2224
|
+
return null;
|
|
2225
|
+
}
|
|
2226
|
+
const track = value;
|
|
2227
|
+
const kind = this.normalizeMediaKind(track.kind);
|
|
2228
|
+
if (!kind) {
|
|
2229
|
+
return null;
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
kind,
|
|
2233
|
+
trackId: typeof track.trackId === "string" ? track.trackId : void 0,
|
|
2234
|
+
deviceId: typeof track.deviceId === "string" ? track.deviceId : void 0,
|
|
2235
|
+
muted: track.muted === true,
|
|
2236
|
+
publishedAt: typeof track.publishedAt === "number" ? track.publishedAt : void 0,
|
|
2237
|
+
adminDisabled: track.adminDisabled === true
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
normalizeMediaKind(value) {
|
|
2241
|
+
switch (value) {
|
|
2242
|
+
case "audio":
|
|
2243
|
+
case "video":
|
|
2244
|
+
case "screen":
|
|
2245
|
+
return value;
|
|
2246
|
+
default:
|
|
2247
|
+
return null;
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
normalizeSignalMeta(value) {
|
|
2251
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2252
|
+
return {};
|
|
2253
|
+
}
|
|
2254
|
+
const meta = value;
|
|
2255
|
+
return {
|
|
2256
|
+
memberId: typeof meta.memberId === "string" || meta.memberId === null ? meta.memberId : void 0,
|
|
2257
|
+
userId: typeof meta.userId === "string" || meta.userId === null ? meta.userId : void 0,
|
|
2258
|
+
connectionId: typeof meta.connectionId === "string" || meta.connectionId === null ? meta.connectionId : void 0,
|
|
2259
|
+
sentAt: typeof meta.sentAt === "number" ? meta.sentAt : void 0,
|
|
2260
|
+
serverSent: meta.serverSent === true
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
normalizeLeaveReason(value) {
|
|
2264
|
+
switch (value) {
|
|
2265
|
+
case "leave":
|
|
2266
|
+
case "timeout":
|
|
2267
|
+
case "kicked":
|
|
2268
|
+
return value;
|
|
2269
|
+
default:
|
|
2270
|
+
return "leave";
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
upsertMember(member) {
|
|
2274
|
+
const index = this._members.findIndex((entry) => entry.memberId === member.memberId);
|
|
2275
|
+
if (index >= 0) {
|
|
2276
|
+
this._members[index] = cloneValue(member);
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
this._members.push(cloneValue(member));
|
|
2280
|
+
}
|
|
2281
|
+
removeMember(memberId) {
|
|
2282
|
+
this._members = this._members.filter((member) => member.memberId !== memberId);
|
|
2283
|
+
}
|
|
2284
|
+
syncMediaMemberInfo(member) {
|
|
2285
|
+
const mediaMember = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
2286
|
+
if (!mediaMember) {
|
|
2287
|
+
return;
|
|
2288
|
+
}
|
|
2289
|
+
mediaMember.member = cloneValue(member);
|
|
2290
|
+
}
|
|
2291
|
+
ensureMediaMember(member) {
|
|
2292
|
+
const existing = this._mediaMembers.find((entry) => entry.member.memberId === member.memberId);
|
|
2293
|
+
if (existing) {
|
|
2294
|
+
existing.member = cloneValue(member);
|
|
2295
|
+
return existing;
|
|
2296
|
+
}
|
|
2297
|
+
const created = {
|
|
2298
|
+
member: cloneValue(member),
|
|
2299
|
+
state: {},
|
|
2300
|
+
tracks: []
|
|
2301
|
+
};
|
|
2302
|
+
this._mediaMembers.push(created);
|
|
2303
|
+
return created;
|
|
2304
|
+
}
|
|
2305
|
+
removeMediaMember(memberId) {
|
|
2306
|
+
this._mediaMembers = this._mediaMembers.filter((member) => member.member.memberId !== memberId);
|
|
2307
|
+
}
|
|
2308
|
+
upsertMediaTrack(mediaMember, track) {
|
|
2309
|
+
const index = mediaMember.tracks.findIndex(
|
|
2310
|
+
(entry) => entry.kind === track.kind && entry.trackId === track.trackId
|
|
2311
|
+
);
|
|
2312
|
+
if (index >= 0) {
|
|
2313
|
+
mediaMember.tracks[index] = cloneValue(track);
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
mediaMember.tracks = mediaMember.tracks.filter((entry) => !(entry.kind === track.kind && !track.trackId)).concat(cloneValue(track));
|
|
2317
|
+
}
|
|
2318
|
+
removeMediaTrack(mediaMember, track) {
|
|
2319
|
+
mediaMember.tracks = mediaMember.tracks.filter((entry) => {
|
|
2320
|
+
if (track.trackId) {
|
|
2321
|
+
return !(entry.kind === track.kind && entry.trackId === track.trackId);
|
|
2322
|
+
}
|
|
2323
|
+
return entry.kind !== track.kind;
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
mergeMediaState(mediaMember, kind, partial) {
|
|
2327
|
+
const next = {
|
|
2328
|
+
published: partial.published ?? mediaMember.state[kind]?.published ?? false,
|
|
2329
|
+
muted: partial.muted ?? mediaMember.state[kind]?.muted ?? false,
|
|
2330
|
+
trackId: partial.trackId ?? mediaMember.state[kind]?.trackId,
|
|
2331
|
+
deviceId: partial.deviceId ?? mediaMember.state[kind]?.deviceId,
|
|
2332
|
+
publishedAt: partial.publishedAt ?? mediaMember.state[kind]?.publishedAt,
|
|
2333
|
+
adminDisabled: partial.adminDisabled ?? mediaMember.state[kind]?.adminDisabled
|
|
2334
|
+
};
|
|
2335
|
+
mediaMember.state = {
|
|
2336
|
+
...mediaMember.state,
|
|
2337
|
+
[kind]: next
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
rejectPendingVoidRequests(pendingRequests, error) {
|
|
2341
|
+
for (const [, pending] of pendingRequests) {
|
|
2342
|
+
clearTimeout(pending.timeout);
|
|
2343
|
+
pending.reject(error);
|
|
2344
|
+
}
|
|
2345
|
+
pendingRequests.clear();
|
|
2346
|
+
}
|
|
2347
|
+
setConnectionState(next) {
|
|
2348
|
+
if (this.connectionState === next) {
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
2351
|
+
this.connectionState = next;
|
|
2352
|
+
for (const handler of this.connectionStateHandlers) {
|
|
2353
|
+
handler(next);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
// ─── Private: Helpers ───
|
|
2357
|
+
sendRaw(data) {
|
|
2358
|
+
if (this.ws && this.connected) {
|
|
2359
|
+
this.ws.send(JSON.stringify(data));
|
|
2360
|
+
return;
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
buildWsUrl() {
|
|
2364
|
+
const httpUrl = this.baseUrl.replace(/\/$/, "");
|
|
2365
|
+
const wsUrl = httpUrl.replace(/^http/, "ws");
|
|
2366
|
+
return `${wsUrl}/api/room?namespace=${encodeURIComponent(this.namespace)}&id=${encodeURIComponent(this.roomId)}`;
|
|
2367
|
+
}
|
|
2368
|
+
scheduleReconnect() {
|
|
2369
|
+
const attempt = this.reconnectAttempts + 1;
|
|
2370
|
+
const delay = this.options.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts);
|
|
2371
|
+
this.reconnectAttempts++;
|
|
2372
|
+
this.reconnectInfo = { attempt };
|
|
2373
|
+
this.setConnectionState("reconnecting");
|
|
2374
|
+
setTimeout(() => {
|
|
2375
|
+
if (this.connectingPromise || !this.joinRequested || this.waitingForAuth || isSocketOpenOrConnecting(this.ws)) {
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
this.ensureConnection().catch(() => {
|
|
2379
|
+
});
|
|
2380
|
+
}, Math.min(delay, 3e4));
|
|
2381
|
+
}
|
|
2382
|
+
startHeartbeat() {
|
|
2383
|
+
this.stopHeartbeat();
|
|
2384
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2385
|
+
if (this.ws && this.connected) {
|
|
2386
|
+
this.ws.send(JSON.stringify({ type: "ping" }));
|
|
2387
|
+
}
|
|
2388
|
+
}, 3e4);
|
|
2389
|
+
}
|
|
2390
|
+
stopHeartbeat() {
|
|
2391
|
+
if (this.heartbeatTimer) {
|
|
2392
|
+
clearInterval(this.heartbeatTimer);
|
|
2393
|
+
this.heartbeatTimer = null;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
};
|
|
2397
|
+
var PushClient = class {
|
|
2398
|
+
constructor(http, storage, core) {
|
|
2399
|
+
this.http = http;
|
|
2400
|
+
this.storage = storage;
|
|
2401
|
+
this.core = core;
|
|
2402
|
+
}
|
|
2403
|
+
tokenProvider = null;
|
|
2404
|
+
permissionProvider = null;
|
|
2405
|
+
topicProvider = null;
|
|
2406
|
+
messageListeners = [];
|
|
2407
|
+
openedAppListeners = [];
|
|
2408
|
+
/**
|
|
2409
|
+
* Set the native token provider.
|
|
2410
|
+
* Must be called before register() — typically in App.tsx or native bootstrapping.
|
|
2411
|
+
*
|
|
2412
|
+
* @example (Firebase Messaging)
|
|
2413
|
+
* client.push.setTokenProvider(async () => ({
|
|
2414
|
+
* token: await messaging().getToken(),
|
|
2415
|
+
* platform: 'android',
|
|
2416
|
+
* }));
|
|
2417
|
+
*
|
|
2418
|
+
* @example (APNs via native bridge)
|
|
2419
|
+
* client.push.setTokenProvider(async () => ({
|
|
2420
|
+
* token: nativeBridge.getAPNsToken(),
|
|
2421
|
+
* platform: 'ios',
|
|
2422
|
+
* }));
|
|
2423
|
+
*/
|
|
2424
|
+
setTokenProvider(provider) {
|
|
2425
|
+
this.tokenProvider = provider;
|
|
2426
|
+
}
|
|
2427
|
+
/**
|
|
2428
|
+
* Set the native permission provider.
|
|
2429
|
+
* Call this with your FCM / @notifee/react-native permission handler.
|
|
2430
|
+
*
|
|
2431
|
+
* @example (Firebase Messaging)
|
|
2432
|
+
* client.push.setPermissionProvider({
|
|
2433
|
+
* getPermissionStatus: async () => {
|
|
2434
|
+
* const status = await messaging().hasPermission();
|
|
2435
|
+
* if (status === messaging.AuthorizationStatus.AUTHORIZED) return 'granted';
|
|
2436
|
+
* if (status === messaging.AuthorizationStatus.PROVISIONAL) return 'provisional';
|
|
2437
|
+
* if (status === messaging.AuthorizationStatus.DENIED) return 'denied';
|
|
2438
|
+
* return 'not-determined';
|
|
2439
|
+
* },
|
|
2440
|
+
* requestPermission: async () => {
|
|
2441
|
+
* const status = await messaging().requestPermission();
|
|
2442
|
+
* if (status === messaging.AuthorizationStatus.AUTHORIZED) return 'granted';
|
|
2443
|
+
* if (status === messaging.AuthorizationStatus.PROVISIONAL) return 'provisional';
|
|
2444
|
+
* return 'denied';
|
|
2445
|
+
* },
|
|
2446
|
+
* });
|
|
2447
|
+
*/
|
|
2448
|
+
setPermissionProvider(provider) {
|
|
2449
|
+
this.permissionProvider = provider;
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Get current push notification permission status.
|
|
2453
|
+
* Uses custom provider if set via setPermissionProvider(), otherwise uses
|
|
2454
|
+
* built-in platform defaults (PermissionsAndroid on Android, auto-grant on iOS).
|
|
2455
|
+
*/
|
|
2456
|
+
async getPermissionStatus() {
|
|
2457
|
+
if (this.permissionProvider) {
|
|
2458
|
+
return this.permissionProvider.getPermissionStatus();
|
|
2459
|
+
}
|
|
2460
|
+
return this._defaultGetPermissionStatus();
|
|
2461
|
+
}
|
|
2462
|
+
/**
|
|
2463
|
+
* Request push notification permission from the user.
|
|
2464
|
+
* Uses custom provider if set via setPermissionProvider(), otherwise uses
|
|
2465
|
+
* built-in platform defaults (PermissionsAndroid on Android, auto-grant on iOS).
|
|
2466
|
+
*/
|
|
2467
|
+
async requestPermission() {
|
|
2468
|
+
if (this.permissionProvider) {
|
|
2469
|
+
return this.permissionProvider.requestPermission();
|
|
2470
|
+
}
|
|
2471
|
+
return this._defaultRequestPermission();
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Register for push notifications.
|
|
2475
|
+
* Zero-parameter — token is acquired via setTokenProvider().
|
|
2476
|
+
* Token is cached; network request only fires if token changes.
|
|
2477
|
+
*/
|
|
2478
|
+
async register(options) {
|
|
2479
|
+
if (!this.tokenProvider) {
|
|
2480
|
+
throw new Error(
|
|
2481
|
+
"[EdgeBase] push.register(): No token provider set. Call client.push.setTokenProvider(async () => ({ token, platform })) first."
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const permStatus = await this.requestPermission();
|
|
2485
|
+
if (permStatus === "denied") return;
|
|
2486
|
+
const { token, platform } = await this.tokenProvider();
|
|
2487
|
+
const cachedToken = await this.storage.getItem(PUSH_TOKEN_CACHE_KEY);
|
|
2488
|
+
if (cachedToken === token && !options?.metadata) return;
|
|
2489
|
+
const deviceId = await this.getOrCreateDeviceId();
|
|
2490
|
+
if (this.core) {
|
|
2491
|
+
await this.core.pushRegister({
|
|
2492
|
+
deviceId,
|
|
2493
|
+
token,
|
|
2494
|
+
platform,
|
|
2495
|
+
metadata: options?.metadata
|
|
2496
|
+
});
|
|
2497
|
+
} else {
|
|
2498
|
+
await this.http.post(ApiPaths.PUSH_REGISTER, {
|
|
2499
|
+
deviceId,
|
|
2500
|
+
token,
|
|
2501
|
+
platform,
|
|
2502
|
+
metadata: options?.metadata
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
await this.storage.setItem(PUSH_TOKEN_CACHE_KEY, token);
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Unregister the current device from push notifications.
|
|
2509
|
+
* Called automatically on signOut.
|
|
2510
|
+
*/
|
|
2511
|
+
async unregister(deviceId) {
|
|
2512
|
+
const id = deviceId ?? await this.getOrCreateDeviceId();
|
|
2513
|
+
if (this.core) {
|
|
2514
|
+
await this.core.pushUnregister({ deviceId: id });
|
|
2515
|
+
} else {
|
|
2516
|
+
await this.http.post(ApiPaths.PUSH_UNREGISTER, { deviceId: id });
|
|
2517
|
+
}
|
|
2518
|
+
await this.storage.removeItem(PUSH_TOKEN_CACHE_KEY);
|
|
2519
|
+
}
|
|
2520
|
+
/** Listen for push messages while app is in foreground. */
|
|
2521
|
+
onMessage(callback) {
|
|
2522
|
+
this.messageListeners.push(callback);
|
|
2523
|
+
return () => {
|
|
2524
|
+
this.messageListeners = this.messageListeners.filter((h) => h !== callback);
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
/** Listen for notification taps that opened the app. */
|
|
2528
|
+
onMessageOpenedApp(callback) {
|
|
2529
|
+
this.openedAppListeners.push(callback);
|
|
2530
|
+
return () => {
|
|
2531
|
+
this.openedAppListeners = this.openedAppListeners.filter((h) => h !== callback);
|
|
2532
|
+
};
|
|
2533
|
+
}
|
|
2534
|
+
/**
|
|
2535
|
+
* Dispatch a foreground message to all onMessage listeners.
|
|
2536
|
+
* Call this from your native FCM/APNs foreground handler.
|
|
2537
|
+
*
|
|
2538
|
+
* @example (Firebase Messaging)
|
|
2539
|
+
* messaging().onMessage(async (remoteMessage) => {
|
|
2540
|
+
* client.push._dispatchForegroundMessage({
|
|
2541
|
+
* title: remoteMessage.notification?.title,
|
|
2542
|
+
* body: remoteMessage.notification?.body,
|
|
2543
|
+
* data: remoteMessage.data,
|
|
2544
|
+
* });
|
|
2545
|
+
* });
|
|
2546
|
+
*/
|
|
2547
|
+
_dispatchForegroundMessage(message) {
|
|
2548
|
+
for (const handler of this.messageListeners) {
|
|
2549
|
+
handler(message);
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
/**
|
|
2553
|
+
* Dispatch an opened-app notification to all onMessageOpenedApp listeners.
|
|
2554
|
+
* Call this from your notification tap handler.
|
|
2555
|
+
*/
|
|
2556
|
+
_dispatchOpenedAppMessage(message) {
|
|
2557
|
+
for (const handler of this.openedAppListeners) {
|
|
2558
|
+
handler(message);
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Set topic subscription provider.
|
|
2563
|
+
* Inject your Firebase RN SDK's topic subscription handlers.
|
|
2564
|
+
*
|
|
2565
|
+
* @example
|
|
2566
|
+
* client.push.setTopicProvider({
|
|
2567
|
+
* subscribeTopic: (topic) => messaging().subscribeToTopic(topic),
|
|
2568
|
+
* unsubscribeTopic: (topic) => messaging().unsubscribeFromTopic(topic),
|
|
2569
|
+
* });
|
|
2570
|
+
*/
|
|
2571
|
+
setTopicProvider(provider) {
|
|
2572
|
+
this.topicProvider = provider;
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Subscribe to a push notification topic.
|
|
2576
|
+
* Delegates to the topic provider set via setTopicProvider().
|
|
2577
|
+
*/
|
|
2578
|
+
async subscribeTopic(topic) {
|
|
2579
|
+
if (!this.topicProvider) {
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
"[EdgeBase] push.subscribeTopic(): No topic provider set. Call client.push.setTopicProvider({ subscribeTopic, unsubscribeTopic }) first."
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
return this.topicProvider.subscribeTopic(topic);
|
|
2585
|
+
}
|
|
2586
|
+
/**
|
|
2587
|
+
* Unsubscribe from a push notification topic.
|
|
2588
|
+
* Delegates to the topic provider set via setTopicProvider().
|
|
2589
|
+
*/
|
|
2590
|
+
async unsubscribeTopic(topic) {
|
|
2591
|
+
if (!this.topicProvider) {
|
|
2592
|
+
throw new Error(
|
|
2593
|
+
"[EdgeBase] push.unsubscribeTopic(): No topic provider set. Call client.push.setTopicProvider({ subscribeTopic, unsubscribeTopic }) first."
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
return this.topicProvider.unsubscribeTopic(topic);
|
|
2597
|
+
}
|
|
2598
|
+
// ─── Built-in permission defaults ───
|
|
2599
|
+
//
|
|
2600
|
+
// Used when no custom permissionProvider is set.
|
|
2601
|
+
// Android: uses react-native PermissionsAndroid for POST_NOTIFICATIONS (API 33+).
|
|
2602
|
+
// iOS: returns 'granted' — Firebase messaging handles iOS permission internally
|
|
2603
|
+
// when getToken() is called. Use setPermissionProvider() for explicit control.
|
|
2604
|
+
async _defaultGetPermissionStatus() {
|
|
2605
|
+
try {
|
|
2606
|
+
const { Platform, PermissionsAndroid } = __require("react-native");
|
|
2607
|
+
if (Platform.OS === "android") {
|
|
2608
|
+
if (Platform.Version < 33) return "granted";
|
|
2609
|
+
const granted = await PermissionsAndroid.check(
|
|
2610
|
+
"android.permission.POST_NOTIFICATIONS"
|
|
2611
|
+
);
|
|
2612
|
+
return granted ? "granted" : "not-determined";
|
|
2613
|
+
}
|
|
2614
|
+
return "granted";
|
|
2615
|
+
} catch {
|
|
2616
|
+
return "not-determined";
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
async _defaultRequestPermission() {
|
|
2620
|
+
try {
|
|
2621
|
+
const { Platform, PermissionsAndroid } = __require("react-native");
|
|
2622
|
+
if (Platform.OS === "android") {
|
|
2623
|
+
if (Platform.Version < 33) return "granted";
|
|
2624
|
+
const result = await PermissionsAndroid.request(
|
|
2625
|
+
"android.permission.POST_NOTIFICATIONS"
|
|
2626
|
+
);
|
|
2627
|
+
return result === PermissionsAndroid.RESULTS.GRANTED ? "granted" : "denied";
|
|
2628
|
+
}
|
|
2629
|
+
return "granted";
|
|
2630
|
+
} catch {
|
|
2631
|
+
return "not-determined";
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
// ─── Private helpers ───
|
|
2635
|
+
async getOrCreateDeviceId() {
|
|
2636
|
+
const existing = await this.storage.getItem(PUSH_DEVICE_ID_KEY);
|
|
2637
|
+
if (existing) return existing;
|
|
2638
|
+
const id = `rn-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2639
|
+
await this.storage.setItem(PUSH_DEVICE_ID_KEY, id);
|
|
2640
|
+
return id;
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
|
|
2644
|
+
// src/lifecycle.ts
|
|
2645
|
+
var LifecycleManager = class {
|
|
2646
|
+
constructor(tokenManager, databaseLive, appState, doRefresh) {
|
|
2647
|
+
this.tokenManager = tokenManager;
|
|
2648
|
+
this.databaseLive = databaseLive;
|
|
2649
|
+
this.appState = appState;
|
|
2650
|
+
this.doRefresh = doRefresh;
|
|
2651
|
+
this.previousState = appState.currentState;
|
|
2652
|
+
}
|
|
2653
|
+
subscription = null;
|
|
2654
|
+
previousState;
|
|
2655
|
+
/** Start listening to AppState changes. */
|
|
2656
|
+
start() {
|
|
2657
|
+
if (this.subscription) return;
|
|
2658
|
+
this.subscription = this.appState.addEventListener("change", this.handleStateChange);
|
|
2659
|
+
}
|
|
2660
|
+
/** Stop listening to AppState changes and clean up. */
|
|
2661
|
+
stop() {
|
|
2662
|
+
this.subscription?.remove();
|
|
2663
|
+
this.subscription = null;
|
|
2664
|
+
}
|
|
2665
|
+
handleStateChange = (nextState) => {
|
|
2666
|
+
const prev = this.previousState;
|
|
2667
|
+
this.previousState = nextState;
|
|
2668
|
+
if (prev === nextState) return;
|
|
2669
|
+
if (nextState === "active") {
|
|
2670
|
+
this.onForeground();
|
|
2671
|
+
} else if (nextState === "background" || nextState === "inactive") {
|
|
2672
|
+
this.onBackground();
|
|
2673
|
+
}
|
|
2674
|
+
};
|
|
2675
|
+
onForeground() {
|
|
2676
|
+
if (this.doRefresh) {
|
|
2677
|
+
void this.tokenManager.getAccessToken(this.doRefresh).catch(() => {
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
if (this.databaseLive?.reconnect) {
|
|
2681
|
+
this.databaseLive.reconnect();
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
onBackground() {
|
|
2685
|
+
this.databaseLive?.disconnect();
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
function useLifecycle({
|
|
2689
|
+
tokenManager,
|
|
2690
|
+
databaseLive,
|
|
2691
|
+
appState,
|
|
2692
|
+
doRefresh
|
|
2693
|
+
}) {
|
|
2694
|
+
const { useEffect: useEffect2 } = __require("react");
|
|
2695
|
+
useEffect2(() => {
|
|
2696
|
+
const manager = new LifecycleManager(tokenManager, databaseLive, appState, doRefresh);
|
|
2697
|
+
manager.start();
|
|
2698
|
+
return () => manager.stop();
|
|
2699
|
+
}, [tokenManager, databaseLive, appState, doRefresh]);
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// src/analytics.ts
|
|
2703
|
+
var ClientAnalytics = class {
|
|
2704
|
+
constructor(core) {
|
|
2705
|
+
this.core = core;
|
|
2706
|
+
}
|
|
2707
|
+
async track(name, properties) {
|
|
2708
|
+
await this.trackBatch([{ name, properties }]);
|
|
2709
|
+
}
|
|
2710
|
+
async trackBatch(events) {
|
|
2711
|
+
if (events.length === 0) return;
|
|
2712
|
+
await this.core.trackEvents({
|
|
2713
|
+
events: events.map((event) => ({
|
|
2714
|
+
name: event.name,
|
|
2715
|
+
properties: event.properties,
|
|
2716
|
+
timestamp: event.timestamp ?? Date.now()
|
|
2717
|
+
}))
|
|
2718
|
+
});
|
|
2719
|
+
}
|
|
2720
|
+
async flush() {
|
|
2721
|
+
}
|
|
2722
|
+
destroy() {
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
// src/match-filter.ts
|
|
2727
|
+
function matchesFilter(data, filters) {
|
|
2728
|
+
const entries = Array.isArray(filters) && filters.length > 0 && Array.isArray(filters[0]) ? parseTupleFilters(filters) : parseFilters(filters);
|
|
2729
|
+
return entries.every(
|
|
2730
|
+
({ field, operator, value }) => evaluateCondition(data[field], operator, value)
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
function parseFilters(filters) {
|
|
2734
|
+
const entries = [];
|
|
2735
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
2736
|
+
const dotIdx = key.lastIndexOf(".");
|
|
2737
|
+
if (dotIdx > 0) {
|
|
2738
|
+
const possibleOp = key.slice(dotIdx + 1);
|
|
2739
|
+
if (isValidOperator(possibleOp)) {
|
|
2740
|
+
entries.push({
|
|
2741
|
+
field: key.slice(0, dotIdx),
|
|
2742
|
+
operator: possibleOp,
|
|
2743
|
+
value
|
|
2744
|
+
});
|
|
2745
|
+
continue;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
entries.push({ field: key, operator: "==", value });
|
|
2749
|
+
}
|
|
2750
|
+
return entries;
|
|
2751
|
+
}
|
|
2752
|
+
function parseTupleFilters(tuples) {
|
|
2753
|
+
return tuples.map(([field, operator, value]) => ({ field, operator, value }));
|
|
2754
|
+
}
|
|
2755
|
+
function isValidOperator(op) {
|
|
2756
|
+
return ["==", "!=", "<", ">", "<=", ">=", "contains", "contains-any", "in", "not in"].includes(op);
|
|
2757
|
+
}
|
|
2758
|
+
function evaluateCondition(fieldValue, operator, expected) {
|
|
2759
|
+
switch (operator) {
|
|
2760
|
+
case "==":
|
|
2761
|
+
return fieldValue === expected;
|
|
2762
|
+
case "!=":
|
|
2763
|
+
return fieldValue !== expected;
|
|
2764
|
+
case "<":
|
|
2765
|
+
return fieldValue < expected;
|
|
2766
|
+
case ">":
|
|
2767
|
+
return fieldValue > expected;
|
|
2768
|
+
case "<=":
|
|
2769
|
+
return fieldValue <= expected;
|
|
2770
|
+
case ">=":
|
|
2771
|
+
return fieldValue >= expected;
|
|
2772
|
+
case "contains":
|
|
2773
|
+
if (typeof fieldValue === "string") return fieldValue.includes(expected);
|
|
2774
|
+
if (Array.isArray(fieldValue)) return fieldValue.includes(expected);
|
|
2775
|
+
return false;
|
|
2776
|
+
case "contains-any":
|
|
2777
|
+
if (!Array.isArray(fieldValue) || !Array.isArray(expected)) return false;
|
|
2778
|
+
return expected.some((value) => fieldValue.includes(value));
|
|
2779
|
+
case "in":
|
|
2780
|
+
if (Array.isArray(expected)) return expected.includes(fieldValue);
|
|
2781
|
+
return false;
|
|
2782
|
+
case "not in":
|
|
2783
|
+
if (Array.isArray(expected)) return !expected.includes(fieldValue);
|
|
2784
|
+
return true;
|
|
2785
|
+
default:
|
|
2786
|
+
return false;
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// src/client.ts
|
|
2791
|
+
var ClientEdgeBase = class {
|
|
2792
|
+
auth;
|
|
2793
|
+
storage;
|
|
2794
|
+
push;
|
|
2795
|
+
functions;
|
|
2796
|
+
analytics;
|
|
2797
|
+
databaseLive;
|
|
2798
|
+
/** @internal exposed for advanced use (e.g. setDatabaseLive, testing) */
|
|
2799
|
+
_tokenManager;
|
|
2800
|
+
/** @internal */
|
|
2801
|
+
_httpClient;
|
|
2802
|
+
lifecycleManager = null;
|
|
2803
|
+
contextManager;
|
|
2804
|
+
baseUrl;
|
|
2805
|
+
core;
|
|
2806
|
+
constructor(url, options) {
|
|
2807
|
+
this.baseUrl = url.replace(/\/$/, "");
|
|
2808
|
+
this._tokenManager = new TokenManager(this.baseUrl, options.storage);
|
|
2809
|
+
this.contextManager = new ContextManager();
|
|
2810
|
+
this._httpClient = new HttpClient({
|
|
2811
|
+
baseUrl: this.baseUrl,
|
|
2812
|
+
tokenManager: this._tokenManager,
|
|
2813
|
+
contextManager: this.contextManager
|
|
2814
|
+
});
|
|
2815
|
+
this.core = new DefaultDbApi(new HttpClientAdapter(this._httpClient));
|
|
2816
|
+
const corePublic = new DefaultDbApi(new PublicHttpClientAdapter(this._httpClient));
|
|
2817
|
+
this.auth = new AuthClient(this._httpClient, this._tokenManager, this.core, corePublic, options.linking);
|
|
2818
|
+
this.databaseLive = new DatabaseLiveClient(
|
|
2819
|
+
this.baseUrl,
|
|
2820
|
+
this._tokenManager,
|
|
2821
|
+
options.databaseLive,
|
|
2822
|
+
this.contextManager
|
|
2823
|
+
);
|
|
2824
|
+
this.storage = new StorageClient(this._httpClient, this.core);
|
|
2825
|
+
this.push = new PushClient(this._httpClient, options.storage, this.core);
|
|
2826
|
+
this.functions = new FunctionsClient(this._httpClient);
|
|
2827
|
+
this.analytics = new ClientAnalytics(this.core);
|
|
2828
|
+
const originalSignOut = this.auth.signOut.bind(this.auth);
|
|
2829
|
+
const pushRef = this.push;
|
|
2830
|
+
const storageRef = options.storage;
|
|
2831
|
+
this.auth.signOut = async function() {
|
|
2832
|
+
try {
|
|
2833
|
+
const cached = await storageRef.getItem("edgebase:push-token-cache");
|
|
2834
|
+
if (cached) await pushRef.unregister();
|
|
2835
|
+
} catch {
|
|
2836
|
+
}
|
|
2837
|
+
return originalSignOut();
|
|
2838
|
+
};
|
|
2839
|
+
if (options.appState) {
|
|
2840
|
+
const doRefresh = async (refreshToken) => {
|
|
2841
|
+
return this._httpClient.postPublic(
|
|
2842
|
+
ApiPaths.AUTH_REFRESH,
|
|
2843
|
+
{ refreshToken }
|
|
2844
|
+
);
|
|
2845
|
+
};
|
|
2846
|
+
this.lifecycleManager = new LifecycleManager(
|
|
2847
|
+
this._tokenManager,
|
|
2848
|
+
{
|
|
2849
|
+
disconnect: () => {
|
|
2850
|
+
this.databaseLive.disconnect();
|
|
2851
|
+
},
|
|
2852
|
+
reconnect: () => {
|
|
2853
|
+
this.databaseLive.reconnect?.();
|
|
2854
|
+
}
|
|
2855
|
+
},
|
|
2856
|
+
options.appState,
|
|
2857
|
+
doRefresh
|
|
2858
|
+
);
|
|
2859
|
+
this.lifecycleManager.start();
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* Select a DB block by namespace and optional instance ID (#133 §2).
|
|
2864
|
+
*
|
|
2865
|
+
* @example
|
|
2866
|
+
* const posts = await client.db('shared').table('posts').where('status', '==', 'published').get();
|
|
2867
|
+
* client.db('shared').table('posts').onSnapshot((change) => { ... });
|
|
2868
|
+
*/
|
|
2869
|
+
db(namespace, instanceId) {
|
|
2870
|
+
return new DbRef(this.core, namespace, instanceId, this.databaseLive, matchesFilter);
|
|
2871
|
+
}
|
|
2872
|
+
/**
|
|
2873
|
+
* Get a Room client for ephemeral stateful real-time sessions.
|
|
2874
|
+
*
|
|
2875
|
+
* @param namespace - The room namespace (e.g. 'game', 'chat')
|
|
2876
|
+
* @param roomId - The room instance ID within the namespace
|
|
2877
|
+
* @param options - Connection options
|
|
2878
|
+
*
|
|
2879
|
+
* @example
|
|
2880
|
+
* const room = client.room('game', 'room-123');
|
|
2881
|
+
* await room.join();
|
|
2882
|
+
* const result = await room.send('SET_SCORE', { score: 42 });
|
|
2883
|
+
*/
|
|
2884
|
+
room(namespace, roomId, options) {
|
|
2885
|
+
return new RoomClient(this.baseUrl, namespace, roomId, this._tokenManager, options);
|
|
2886
|
+
}
|
|
2887
|
+
/** Set legacy isolateBy context state. HTTP DB routing uses db(namespace, id). */
|
|
2888
|
+
setContext(context) {
|
|
2889
|
+
this.contextManager.setContext(context);
|
|
2890
|
+
}
|
|
2891
|
+
/** Set locale for auth email i18n and Accept-Language headers. */
|
|
2892
|
+
setLocale(locale) {
|
|
2893
|
+
this._httpClient.setLocale(locale);
|
|
2894
|
+
}
|
|
2895
|
+
/** Get the currently configured locale override. */
|
|
2896
|
+
getLocale() {
|
|
2897
|
+
return this._httpClient.getLocale();
|
|
2898
|
+
}
|
|
2899
|
+
/** Get the currently configured legacy isolateBy context state. */
|
|
2900
|
+
getContext() {
|
|
2901
|
+
return this.contextManager.getContext();
|
|
2902
|
+
}
|
|
2903
|
+
/** Clean up all connections and listeners. */
|
|
2904
|
+
destroy() {
|
|
2905
|
+
this.analytics.destroy();
|
|
2906
|
+
this._tokenManager.destroy();
|
|
2907
|
+
this.lifecycleManager?.stop();
|
|
2908
|
+
this.databaseLive.disconnect();
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
function createClient(url, options) {
|
|
2912
|
+
return new ClientEdgeBase(url, options);
|
|
2913
|
+
}
|
|
2914
|
+
function buildTurnstileHtml(siteKey, action, appearance, size) {
|
|
2915
|
+
return `<!DOCTYPE html>
|
|
2916
|
+
<html>
|
|
2917
|
+
<head>
|
|
2918
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2919
|
+
<meta http-equiv="Content-Security-Policy" content="script-src 'unsafe-inline' https://challenges.cloudflare.com; style-src 'unsafe-inline';">
|
|
2920
|
+
<style>
|
|
2921
|
+
html, body { margin: 0; padding: 0; background: transparent; overflow: hidden; }
|
|
2922
|
+
#container { display: flex; align-items: center; justify-content: center; min-height: 65px; }
|
|
2923
|
+
</style>
|
|
2924
|
+
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
|
|
2925
|
+
<script>
|
|
2926
|
+
function sendToNative(data) {
|
|
2927
|
+
try {
|
|
2928
|
+
// Android/iOS via react-native-webview
|
|
2929
|
+
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
2930
|
+
window.ReactNativeWebView.postMessage(JSON.stringify(data));
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
// React Native Web / fallback
|
|
2934
|
+
window.postMessage(JSON.stringify(data), '*');
|
|
2935
|
+
} catch(e) {}
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
function onTurnstileLoad() {
|
|
2939
|
+
turnstile.render('#container', {
|
|
2940
|
+
sitekey: ${JSON.stringify(siteKey)},
|
|
2941
|
+
action: ${JSON.stringify(action)},
|
|
2942
|
+
appearance: ${JSON.stringify(appearance)},
|
|
2943
|
+
size: ${JSON.stringify(size)},
|
|
2944
|
+
callback: function(token) {
|
|
2945
|
+
sendToNative({ type: 'captcha-token', token: token });
|
|
2946
|
+
},
|
|
2947
|
+
'error-callback': function(error) {
|
|
2948
|
+
sendToNative({ type: 'captcha-error', error: String(error) });
|
|
2949
|
+
},
|
|
2950
|
+
'before-interactive-callback': function() {
|
|
2951
|
+
sendToNative({ type: 'captcha-interactive' });
|
|
2952
|
+
},
|
|
2953
|
+
'after-interactive-callback': function() {
|
|
2954
|
+
sendToNative({ type: 'captcha-done' });
|
|
2955
|
+
},
|
|
2956
|
+
'timeout-callback': function() {
|
|
2957
|
+
sendToNative({ type: 'captcha-error', error: 'timeout' });
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
// Wait for Turnstile script to load
|
|
2963
|
+
var checkInterval = setInterval(function() {
|
|
2964
|
+
if (window.turnstile) {
|
|
2965
|
+
clearInterval(checkInterval);
|
|
2966
|
+
onTurnstileLoad();
|
|
2967
|
+
}
|
|
2968
|
+
}, 100);
|
|
2969
|
+
|
|
2970
|
+
// Safety timeout \u2014 give up after 15 seconds
|
|
2971
|
+
setTimeout(function() {
|
|
2972
|
+
clearInterval(checkInterval);
|
|
2973
|
+
if (!window.turnstile) {
|
|
2974
|
+
sendToNative({ type: 'captcha-error', error: 'script_load_failed' });
|
|
2975
|
+
}
|
|
2976
|
+
}, 15000);
|
|
2977
|
+
</script>
|
|
2978
|
+
</head>
|
|
2979
|
+
<body><div id="container"></div></body>
|
|
2980
|
+
</html>`;
|
|
2981
|
+
}
|
|
2982
|
+
function TurnstileWebView({
|
|
2983
|
+
siteKey,
|
|
2984
|
+
action = "auth",
|
|
2985
|
+
onToken,
|
|
2986
|
+
onError,
|
|
2987
|
+
onInteractive,
|
|
2988
|
+
appearance = "interaction-only",
|
|
2989
|
+
size = "normal",
|
|
2990
|
+
testID,
|
|
2991
|
+
style,
|
|
2992
|
+
WebViewComponent
|
|
2993
|
+
}) {
|
|
2994
|
+
const html = buildTurnstileHtml(siteKey, action, appearance, size);
|
|
2995
|
+
const handleMessage = useCallback(
|
|
2996
|
+
(event) => {
|
|
2997
|
+
try {
|
|
2998
|
+
let raw = event.nativeEvent.data;
|
|
2999
|
+
if (typeof raw !== "string") raw = JSON.stringify(raw);
|
|
3000
|
+
const msg = JSON.parse(raw);
|
|
3001
|
+
switch (msg.type) {
|
|
3002
|
+
case "captcha-token":
|
|
3003
|
+
if (msg.token) onToken(msg.token);
|
|
3004
|
+
break;
|
|
3005
|
+
case "captcha-error":
|
|
3006
|
+
onError?.(msg.error ?? "unknown");
|
|
3007
|
+
break;
|
|
3008
|
+
case "captcha-interactive":
|
|
3009
|
+
onInteractive?.();
|
|
3010
|
+
break;
|
|
3011
|
+
default:
|
|
3012
|
+
break;
|
|
3013
|
+
}
|
|
3014
|
+
} catch {
|
|
3015
|
+
}
|
|
3016
|
+
},
|
|
3017
|
+
[onToken, onError, onInteractive]
|
|
3018
|
+
);
|
|
3019
|
+
return React.createElement(WebViewComponent, {
|
|
3020
|
+
source: { html },
|
|
3021
|
+
style: style ?? { width: 300, height: 65, backgroundColor: "transparent" },
|
|
3022
|
+
onMessage: handleMessage,
|
|
3023
|
+
testID,
|
|
3024
|
+
javaScriptEnabled: true,
|
|
3025
|
+
originWhitelist: ["*"],
|
|
3026
|
+
scrollEnabled: false,
|
|
3027
|
+
showsHorizontalScrollIndicator: false,
|
|
3028
|
+
showsVerticalScrollIndicator: false
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
var cachedSiteKeys = /* @__PURE__ */ new Map();
|
|
3032
|
+
var siteKeyFetchPromises = /* @__PURE__ */ new Map();
|
|
3033
|
+
async function fetchSiteKey(baseUrl) {
|
|
3034
|
+
if (cachedSiteKeys.has(baseUrl)) return cachedSiteKeys.get(baseUrl) ?? null;
|
|
3035
|
+
const inflight = siteKeyFetchPromises.get(baseUrl);
|
|
3036
|
+
if (inflight) return inflight;
|
|
3037
|
+
const nextPromise = (async () => {
|
|
3038
|
+
try {
|
|
3039
|
+
const res = await fetch(`${baseUrl}/api/config`);
|
|
3040
|
+
if (!res.ok) return null;
|
|
3041
|
+
const data = await res.json();
|
|
3042
|
+
const nextKey = data.captcha?.siteKey ?? null;
|
|
3043
|
+
cachedSiteKeys.set(baseUrl, nextKey);
|
|
3044
|
+
return nextKey;
|
|
3045
|
+
} catch {
|
|
3046
|
+
return null;
|
|
3047
|
+
} finally {
|
|
3048
|
+
siteKeyFetchPromises.delete(baseUrl);
|
|
3049
|
+
}
|
|
3050
|
+
})();
|
|
3051
|
+
siteKeyFetchPromises.set(baseUrl, nextPromise);
|
|
3052
|
+
return nextPromise;
|
|
3053
|
+
}
|
|
3054
|
+
function useTurnstile({
|
|
3055
|
+
baseUrl,
|
|
3056
|
+
action = "auth"
|
|
3057
|
+
}) {
|
|
3058
|
+
const [token, setTokenState] = useState(null);
|
|
3059
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
3060
|
+
const [error, setError] = useState(null);
|
|
3061
|
+
const [needsInteraction, setNeedsInteraction] = useState(false);
|
|
3062
|
+
const [siteKey, setSiteKey] = useState(null);
|
|
3063
|
+
useEffect(() => {
|
|
3064
|
+
let cancelled = false;
|
|
3065
|
+
setIsLoading(true);
|
|
3066
|
+
fetchSiteKey(baseUrl).then((key) => {
|
|
3067
|
+
if (!cancelled) {
|
|
3068
|
+
setSiteKey(key);
|
|
3069
|
+
if (!key) setIsLoading(false);
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
return () => {
|
|
3073
|
+
cancelled = true;
|
|
3074
|
+
};
|
|
3075
|
+
}, [baseUrl]);
|
|
3076
|
+
const reset = useCallback(() => {
|
|
3077
|
+
setTokenState(null);
|
|
3078
|
+
setError(null);
|
|
3079
|
+
setNeedsInteraction(false);
|
|
3080
|
+
setIsLoading(true);
|
|
3081
|
+
}, []);
|
|
3082
|
+
const handleToken = useCallback((t) => {
|
|
3083
|
+
setTokenState(t);
|
|
3084
|
+
setIsLoading(false);
|
|
3085
|
+
setError(null);
|
|
3086
|
+
setNeedsInteraction(false);
|
|
3087
|
+
}, []);
|
|
3088
|
+
const handleError = useCallback((e) => {
|
|
3089
|
+
setError(e);
|
|
3090
|
+
setIsLoading(false);
|
|
3091
|
+
}, []);
|
|
3092
|
+
const handleInteractive = useCallback(() => {
|
|
3093
|
+
setNeedsInteraction(true);
|
|
3094
|
+
}, []);
|
|
3095
|
+
const setToken = useCallback((t) => {
|
|
3096
|
+
setTokenState(t);
|
|
3097
|
+
setIsLoading(false);
|
|
3098
|
+
}, []);
|
|
3099
|
+
return {
|
|
3100
|
+
token,
|
|
3101
|
+
isLoading,
|
|
3102
|
+
error,
|
|
3103
|
+
needsInteraction,
|
|
3104
|
+
siteKey,
|
|
3105
|
+
reset,
|
|
3106
|
+
setToken,
|
|
3107
|
+
onToken: handleToken,
|
|
3108
|
+
onError: handleError,
|
|
3109
|
+
onInteractive: handleInteractive
|
|
3110
|
+
};
|
|
3111
|
+
}
|
|
3112
|
+
function isPlatformWeb() {
|
|
3113
|
+
return typeof document !== "undefined" && typeof navigator !== "undefined" && !("ReactNativeWebView" in window);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
export { AuthClient, ClientAnalytics, ClientEdgeBase, DatabaseLiveClient, LifecycleManager, PushClient, RoomClient, TokenManager, TurnstileWebView, createClient, isPlatformWeb, matchesFilter, useLifecycle, useTurnstile };
|
|
3117
|
+
//# sourceMappingURL=index.js.map
|
|
3118
|
+
//# sourceMappingURL=index.js.map
|