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