CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/client/client.ts
Views: 926
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
import { bind_methods } from "@cocalc/util/misc";
6
import { EventEmitter } from "events";
7
import { delay } from "awaiting";
8
import { alert_message } from "../alerts";
9
import { StripeClient } from "./stripe";
10
import { ProjectCollaborators } from "./project-collaborators";
11
import { SupportTickets } from "./support";
12
import { Messages } from "./messages";
13
import { QueryClient } from "./query";
14
import { TimeClient } from "./time";
15
import { AccountClient } from "./account";
16
import { ProjectClient } from "./project";
17
import { AdminClient } from "./admin";
18
import { LLMClient } from "./llm";
19
import { PurchasesClient } from "./purchases";
20
import { JupyterClient } from "./jupyter";
21
import { SyncClient } from "@cocalc/sync/client/sync-client";
22
import { UsersClient } from "./users";
23
import { FileClient } from "./file";
24
import { TrackingClient } from "./tracking";
25
import { HubClient } from "./hub";
26
import { IdleClient } from "./idle";
27
import { version } from "@cocalc/util/smc-version";
28
import { start_metrics } from "../prom-client";
29
import { setup_global_cocalc } from "./console";
30
import { Query } from "@cocalc/sync/table";
31
import debug from "debug";
32
33
// This DEBUG variable comes from webpack:
34
declare const DEBUG;
35
36
const log = debug("cocalc");
37
// To get actual extreme logging though you have to also set
38
//
39
// localStorage.DEBUG='cocalc'
40
//
41
// and refresh your browser. Example, this will turn on
42
// all the sync activity logging and everything that calls
43
// client.dbg.
44
45
export type AsyncCall = (opts: object) => Promise<any>;
46
47
export interface WebappClient extends EventEmitter {
48
account_id?: string;
49
50
stripe: StripeClient;
51
project_collaborators: ProjectCollaborators;
52
support_tickets: SupportTickets;
53
messages: Messages;
54
query_client: QueryClient;
55
time_client: TimeClient;
56
account_client: AccountClient;
57
project_client: ProjectClient;
58
admin_client: AdminClient;
59
openai_client: LLMClient;
60
purchases_client: PurchasesClient;
61
jupyter_client: JupyterClient;
62
sync_client: SyncClient;
63
users_client: UsersClient;
64
file_client: FileClient;
65
tracking_client: TrackingClient;
66
hub_client: HubClient;
67
idle_client: IdleClient;
68
client: Client;
69
70
sync_string: Function;
71
sync_db: Function;
72
73
server_time: Function;
74
get_username: Function;
75
is_signed_in: () => boolean;
76
synctable_project: Function;
77
project_websocket: Function;
78
prettier: Function;
79
exec: Function;
80
touch_project: (project_id: string, compute_server_id?: number) => void;
81
ipywidgetsGetBuffer: (
82
project_id: string,
83
path: string,
84
model_id: string,
85
buffer_path: string,
86
) => Promise<ArrayBuffer>;
87
log_error: (any) => void;
88
async_call: AsyncCall;
89
user_tracking: Function;
90
send: Function;
91
call: Function;
92
dbg: (str: string) => Function;
93
is_project: () => boolean;
94
is_browser: () => boolean;
95
is_compute_server: () => boolean;
96
is_connected: () => boolean;
97
query: Query; // TODO typing
98
query_cancel: Function;
99
is_deleted: (filename: string, project_id: string) => boolean;
100
set_deleted: Function;
101
mark_file: (opts: any) => Promise<void>;
102
set_connected?: Function;
103
version: Function;
104
}
105
106
export const WebappClient = null; // webpack + TS es2020 modules need this
107
108
/*
109
Connection events:
110
- 'connecting' -- trying to establish a connection
111
- 'connected' -- succesfully established a connection; data is the protocol as a string
112
- 'error' -- called when an error occurs
113
- 'output' -- received some output for stateless execution (not in any session)
114
- 'execute_javascript' -- code that server wants client to run (not for a particular session)
115
- 'message' -- emitted when a JSON message is received on('message', (obj) -> ...)
116
- 'data' -- emitted when raw data (not JSON) is received -- on('data, (id, data) -> )...
117
- 'signed_in' -- server pushes a succesful sign in to the client (e.g., due to
118
'remember me' functionality); data is the signed_in message.
119
- 'project_list_updated' -- sent whenever the list of projects owned by this user
120
changed; data is empty -- browser could ignore this unless
121
the project list is currently being displayed.
122
- 'project_data_changed - sent when data about a specific project has changed,
123
e.g., title/description/settings/etc.
124
- 'new_version', number -- sent when there is a new version of the source code so client should refresh
125
*/
126
127
class Client extends EventEmitter implements WebappClient {
128
account_id?: string;
129
stripe: StripeClient;
130
project_collaborators: ProjectCollaborators;
131
support_tickets: SupportTickets;
132
messages: Messages;
133
query_client: QueryClient;
134
time_client: TimeClient;
135
account_client: AccountClient;
136
project_client: ProjectClient;
137
admin_client: AdminClient;
138
openai_client: LLMClient;
139
purchases_client: PurchasesClient;
140
jupyter_client: JupyterClient;
141
sync_client: SyncClient;
142
users_client: UsersClient;
143
file_client: FileClient;
144
tracking_client: TrackingClient;
145
hub_client: HubClient;
146
idle_client: IdleClient;
147
client: Client;
148
149
sync_string: Function;
150
sync_db: Function;
151
152
server_time: Function; // TODO: make this () => Date and deal with the fallout
153
ping_test: Function;
154
get_username: Function;
155
is_signed_in: () => boolean;
156
synctable_project: Function;
157
project_websocket: Function;
158
prettier: Function;
159
exec: Function;
160
touch_project: (project_id: string, compute_server_id?: number) => void;
161
ipywidgetsGetBuffer: (
162
project_id: string,
163
path: string,
164
model_id: string,
165
buffer_path: string,
166
) => Promise<ArrayBuffer>;
167
168
log_error: (any) => void;
169
async_call: AsyncCall;
170
user_tracking: Function;
171
send: Function;
172
call: Function;
173
is_connected: () => boolean;
174
query: typeof QueryClient.prototype.query;
175
query_cancel: Function;
176
177
is_deleted: (filename: string, project_id: string) => boolean;
178
mark_file: (opts: any) => Promise<void>;
179
180
idle_reset: Function;
181
latency: Function;
182
synctable_database: Function;
183
async_query: Function;
184
alert_message: Function;
185
186
constructor() {
187
super();
188
189
if (DEBUG) {
190
this.dbg = this.dbg.bind(this);
191
} else {
192
this.dbg = (..._) => {
193
return (..._) => {};
194
};
195
}
196
197
this.hub_client = bind_methods(new HubClient(this));
198
this.is_signed_in = this.hub_client.is_signed_in.bind(this.hub_client);
199
this.is_connected = this.hub_client.is_connected.bind(this.hub_client);
200
this.call = this.hub_client.call.bind(this.hub_client);
201
this.async_call = this.hub_client.async_call.bind(this.hub_client);
202
this.latency = this.hub_client.latency.bind(this.hub_client);
203
204
this.stripe = bind_methods(new StripeClient(this.call.bind(this)));
205
this.project_collaborators = bind_methods(
206
new ProjectCollaborators(this.async_call.bind(this)),
207
);
208
this.support_tickets = bind_methods(
209
new SupportTickets(this.async_call.bind(this)),
210
);
211
this.messages = new Messages();
212
this.query_client = bind_methods(new QueryClient(this));
213
this.time_client = bind_methods(new TimeClient(this));
214
this.account_client = bind_methods(new AccountClient(this));
215
this.project_client = bind_methods(new ProjectClient(this));
216
217
this.sync_client = bind_methods(new SyncClient(this));
218
this.sync_string = this.sync_client.sync_string;
219
this.sync_db = this.sync_client.sync_db;
220
221
this.admin_client = bind_methods(
222
new AdminClient(this.async_call.bind(this)),
223
);
224
this.openai_client = bind_methods(new LLMClient(this));
225
//this.purchases_client = bind_methods(new PurchasesClient(this));
226
this.purchases_client = bind_methods(new PurchasesClient());
227
this.jupyter_client = bind_methods(
228
new JupyterClient(this.async_call.bind(this)),
229
);
230
this.users_client = bind_methods(
231
new UsersClient(this.call.bind(this), this.async_call.bind(this)),
232
);
233
this.tracking_client = bind_methods(new TrackingClient(this));
234
this.file_client = bind_methods(new FileClient(this.async_call.bind(this)));
235
this.idle_client = bind_methods(new IdleClient(this));
236
237
// Expose a public API as promised by WebappClient
238
this.server_time = this.time_client.server_time.bind(this.time_client);
239
this.ping_test = this.time_client.ping_test.bind(this.time_client);
240
241
this.idle_reset = this.idle_client.idle_reset.bind(this.idle_client);
242
243
this.exec = this.project_client.exec.bind(this.project_client);
244
this.touch_project = this.project_client.touch_project.bind(this.project_client);
245
this.ipywidgetsGetBuffer = this.project_client.ipywidgetsGetBuffer.bind(
246
this.project_client,
247
);
248
249
this.synctable_database = this.sync_client.synctable_database.bind(
250
this.sync_client,
251
);
252
this.synctable_project = this.sync_client.synctable_project.bind(
253
this.sync_client,
254
);
255
256
this.query = this.query_client.query.bind(this.query_client);
257
this.async_query = this.query_client.query.bind(this.query_client);
258
this.query_cancel = this.query_client.cancel.bind(this.query_client);
259
260
this.is_deleted = this.file_client.is_deleted.bind(this.file_client);
261
this.mark_file = this.file_client.mark_file.bind(this.file_client);
262
263
this.alert_message = alert_message;
264
265
// Tweaks the maximum number of listeners an EventEmitter can have --
266
// 0 would mean unlimited
267
// The issue is https://github.com/sagemathinc/cocalc/issues/1098 and
268
// the errors we got are
269
// (node) warning: possible EventEmitter memory leak detected.
270
// 301 listeners added.
271
// Use emitter.setMaxListeners() to increase limit.
272
// every open file/table/sync db listens for connect event, which adds up.
273
this.setMaxListeners(3000);
274
275
// start pinging -- not used/needed for primus,
276
// but *is* needed for getting information about
277
// server_time skew and showing ping time to user.
278
this.once("connected", async () => {
279
this.time_client.ping(true);
280
// Ping again a few seconds after connecting the first time,
281
// after things have settled down a little (to not throw off
282
// ping time).
283
await delay(5000);
284
this.time_client.ping(); // this will ping periodically
285
});
286
287
this.init_prom_client();
288
this.init_global_cocalc();
289
290
bind_methods(this);
291
}
292
293
private async init_global_cocalc(): Promise<void> {
294
await delay(1);
295
setup_global_cocalc(this);
296
}
297
298
private init_prom_client(): void {
299
this.on("start_metrics", start_metrics);
300
}
301
302
public dbg(f): Function {
303
if (log.enabled) {
304
return (...args) => log(new Date().toISOString(), f, ...args);
305
} else {
306
return (..._) => {};
307
}
308
// return function (...m) {
309
// console.log(`${new Date().toISOString()} - Client.${f}: `, ...m);
310
// };
311
}
312
313
public version(): number {
314
return version;
315
}
316
317
// account_id of this client
318
public client_id(): string | undefined {
319
return this.account_id;
320
}
321
322
// false since this client is not a project
323
public is_project(): boolean {
324
return false;
325
}
326
327
public is_browser(): boolean {
328
return true;
329
}
330
331
public is_compute_server(): boolean {
332
return false;
333
}
334
335
// true since this client is a user
336
public is_user(): boolean {
337
return true;
338
}
339
340
public set_deleted(): void {
341
throw Error("not implemented for frontend");
342
}
343
}
344
345
export const webapp_client = new Client();
346
347