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.

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