@rool-dev/sdk 0.12.5 → 2.0.0-dev.36748aa
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 +436 -784
- package/dist/agents.d.ts +138 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +563 -0
- package/dist/agents.js.map +1 -0
- package/dist/auth-base.d.ts +43 -88
- package/dist/auth-base.d.ts.map +1 -1
- package/dist/auth-base.js +178 -302
- package/dist/auth-base.js.map +1 -1
- package/dist/auth-browser.d.ts +7 -25
- package/dist/auth-browser.d.ts.map +1 -1
- package/dist/auth-browser.js +45 -82
- package/dist/auth-browser.js.map +1 -1
- package/dist/auth-native.d.ts +9 -56
- package/dist/auth-native.d.ts.map +1 -1
- package/dist/auth-native.js +88 -177
- package/dist/auth-native.js.map +1 -1
- package/dist/auth-node.d.ts +32 -36
- package/dist/auth-node.d.ts.map +1 -1
- package/dist/auth-node.js +338 -265
- package/dist/auth-node.js.map +1 -1
- package/dist/auth-url.d.ts +6 -0
- package/dist/auth-url.d.ts.map +1 -0
- package/dist/auth-url.js +30 -0
- package/dist/auth-url.js.map +1 -0
- package/dist/client.d.ts +35 -290
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +323 -635
- package/dist/client.js.map +1 -1
- package/dist/events.d.ts +40 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +175 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +174 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +846 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +16 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -20
- package/dist/index.js.map +1 -1
- package/dist/machine.d.ts +51 -0
- package/dist/machine.d.ts.map +1 -0
- package/dist/machine.js +142 -0
- package/dist/machine.js.map +1 -0
- package/dist/problem.d.ts +11 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +20 -0
- package/dist/problem.js.map +1 -0
- package/dist/structured.d.ts +84 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/structured.js +405 -0
- package/dist/structured.js.map +1 -0
- package/dist/types.d.ts +119 -520
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -16
- package/dist/auth.d.ts +0 -99
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -217
- package/dist/auth.js.map +0 -1
- package/dist/client-info.d.ts +0 -18
- package/dist/client-info.d.ts.map +0 -1
- package/dist/client-info.js +0 -25
- package/dist/client-info.js.map +0 -1
- package/dist/event-emitter.d.ts +0 -45
- package/dist/event-emitter.d.ts.map +0 -1
- package/dist/event-emitter.js +0 -86
- package/dist/event-emitter.js.map +0 -1
- package/dist/graphql.d.ts +0 -90
- package/dist/graphql.d.ts.map +0 -1
- package/dist/graphql.js +0 -471
- package/dist/graphql.js.map +0 -1
- package/dist/logger.d.ts +0 -13
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -17
- package/dist/logger.js.map +0 -1
- package/dist/path.d.ts +0 -6
- package/dist/path.d.ts.map +0 -1
- package/dist/path.js +0 -47
- package/dist/path.js.map +0 -1
- package/dist/reroute.d.ts +0 -22
- package/dist/reroute.d.ts.map +0 -1
- package/dist/reroute.js +0 -70
- package/dist/reroute.js.map +0 -1
- package/dist/rest.d.ts +0 -54
- package/dist/rest.d.ts.map +0 -1
- package/dist/rest.js +0 -171
- package/dist/rest.js.map +0 -1
- package/dist/router.d.ts +0 -20
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js +0 -54
- package/dist/router.js.map +0 -1
- package/dist/space-session.d.ts +0 -30
- package/dist/space-session.d.ts.map +0 -1
- package/dist/space-session.js +0 -83
- package/dist/space-session.js.map +0 -1
- package/dist/space.d.ts +0 -161
- package/dist/space.d.ts.map +0 -1
- package/dist/space.js +0 -618
- package/dist/space.js.map +0 -1
- package/dist/subscription.d.ts +0 -36
- package/dist/subscription.d.ts.map +0 -1
- package/dist/subscription.js +0 -419
- package/dist/subscription.js.map +0 -1
- package/dist/webdav.d.ts +0 -171
- package/dist/webdav.d.ts.map +0 -1
- package/dist/webdav.js +0 -481
- package/dist/webdav.js.map +0 -1
package/dist/client.js
CHANGED
|
@@ -1,676 +1,364 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* - User storage (cross-device key-value storage)
|
|
21
|
-
*/
|
|
22
|
-
export class RoolClient extends EventEmitter {
|
|
23
|
-
baseUrl;
|
|
24
|
-
urls;
|
|
25
|
-
authManager;
|
|
26
|
-
graphqlClient;
|
|
27
|
-
router;
|
|
28
|
-
subscriptionManager = null;
|
|
29
|
-
logger;
|
|
30
|
-
clientInfo;
|
|
31
|
-
_serverInfo = null;
|
|
32
|
-
// Open spaces (cached for reuse)
|
|
33
|
-
openSpaces = new Map();
|
|
34
|
-
// User storage cache (in-memory; populated from server on initialize)
|
|
35
|
-
_storageCache = {};
|
|
36
|
-
// Current user (fetched during initialize)
|
|
37
|
-
_currentUser = null;
|
|
1
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
2
|
+
import { createRoolEvents } from "./events.js";
|
|
3
|
+
import { RoolMachine } from "./machine.js";
|
|
4
|
+
import { throwProblemResponse } from "./problem.js";
|
|
5
|
+
const DEFAULT_API_URL = "https://api.rool.dev";
|
|
6
|
+
const MACHINE_ROUTE_CHANGED_HEADER = "Rool-Machine-Route-Changed";
|
|
7
|
+
const ROUTE_CHANGE_MAX_RETRIES = 6;
|
|
8
|
+
const ROUTE_CHANGE_RETRY_BASE_MS = 150;
|
|
9
|
+
const ROUTE_CHANGE_RETRY_MAX_MS = 5_000;
|
|
10
|
+
export const roolSdkVersion = packageJson.version;
|
|
11
|
+
export class RoolClient {
|
|
12
|
+
apiUrl;
|
|
13
|
+
fetchRequest;
|
|
14
|
+
usesDefaultFetch;
|
|
15
|
+
getTokens;
|
|
16
|
+
onAuthInvalidated;
|
|
17
|
+
identity;
|
|
18
|
+
machines = new Map();
|
|
19
|
+
events;
|
|
38
20
|
constructor(config = {}) {
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const apiOrigin = ((config.apiUrl ?? config.baseUrl) ?? 'https://api.rool.dev').replace(/\/+$/, '');
|
|
47
|
-
let authOrigin = apiOrigin;
|
|
48
|
-
try {
|
|
49
|
-
const parsed = new URL(apiOrigin);
|
|
50
|
-
if (parsed.hostname.startsWith('api.')) {
|
|
51
|
-
parsed.hostname = parsed.hostname.slice(4);
|
|
52
|
-
authOrigin = parsed.origin;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch { /* keep apiOrigin as fallback */ }
|
|
56
|
-
this.baseUrl = apiOrigin;
|
|
57
|
-
this.urls = {
|
|
58
|
-
graphql: config.graphqlUrl ?? `${apiOrigin}/graphql`,
|
|
59
|
-
auth: config.authUrl ?? `${authOrigin}/auth`,
|
|
60
|
-
webdav: apiOrigin,
|
|
61
|
-
};
|
|
62
|
-
this.authManager = new AuthManager({
|
|
63
|
-
authUrl: this.urls.auth,
|
|
64
|
-
authProvider: config.authProvider,
|
|
65
|
-
logger: this.logger,
|
|
66
|
-
onAuthStateChanged: (authenticated) => {
|
|
67
|
-
// Covers every sign-out path (logout() and 401-driven token clearing).
|
|
68
|
-
// Subscriptions must die with the session, or they would keep
|
|
69
|
-
// reconnecting — and would pick up the next account's tokens.
|
|
70
|
-
if (!authenticated) {
|
|
71
|
-
this._storageCache = {};
|
|
72
|
-
this.setCurrentUser(null);
|
|
73
|
-
this.closeRealtimeResources();
|
|
74
|
-
}
|
|
75
|
-
this.emit('authStateChanged', authenticated);
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
this.graphqlClient = new GraphQLClient({
|
|
79
|
-
graphqlUrl: this.urls.graphql,
|
|
80
|
-
authManager: this.authManager,
|
|
81
|
-
clientInfo: this.clientInfo,
|
|
82
|
-
});
|
|
83
|
-
this.router = new SpaceRouter({
|
|
84
|
-
apiUrl: this.baseUrl,
|
|
85
|
-
authManager: this.authManager,
|
|
86
|
-
clientInfo: this.clientInfo,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Initialize the client - should be called on app startup.
|
|
91
|
-
* Processes any auth callback in the URL, sets up auto-refresh,
|
|
92
|
-
* and starts real-time event subscription if authenticated.
|
|
93
|
-
* @returns true if authenticated, false otherwise
|
|
94
|
-
*/
|
|
95
|
-
async initialize() {
|
|
96
|
-
this.authManager.initialize();
|
|
97
|
-
const authenticated = await this.isAuthenticated();
|
|
98
|
-
if (authenticated) {
|
|
99
|
-
await this.hydrateAuthenticatedSession();
|
|
100
|
-
}
|
|
101
|
-
return authenticated;
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Start the realtime subscription and load the current user/storage. Shared
|
|
105
|
-
* by initialize() and verify() — any path that lands the user in an
|
|
106
|
-
* authenticated state needs this hydration.
|
|
107
|
-
*
|
|
108
|
-
* Hydration is best-effort and never throws: a backend outage must not read
|
|
109
|
-
* as a logout. The subscription owns reconnect/backoff and refetches the user
|
|
110
|
-
* on (re)connect (see ensureSubscribed), so a session that boots while the
|
|
111
|
-
* server is down stays authenticated and self-heals when it returns. Only a
|
|
112
|
-
* hard auth failure ends the session: a refresh rejection (token layer) or
|
|
113
|
-
* a 401 from the GraphQL transport, which calls authManager.logout().
|
|
114
|
-
*/
|
|
115
|
-
async hydrateAuthenticatedSession() {
|
|
116
|
-
this.ensureSubscribed().catch((error) => {
|
|
117
|
-
this.logger.warn('[RoolClient] subscription start deferred:', error);
|
|
118
|
-
});
|
|
119
|
-
try {
|
|
120
|
-
await this.fetchUserAndStorage();
|
|
21
|
+
this.apiUrl = (config.apiUrl ?? DEFAULT_API_URL).replace(/\/+$/, "");
|
|
22
|
+
this.fetchRequest = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
23
|
+
this.usesDefaultFetch = config.fetch === undefined;
|
|
24
|
+
this.identity = config.client;
|
|
25
|
+
this.onAuthInvalidated = config.onAuthInvalidated;
|
|
26
|
+
if (config.getTokens && config.getAccessToken) {
|
|
27
|
+
throw new Error("getTokens and getAccessToken cannot be used together");
|
|
121
28
|
}
|
|
122
|
-
|
|
123
|
-
this.
|
|
29
|
+
if (config.getTokens) {
|
|
30
|
+
this.getTokens = async () => (await config.getTokens()) ?? undefined;
|
|
124
31
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
*/
|
|
131
|
-
async fetchUserAndStorage() {
|
|
132
|
-
const user = await this.graphqlClient.getCurrentUser();
|
|
133
|
-
this._storageCache = user.storage ?? {};
|
|
134
|
-
this.setCurrentUser(user);
|
|
135
|
-
}
|
|
136
|
-
setCurrentUser(user) {
|
|
137
|
-
this._currentUser = user;
|
|
138
|
-
this.emit('currentUserChanged', user);
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Clean up resources - call when destroying the client.
|
|
142
|
-
*/
|
|
143
|
-
destroy() {
|
|
144
|
-
this.authManager.destroy();
|
|
145
|
-
this.closeRealtimeResources();
|
|
146
|
-
this.removeAllListeners();
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Initiate login by redirecting to auth page.
|
|
150
|
-
* @param appName - The name of the application requesting login (displayed on auth page)
|
|
151
|
-
*/
|
|
152
|
-
async login(appName, params) {
|
|
153
|
-
return this.authManager.login(appName, params);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Initiate signup by redirecting to auth page.
|
|
157
|
-
* @param appName - The name of the application requesting signup (displayed on auth page)
|
|
158
|
-
* @param params - Optional additional query parameters to pass to the auth server
|
|
159
|
-
*/
|
|
160
|
-
async signup(appName, params) {
|
|
161
|
-
return this.authManager.signup(appName, params);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Complete an email verification flow using a token from the verification
|
|
165
|
-
* email link. Exchanges the token for a live session and signs the user in
|
|
166
|
-
* without a redirect. Intended to be called when the app detects a
|
|
167
|
-
* `?verify=<token>` query parameter on load.
|
|
168
|
-
*
|
|
169
|
-
* Returns true if the user is now signed in as a result.
|
|
170
|
-
*/
|
|
171
|
-
async verify(token) {
|
|
172
|
-
const ok = await this.authManager.verify(token);
|
|
173
|
-
if (ok) {
|
|
174
|
-
await this.hydrateAuthenticatedSession();
|
|
175
|
-
}
|
|
176
|
-
return ok;
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Complete a native sign-in (PKCE) from a deep-link callback URL. Call this
|
|
180
|
-
* from the app's platform deep-link handler (e.g. Capacitor's `appUrlOpen`)
|
|
181
|
-
* with the full callback URL. Exchanges the code for a live session.
|
|
182
|
-
*
|
|
183
|
-
* Returns true if the user is now signed in as a result.
|
|
184
|
-
*/
|
|
185
|
-
async handleAuthRedirect(url) {
|
|
186
|
-
const ok = await this.authManager.handleRedirect(url);
|
|
187
|
-
if (ok) {
|
|
188
|
-
await this.hydrateAuthenticatedSession();
|
|
32
|
+
else if (config.getAccessToken) {
|
|
33
|
+
this.getTokens = async () => {
|
|
34
|
+
const accessToken = await config.getAccessToken();
|
|
35
|
+
return accessToken ? { accessToken, roolToken: "" } : undefined;
|
|
36
|
+
};
|
|
189
37
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Sign in with email + password. Resolves to `{ status: 'signed_in' }` once
|
|
194
|
-
* authenticated, or `{ status: 'verify_required' }` when the account's email
|
|
195
|
-
* isn't verified yet (a magic link has been emailed). Rejects with a
|
|
196
|
-
* human-readable Error on bad credentials or server failure.
|
|
197
|
-
*/
|
|
198
|
-
async signInWithPassword(email, password) {
|
|
199
|
-
const result = await this.authManager.signInWithPassword(email, password);
|
|
200
|
-
if (result.status === 'signed_in') {
|
|
201
|
-
await this.hydrateAuthenticatedSession();
|
|
38
|
+
else {
|
|
39
|
+
this.getTokens = async () => undefined;
|
|
202
40
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
*/
|
|
211
|
-
async requestMagicLink(email) {
|
|
212
|
-
return this.authManager.requestMagicLink(email);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Set or change the current user's password. Other authenticated sessions are
|
|
216
|
-
* revoked while this client receives replacement credentials.
|
|
217
|
-
*
|
|
218
|
-
* Throws an Error with a human-readable message on validation or server failure.
|
|
219
|
-
*/
|
|
220
|
-
async setPassword(password) {
|
|
221
|
-
return this.authManager.setPassword(password);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Request an email address change for the current user. The server mails a
|
|
225
|
-
* confirmation link to the new address; the change applies when that link
|
|
226
|
-
* is clicked. After confirmation this session no longer belongs to the
|
|
227
|
-
* account — sign out and sign in with the new address.
|
|
228
|
-
*
|
|
229
|
-
* Rejects with EmailChangeError (code + user-facing message) on refusal,
|
|
230
|
-
* e.g. 'email_in_use' or 'invalid_email'.
|
|
231
|
-
*/
|
|
232
|
-
async requestEmailChange(newEmail) {
|
|
233
|
-
return this.authManager.requestEmailChange(newEmail);
|
|
234
|
-
}
|
|
235
|
-
/**
|
|
236
|
-
* Logout - clear all tokens and state.
|
|
237
|
-
*/
|
|
238
|
-
logout() {
|
|
239
|
-
this.authManager.logout();
|
|
240
|
-
// Redundant when the provider notified (the auth-state handler already
|
|
241
|
-
// tore down), but kept for providers without auth-state bridging.
|
|
242
|
-
this.closeRealtimeResources();
|
|
243
|
-
}
|
|
244
|
-
/** Stop the client subscription and close all open spaces. Idempotent. */
|
|
245
|
-
closeRealtimeResources() {
|
|
246
|
-
this.unsubscribe();
|
|
247
|
-
for (const space of this.openSpaces.values())
|
|
248
|
-
space.close();
|
|
249
|
-
this.openSpaces.clear();
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Check if user is currently authenticated (validates token is usable).
|
|
253
|
-
*/
|
|
254
|
-
async isAuthenticated() {
|
|
255
|
-
return this.authManager.isAuthenticated();
|
|
256
|
-
}
|
|
257
|
-
/**
|
|
258
|
-
* Make an authenticated fetch request to the Rool API.
|
|
259
|
-
* @internal Not part of the public API — use typed methods instead.
|
|
260
|
-
*
|
|
261
|
-
* @param path - Path relative to the base URL (e.g., '/billing/usage')
|
|
262
|
-
* @param init - Standard fetch RequestInit options. Authorization header is added automatically.
|
|
263
|
-
*/
|
|
264
|
-
async _api(path, init) {
|
|
265
|
-
const tokens = await this.authManager.getTokens();
|
|
266
|
-
if (!tokens)
|
|
267
|
-
throw new Error('Not authenticated');
|
|
268
|
-
const headers = new Headers(init?.headers);
|
|
269
|
-
headers.set('Authorization', `Bearer ${tokens.accessToken}`);
|
|
270
|
-
headers.set('X-Rool-Token', tokens.roolToken);
|
|
271
|
-
addClientInfoHeaders(headers, this.clientInfo);
|
|
272
|
-
return fetch(`${this.baseUrl}${path}`, { ...init, headers });
|
|
41
|
+
this.events = createRoolEvents({
|
|
42
|
+
poll: (syncToken, signal) => {
|
|
43
|
+
const query = new URLSearchParams({ syncToken });
|
|
44
|
+
return this.request(`/v2/events?${query}`, { headers: { Prefer: "wait=45" }, signal }, true);
|
|
45
|
+
},
|
|
46
|
+
getSession: () => this.getSession(),
|
|
47
|
+
});
|
|
273
48
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
* For the Rool user (with server-assigned id), use currentUser.
|
|
277
|
-
*/
|
|
278
|
-
getAuthUser() {
|
|
279
|
-
return this.authManager.getAuthUser();
|
|
49
|
+
getSession() {
|
|
50
|
+
return this.requestJson("/v2/session");
|
|
280
51
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
* Available after successful authentication.
|
|
284
|
-
*/
|
|
285
|
-
get currentUser() {
|
|
286
|
-
return this._currentUser;
|
|
52
|
+
getAccount() {
|
|
53
|
+
return this.requestJson("/v2/me");
|
|
287
54
|
}
|
|
288
|
-
|
|
289
|
-
|
|
55
|
+
async deleteAccount() {
|
|
56
|
+
await this.request("/v2/me", { method: "DELETE" });
|
|
290
57
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
*/
|
|
294
|
-
async listSpaces() {
|
|
295
|
-
return this.graphqlClient.listSpaces();
|
|
58
|
+
getProfile() {
|
|
59
|
+
return this.requestJson("/v2/me/profile");
|
|
296
60
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
* Call space.close() when done to stop the subscription.
|
|
303
|
-
*/
|
|
304
|
-
async openSpace(spaceId) {
|
|
305
|
-
// Reuse existing open space
|
|
306
|
-
const existing = this.openSpaces.get(spaceId);
|
|
307
|
-
if (existing)
|
|
308
|
-
return existing;
|
|
309
|
-
// Ensure client subscription is active (for lifecycle events)
|
|
310
|
-
// .catch prevents a rejection here from crashing Node before the caller awaits it.
|
|
311
|
-
this.ensureSubscribed().catch(() => { });
|
|
312
|
-
const initialRoute = await this.router.resolve(spaceId);
|
|
313
|
-
const scopedGraphqlUrl = `${initialRoute.server.replace(/\/+$/, '')}/graphql`;
|
|
314
|
-
const scopedClient = new GraphQLClient({
|
|
315
|
-
graphqlUrl: scopedGraphqlUrl,
|
|
316
|
-
authManager: this.authManager,
|
|
317
|
-
clientInfo: this.clientInfo,
|
|
318
|
-
});
|
|
319
|
-
const scopedRestClient = new RestClient({
|
|
320
|
-
apiUrl: initialRoute.server,
|
|
321
|
-
authManager: this.authManager,
|
|
322
|
-
clientInfo: this.clientInfo,
|
|
61
|
+
replaceProfile(profile) {
|
|
62
|
+
return this.requestJson("/v2/me/profile", {
|
|
63
|
+
method: "PUT",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify(profile),
|
|
323
66
|
});
|
|
324
|
-
const openSpaceResult = await scopedClient.openSpace(spaceId);
|
|
325
|
-
const space = new RoolSpace({
|
|
326
|
-
id: spaceId,
|
|
327
|
-
openSpaceResult,
|
|
328
|
-
graphqlClient: scopedClient,
|
|
329
|
-
restClient: scopedRestClient,
|
|
330
|
-
authManager: this.authManager,
|
|
331
|
-
clientInfo: this.clientInfo,
|
|
332
|
-
router: this.router,
|
|
333
|
-
initialRoute,
|
|
334
|
-
logger: this.logger,
|
|
335
|
-
onClose: () => this.openSpaces.delete(spaceId),
|
|
336
|
-
});
|
|
337
|
-
this.openSpaces.set(spaceId, space);
|
|
338
|
-
return space;
|
|
339
67
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
* Returns a RoolSpace handle with a real-time subscription.
|
|
343
|
-
* Use the returned RoolSpace to work with objects, conversations, and AI.
|
|
344
|
-
*/
|
|
345
|
-
async createSpace(name) {
|
|
346
|
-
// Prevents a rejection here from crashing Node before the caller awaits it.
|
|
347
|
-
this.ensureSubscribed().catch(() => { });
|
|
348
|
-
const { spaceId } = await this.graphqlClient.createSpace(name);
|
|
349
|
-
return this.openSpace(spaceId);
|
|
68
|
+
getUserAppData() {
|
|
69
|
+
return this.requestJson("/v2/me/app-data");
|
|
350
70
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const scopedUrl = (server) => `${server.replace(/\/+$/, '')}/graphql`;
|
|
357
|
-
const route = await this.router.resolve(spaceId);
|
|
358
|
-
return new GraphQLClient({
|
|
359
|
-
graphqlUrl: scopedUrl(route.server),
|
|
360
|
-
authManager: this.authManager,
|
|
361
|
-
clientInfo: this.clientInfo,
|
|
362
|
-
onRefused: async () => scopedUrl((await this.router.resolve(spaceId)).server),
|
|
71
|
+
async setUserAppData(key, value) {
|
|
72
|
+
await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
|
|
73
|
+
method: "PUT",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ value }),
|
|
363
76
|
});
|
|
364
77
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
async deleteSpace(spaceId) {
|
|
370
|
-
const scoped = await this.scopedGraphqlClient(spaceId);
|
|
371
|
-
await scoped.deleteSpace(spaceId);
|
|
372
|
-
// Close and remove the cached space if open
|
|
373
|
-
const space = this.openSpaces.get(spaceId);
|
|
374
|
-
if (space) {
|
|
375
|
-
space.close();
|
|
376
|
-
this.openSpaces.delete(spaceId);
|
|
377
|
-
}
|
|
378
|
-
// Client-level event will be emitted via SSE subscription
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Duplicate an existing space. Returns a handle to the new space.
|
|
382
|
-
*/
|
|
383
|
-
async duplicateSpace(sourceSpaceId, name) {
|
|
384
|
-
this.ensureSubscribed().catch(() => { });
|
|
385
|
-
const scoped = await this.scopedGraphqlClient(sourceSpaceId);
|
|
386
|
-
const { spaceId } = await scoped.duplicateSpace(sourceSpaceId, name);
|
|
387
|
-
return this.openSpace(spaceId);
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Mark the current user for deletion (7-day grace period).
|
|
391
|
-
* Irrecoverable after the grace period elapses.
|
|
392
|
-
*/
|
|
393
|
-
async deleteCurrentUser() {
|
|
394
|
-
await this.graphqlClient.deleteCurrentUser();
|
|
395
|
-
this.logout();
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Import a space from a zip archive.
|
|
399
|
-
* Creates a new space with the given name and imports objects, relations, and files.
|
|
400
|
-
* Returns a RoolSpace handle.
|
|
401
|
-
*/
|
|
402
|
-
async importArchive(name, archive) {
|
|
403
|
-
// .catch prevents a rejection here from crashing Node before the caller awaits it.
|
|
404
|
-
this.ensureSubscribed().catch(() => { });
|
|
405
|
-
// Import via REST endpoint (creates the space)
|
|
406
|
-
const spaceId = await this.restClient.importArchive(name, archive);
|
|
407
|
-
// Open the space to get its data
|
|
408
|
-
return this.openSpace(spaceId);
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Get the current Rool user from the server.
|
|
412
|
-
* Returns the user's server-assigned id, email, plan, and credits.
|
|
413
|
-
*/
|
|
414
|
-
async getCurrentUser() {
|
|
415
|
-
const user = await this.graphqlClient.getCurrentUser();
|
|
416
|
-
// On first hydration, populate the storage cache before emitting so
|
|
417
|
-
// listeners don't see a user with empty storage. Once hydrated, a polled
|
|
418
|
-
// snapshot must not clobber optimistic setUserStorage writes.
|
|
419
|
-
if (!this._currentUser) {
|
|
420
|
-
this._storageCache = user.storage ?? {};
|
|
421
|
-
}
|
|
422
|
-
this.setCurrentUser(user);
|
|
423
|
-
return user;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Look up an invite link by its token, without redeeming it.
|
|
427
|
-
* Does not require authentication.
|
|
428
|
-
*/
|
|
429
|
-
async previewInvite(token) {
|
|
430
|
-
return this.restClient.previewInvite(token);
|
|
78
|
+
async deleteUserAppData(key) {
|
|
79
|
+
await this.request(`/v2/me/app-data/${encodeURIComponent(key)}`, {
|
|
80
|
+
method: "DELETE",
|
|
81
|
+
});
|
|
431
82
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
return this.
|
|
83
|
+
getGreeting(language) {
|
|
84
|
+
if (!language)
|
|
85
|
+
return this.requestJson("/v2/greeting");
|
|
86
|
+
const query = new URLSearchParams({ language });
|
|
87
|
+
return this.requestJson(`/v2/greeting?${query}`);
|
|
437
88
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
* Does not require authentication.
|
|
441
|
-
*/
|
|
442
|
-
async previewGift(code) {
|
|
443
|
-
return this.restClient.previewGift(code);
|
|
89
|
+
listMachines() {
|
|
90
|
+
return this.requestJson("/v2/machines");
|
|
444
91
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// The claim is spent and irreversible by now, so a failed refresh must not
|
|
452
|
-
// surface as a failed claim: the retry would report gift_claimed.
|
|
453
|
-
try {
|
|
454
|
-
await this.getCurrentUser();
|
|
455
|
-
}
|
|
456
|
-
catch (error) {
|
|
457
|
-
this.logger.warn('[RoolClient] user refresh after gift claim deferred:', error);
|
|
458
|
-
}
|
|
459
|
-
return result;
|
|
460
|
-
}
|
|
461
|
-
/**
|
|
462
|
-
* The current user's gifts, spent and unspent.
|
|
463
|
-
*/
|
|
464
|
-
async listGifts() {
|
|
465
|
-
return this.restClient.listGifts();
|
|
92
|
+
createMachine(settings) {
|
|
93
|
+
return this.requestJson("/v2/machines", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify(settings),
|
|
97
|
+
});
|
|
466
98
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
99
|
+
machine(machineId) {
|
|
100
|
+
let machine = this.machines.get(machineId);
|
|
101
|
+
if (machine)
|
|
102
|
+
return machine;
|
|
103
|
+
machine = new RoolMachine(machineId, {
|
|
104
|
+
send: (path, init, onUploadProgress) => this.send(path, init, { onUploadProgress }),
|
|
105
|
+
request: (path, init, allowHttpErrors) => this.request(path, init, allowHttpErrors),
|
|
106
|
+
requestJson: (path, init) => this.requestJson(path, init),
|
|
107
|
+
subscribeEvents: (listener) => this.events.subscribe(listener),
|
|
108
|
+
deleted: () => this.machines.delete(machineId),
|
|
109
|
+
});
|
|
110
|
+
this.machines.set(machineId, machine);
|
|
111
|
+
return machine;
|
|
473
112
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
* link stop working; the gift itself is untouched.
|
|
477
|
-
*/
|
|
478
|
-
async rotateGiftCode(giftId) {
|
|
479
|
-
return this.restClient.rotateGiftCode(giftId);
|
|
113
|
+
getInvitePreview(token) {
|
|
114
|
+
return this.requestJson(`/v2/invites/${encodeURIComponent(token)}`);
|
|
480
115
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
* - name: display name
|
|
484
|
-
* - slug: used in app publishing URLs (3-32 chars, start with letter, lowercase alphanumeric/hyphens/underscores)
|
|
485
|
-
*/
|
|
486
|
-
async updateCurrentUser(input) {
|
|
487
|
-
const user = await this.graphqlClient.updateCurrentUser(input);
|
|
488
|
-
this.setCurrentUser(user);
|
|
489
|
-
return user;
|
|
116
|
+
redeemInvite(token) {
|
|
117
|
+
return this.requestJson(`/v2/invites/${encodeURIComponent(token)}/redemption`, { method: "POST" });
|
|
490
118
|
}
|
|
491
|
-
/**
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
* Returns undefined if key doesn't exist.
|
|
495
|
-
*/
|
|
496
|
-
getUserStorage(key) {
|
|
497
|
-
return this._storageCache[key];
|
|
119
|
+
/** Look up a gift by its code without claiming it. */
|
|
120
|
+
previewGift(code) {
|
|
121
|
+
return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}`);
|
|
498
122
|
}
|
|
499
|
-
/**
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
* Storage is limited to 10MB total.
|
|
504
|
-
*/
|
|
505
|
-
setUserStorage(key, value) {
|
|
506
|
-
// Update local cache
|
|
507
|
-
if (value === null || value === undefined) {
|
|
508
|
-
delete this._storageCache[key];
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
this._storageCache[key] = value;
|
|
512
|
-
}
|
|
513
|
-
// Emit event (local source)
|
|
514
|
-
this.emit('userStorageChanged', { key, value: value ?? null, source: 'local' });
|
|
515
|
-
// Fire-and-forget server sync
|
|
516
|
-
this.graphqlClient.setUserStorage(key, value).catch((error) => {
|
|
517
|
-
this.logger.error('[RoolClient] Failed to sync user storage:', error);
|
|
518
|
-
this.emit('error', error instanceof Error ? error : new Error(String(error)), 'userStorage');
|
|
123
|
+
/** Claim a gift for the current account. */
|
|
124
|
+
claimGift(code) {
|
|
125
|
+
return this.requestJson(`/v2/gifts/${encodeURIComponent(code)}/claim`, {
|
|
126
|
+
method: "POST",
|
|
519
127
|
});
|
|
520
128
|
}
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
getAllUserStorage() {
|
|
525
|
-
return { ...this._storageCache };
|
|
129
|
+
/** List the current user's claimed and unclaimed gifts. */
|
|
130
|
+
listGifts() {
|
|
131
|
+
return this.requestJson("/v2/me/gifts");
|
|
526
132
|
}
|
|
527
|
-
/**
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
this.logger.error('[RoolClient] Failed to report event:', error);
|
|
133
|
+
/** Set or clear a gift's note, or change its archived state. */
|
|
134
|
+
updateGift(giftId, changes) {
|
|
135
|
+
return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}`, {
|
|
136
|
+
method: "PATCH",
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify(changes),
|
|
534
139
|
});
|
|
535
140
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
clientInfo: this.clientInfo,
|
|
141
|
+
/** Mint a new code for an unclaimed gift. */
|
|
142
|
+
rotateGiftCode(giftId) {
|
|
143
|
+
return this.requestJson(`/v2/me/gifts/${encodeURIComponent(giftId)}/code`, {
|
|
144
|
+
method: "POST",
|
|
541
145
|
});
|
|
542
146
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
},
|
|
568
|
-
onError: (error) => {
|
|
569
|
-
this.emit('error', error, 'subscription');
|
|
570
|
-
},
|
|
571
|
-
});
|
|
572
|
-
await this.subscriptionManager.subscribe();
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Disconnect from real-time events.
|
|
576
|
-
* @internal
|
|
577
|
-
*/
|
|
578
|
-
unsubscribe() {
|
|
579
|
-
if (this.subscriptionManager) {
|
|
580
|
-
this.subscriptionManager.destroy();
|
|
581
|
-
this.subscriptionManager = null;
|
|
147
|
+
async requestJson(path, init) {
|
|
148
|
+
const response = await this.request(path, init);
|
|
149
|
+
return response.json();
|
|
150
|
+
}
|
|
151
|
+
async request(path, init, allowHttpErrors = false) {
|
|
152
|
+
const accept = new Headers(init?.headers).get("Accept") ?? "application/json";
|
|
153
|
+
const response = await this.send(path, init, { accept });
|
|
154
|
+
if (response.status === 401)
|
|
155
|
+
void this.onAuthInvalidated?.();
|
|
156
|
+
if (response.ok || allowHttpErrors)
|
|
157
|
+
return response;
|
|
158
|
+
return throwProblemResponse(response);
|
|
159
|
+
}
|
|
160
|
+
async send(path, init, options = {}) {
|
|
161
|
+
const signal = init?.signal;
|
|
162
|
+
signal?.throwIfAborted();
|
|
163
|
+
const headers = new Headers(init?.headers);
|
|
164
|
+
if (options.accept)
|
|
165
|
+
headers.set("Accept", options.accept);
|
|
166
|
+
headers.set("X-Rool-SDK-Name", packageJson.name);
|
|
167
|
+
headers.set("X-Rool-SDK-Version", packageJson.version);
|
|
168
|
+
if (this.identity?.appName) {
|
|
169
|
+
headers.set("X-Rool-App-Name", this.identity.appName);
|
|
582
170
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
171
|
+
if (this.identity?.appVersion) {
|
|
172
|
+
headers.set("X-Rool-App-Version", this.identity.appVersion);
|
|
173
|
+
}
|
|
174
|
+
if (this.identity?.osVersion) {
|
|
175
|
+
headers.set("X-Rool-OS-Version", this.identity.osVersion);
|
|
176
|
+
}
|
|
177
|
+
const tokens = await this.getTokens();
|
|
178
|
+
signal?.throwIfAborted();
|
|
179
|
+
if (tokens?.accessToken) {
|
|
180
|
+
headers.set("Authorization", `Bearer ${tokens.accessToken}`);
|
|
181
|
+
}
|
|
182
|
+
if (tokens?.roolToken)
|
|
183
|
+
headers.set("X-Rool-Token", tokens.roolToken);
|
|
184
|
+
const url = `${this.apiUrl}${path}`;
|
|
185
|
+
const configuredBody = init?.body;
|
|
186
|
+
const bodyFactory = typeof configuredBody === "function"
|
|
187
|
+
? configuredBody
|
|
188
|
+
: () => configuredBody;
|
|
189
|
+
const bodyIsReplayable = typeof configuredBody === "function" || !isReadableStream(configuredBody);
|
|
190
|
+
for (let retry = 0;; retry++) {
|
|
191
|
+
signal?.throwIfAborted();
|
|
192
|
+
const body = bodyFactory();
|
|
193
|
+
const requestInit = {
|
|
194
|
+
...init,
|
|
195
|
+
headers,
|
|
196
|
+
body,
|
|
197
|
+
};
|
|
198
|
+
if (isReadableStream(body))
|
|
199
|
+
requestInit.duplex = "half";
|
|
200
|
+
let response;
|
|
201
|
+
if (!options.onUploadProgress || body === undefined || body === null) {
|
|
202
|
+
response = await this.fetchRequest(url, requestInit);
|
|
614
203
|
}
|
|
615
|
-
|
|
616
|
-
this.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
ownerId: event.ownerId ?? '',
|
|
622
|
-
size: event.size ?? 0,
|
|
623
|
-
createdAt: event.createdAt ?? new Date().toISOString(),
|
|
624
|
-
updatedAt: event.updatedAt ?? new Date().toISOString(),
|
|
625
|
-
memberCount: 1, // Creator is the only member
|
|
626
|
-
});
|
|
627
|
-
break;
|
|
628
|
-
case 'space_deleted':
|
|
629
|
-
this.emit('spaceRemoved', event.spaceId);
|
|
630
|
-
break;
|
|
631
|
-
case 'space_renamed':
|
|
632
|
-
this.emit('spaceRenamed', event.spaceId, event.name);
|
|
633
|
-
break;
|
|
634
|
-
case 'space_access_changed':
|
|
635
|
-
if (event.role === 'none') {
|
|
636
|
-
this.emit('spaceRemoved', event.spaceId);
|
|
204
|
+
else {
|
|
205
|
+
const useXmlHttpRequest = this.usesDefaultFetch &&
|
|
206
|
+
typeof XMLHttpRequest !== "undefined" &&
|
|
207
|
+
!isReadableStream(body);
|
|
208
|
+
if (useXmlHttpRequest) {
|
|
209
|
+
response = await sendXmlHttpRequest(url, requestInit, body, options.onUploadProgress);
|
|
637
210
|
}
|
|
638
211
|
else {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
name: event.name,
|
|
642
|
-
inboundEmailAddress: event.inboundEmailAddress,
|
|
643
|
-
role: event.role,
|
|
644
|
-
ownerId: event.ownerId,
|
|
645
|
-
size: event.size,
|
|
646
|
-
createdAt: event.createdAt,
|
|
647
|
-
updatedAt: event.updatedAt,
|
|
648
|
-
memberCount: event.memberCount,
|
|
649
|
-
});
|
|
212
|
+
const progressRequest = requestWithUploadProgress(url, requestInit, body, options.onUploadProgress);
|
|
213
|
+
response = await this.fetchRequest(url, progressRequest);
|
|
650
214
|
}
|
|
651
|
-
break;
|
|
652
|
-
case 'user_storage_changed':
|
|
653
|
-
this.handleUserStorageChanged(event.key, event.value);
|
|
654
|
-
break;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
/**
|
|
658
|
-
* Handle a user storage change from SSE (remote update).
|
|
659
|
-
* Updates cache and emits event if value actually changed.
|
|
660
|
-
* @internal
|
|
661
|
-
*/
|
|
662
|
-
handleUserStorageChanged(key, value) {
|
|
663
|
-
const currentValue = this._storageCache[key];
|
|
664
|
-
// Only update and emit if value actually changed
|
|
665
|
-
if (JSON.stringify(currentValue) !== JSON.stringify(value)) {
|
|
666
|
-
if (value === null || value === undefined) {
|
|
667
|
-
delete this._storageCache[key];
|
|
668
215
|
}
|
|
669
|
-
|
|
670
|
-
|
|
216
|
+
const routeChanged = response.status === 421 &&
|
|
217
|
+
response.headers.get(MACHINE_ROUTE_CHANGED_HEADER) === "1";
|
|
218
|
+
if (!routeChanged ||
|
|
219
|
+
!bodyIsReplayable ||
|
|
220
|
+
retry >= ROUTE_CHANGE_MAX_RETRIES) {
|
|
221
|
+
return response;
|
|
671
222
|
}
|
|
672
|
-
|
|
223
|
+
await response.body?.cancel();
|
|
224
|
+
await delay(routeChangeBackoffMs(retry), signal);
|
|
673
225
|
}
|
|
674
226
|
}
|
|
675
227
|
}
|
|
228
|
+
function routeChangeBackoffMs(retry) {
|
|
229
|
+
const ceiling = Math.min(ROUTE_CHANGE_RETRY_BASE_MS * 2 ** retry, ROUTE_CHANGE_RETRY_MAX_MS);
|
|
230
|
+
return ceiling / 2 + Math.random() * (ceiling / 2);
|
|
231
|
+
}
|
|
232
|
+
function delay(milliseconds, signal) {
|
|
233
|
+
signal?.throwIfAborted();
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
const finish = () => {
|
|
236
|
+
signal?.removeEventListener("abort", abort);
|
|
237
|
+
resolve();
|
|
238
|
+
};
|
|
239
|
+
const abort = () => {
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
reject(signal?.reason);
|
|
242
|
+
};
|
|
243
|
+
const timer = setTimeout(finish, milliseconds);
|
|
244
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function isReadableStream(body) {
|
|
248
|
+
return (typeof ReadableStream !== "undefined" && body instanceof ReadableStream);
|
|
249
|
+
}
|
|
250
|
+
function requestWithUploadProgress(url, init, body, onUploadProgress) {
|
|
251
|
+
const preparedInit = { ...init };
|
|
252
|
+
if (isReadableStream(body))
|
|
253
|
+
preparedInit.duplex = "half";
|
|
254
|
+
const preparedRequest = new Request(url, preparedInit);
|
|
255
|
+
if (!preparedRequest.body)
|
|
256
|
+
throw new Error("Upload request is missing its body");
|
|
257
|
+
const totalBytes = uploadBodySize(body);
|
|
258
|
+
let transferredBytes = 0;
|
|
259
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
260
|
+
const progressBody = preparedRequest.body.pipeThrough(new TransformStream({
|
|
261
|
+
transform(chunk, controller) {
|
|
262
|
+
controller.enqueue(chunk);
|
|
263
|
+
transferredBytes += chunk.byteLength;
|
|
264
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
return {
|
|
268
|
+
...init,
|
|
269
|
+
headers: preparedRequest.headers,
|
|
270
|
+
body: progressBody,
|
|
271
|
+
duplex: "half",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
function sendXmlHttpRequest(url, init, body, onUploadProgress) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
const request = new XMLHttpRequest();
|
|
277
|
+
const signal = init.signal;
|
|
278
|
+
const totalBytes = uploadBodySize(body);
|
|
279
|
+
let transferredBytes = 0;
|
|
280
|
+
const cleanup = () => signal?.removeEventListener("abort", abort);
|
|
281
|
+
const abort = () => request.abort();
|
|
282
|
+
request.upload.onprogress = (event) => {
|
|
283
|
+
transferredBytes = event.loaded;
|
|
284
|
+
const reportedTotal = event.lengthComputable ? event.total : totalBytes;
|
|
285
|
+
reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
|
|
286
|
+
};
|
|
287
|
+
request.upload.onload = (event) => {
|
|
288
|
+
const loadedBytes = event.loaded;
|
|
289
|
+
if (loadedBytes === transferredBytes)
|
|
290
|
+
return;
|
|
291
|
+
transferredBytes = loadedBytes;
|
|
292
|
+
const reportedTotal = event.lengthComputable ? event.total : totalBytes;
|
|
293
|
+
reportUploadProgress(onUploadProgress, transferredBytes, reportedTotal);
|
|
294
|
+
};
|
|
295
|
+
request.onload = () => {
|
|
296
|
+
cleanup();
|
|
297
|
+
const hasResponseBody = request.response instanceof ArrayBuffer &&
|
|
298
|
+
request.response.byteLength > 0;
|
|
299
|
+
const responseBody = hasResponseBody ? request.response : null;
|
|
300
|
+
resolve(new Response(responseBody, {
|
|
301
|
+
status: request.status,
|
|
302
|
+
statusText: request.statusText,
|
|
303
|
+
headers: parseXmlHttpRequestHeaders(request.getAllResponseHeaders()),
|
|
304
|
+
}));
|
|
305
|
+
};
|
|
306
|
+
request.onerror = () => {
|
|
307
|
+
cleanup();
|
|
308
|
+
reject(new TypeError("Network request failed"));
|
|
309
|
+
};
|
|
310
|
+
request.onabort = () => {
|
|
311
|
+
cleanup();
|
|
312
|
+
reject(signal?.reason ??
|
|
313
|
+
new DOMException("This operation was aborted", "AbortError"));
|
|
314
|
+
};
|
|
315
|
+
if (signal?.aborted) {
|
|
316
|
+
reject(signal.reason ??
|
|
317
|
+
new DOMException("This operation was aborted", "AbortError"));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
request.open(init.method ?? "GET", url);
|
|
321
|
+
request.responseType = "arraybuffer";
|
|
322
|
+
request.withCredentials = init.credentials === "include";
|
|
323
|
+
new Headers(init.headers).forEach((value, name) => {
|
|
324
|
+
request.setRequestHeader(name, value);
|
|
325
|
+
});
|
|
326
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
327
|
+
reportUploadProgress(onUploadProgress, transferredBytes, totalBytes);
|
|
328
|
+
request.send(body);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function reportUploadProgress(listener, transferredBytes, totalBytes) {
|
|
332
|
+
const progress = { transferredBytes };
|
|
333
|
+
if (totalBytes !== undefined)
|
|
334
|
+
progress.totalBytes = totalBytes;
|
|
335
|
+
listener(progress);
|
|
336
|
+
}
|
|
337
|
+
function uploadBodySize(body) {
|
|
338
|
+
if (typeof body === "string")
|
|
339
|
+
return new TextEncoder().encode(body).byteLength;
|
|
340
|
+
if (typeof Blob !== "undefined" && body instanceof Blob)
|
|
341
|
+
return body.size;
|
|
342
|
+
if (body instanceof ArrayBuffer)
|
|
343
|
+
return body.byteLength;
|
|
344
|
+
if (ArrayBuffer.isView(body))
|
|
345
|
+
return body.byteLength;
|
|
346
|
+
if (typeof URLSearchParams !== "undefined" &&
|
|
347
|
+
body instanceof URLSearchParams) {
|
|
348
|
+
return new TextEncoder().encode(body.toString()).byteLength;
|
|
349
|
+
}
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
function parseXmlHttpRequestHeaders(rawHeaders) {
|
|
353
|
+
const headers = new Headers();
|
|
354
|
+
for (const line of rawHeaders.split(/\r?\n/)) {
|
|
355
|
+
if (!line)
|
|
356
|
+
continue;
|
|
357
|
+
const separator = line.indexOf(":");
|
|
358
|
+
if (separator === -1)
|
|
359
|
+
throw new Error(`Invalid response header: ${line}`);
|
|
360
|
+
headers.append(line.slice(0, separator), line.slice(separator + 1).trim());
|
|
361
|
+
}
|
|
362
|
+
return headers;
|
|
363
|
+
}
|
|
676
364
|
//# sourceMappingURL=client.js.map
|