Skip to content

Commit 8961fe5

Browse files
Merge branch 'master' into conat-router-peer-discovery
2 parents 24308f2 + 08b40db commit 8961fe5

File tree

26 files changed

+306
-98
lines changed

26 files changed

+306
-98
lines changed

src/packages/backend/conat/test/core/services.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
connect,
99
delay,
1010
} from "@cocalc/backend/conat/test/setup";
11-
import { Client, type Message } from "@cocalc/conat/core/client";
11+
import type { Client, Message } from "@cocalc/conat/core/client";
1212
import { wait } from "@cocalc/backend/conat/test/util";
1313

1414
beforeAll(before);

src/packages/conat/core/client.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -604,10 +604,9 @@ export class Client extends EventEmitter {
604604
);
605605
}
606606
timeout = Math.min(timeout, MAX_INTEREST_TIMEOUT);
607-
const response = await this.conn.timeout(timeout ? timeout : 10000).emitWithAck(
608-
"wait-for-interest",
609-
{ subject, timeout },
610-
);
607+
const response = await this.conn
608+
.timeout(timeout ? timeout : 10000)
609+
.emitWithAck("wait-for-interest", { subject, timeout });
611610
if (response.error) {
612611
throw new ConatError(response.error, { code: response.code });
613612
}
@@ -1083,9 +1082,14 @@ export class Client extends EventEmitter {
10831082
throw Error(`${name} not defined`);
10841083
}
10851084
const result = await f.apply(mesg, args);
1086-
mesg.respondSync(result);
1085+
// use await mesg.respond so waitForInterest is on, which is almost always
1086+
// good for services.
1087+
await mesg.respond(result);
10871088
} catch (err) {
1088-
mesg.respondSync(null, { headers: { error: `${err}` } });
1089+
await mesg.respond(null, {
1090+
noThrow: true, // we're not catching this one
1091+
headers: { error: `${err}` },
1092+
});
10891093
}
10901094
};
10911095
const loop = async () => {

src/packages/conat/files/sync.ts

Lines changed: 64 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ OUTPUT: starts (or keeps running) the filesystem aware side of an editing sessio
2020
2121
*/
2222

23-
// import { type Client } from "@cocalc/conat/core/client";
23+
import type { Client, Message, Subscription } from "@cocalc/conat/core/client";
24+
import { STICKY_QUEUE_GROUP } from "@cocalc/conat/core/client";
25+
import { isValidUUID } from "@cocalc/util/misc";
2426

2527
interface SyncDoc {
2628
close: () => void;
@@ -32,7 +34,6 @@ export type SyncDocCreator = (opts: {
3234
doctype?: any;
3335
}) => SyncDoc;
3436

35-
/*
3637
interface Options {
3738
client: Client;
3839

@@ -43,28 +44,74 @@ interface Options {
4344
createSyncDoc: SyncDocCreator;
4445
}
4546

46-
export function init(opts: Options) {
47-
return new SyncServer(opts.client, opts.projects, opts.createSyncDocs);
47+
export async function init(opts: Options) {
48+
const syncServer = new SyncServer(
49+
opts.client,
50+
opts.projects,
51+
opts.createSyncDoc,
52+
);
53+
await syncServer.init();
54+
return syncServer;
4855
}
4956

5057
interface Api {
5158
open: (opts: { path: string; doctype?: any }) => Promise<void>;
5259
}
5360

5461
class SyncServer {
55-
constructor(client: Client, projects: string, createSyncDoc: SyncDocCreator) {
56-
this.client.service = await client1.service<Api>("arith.*", {
57-
add: async (a, b) => a + b,
58-
mul: async (a, b) => a * b,
59-
// Here we do NOT use an arrow => function and this is
60-
// bound to the calling mesg, which lets us get the subject.
61-
// Because user identity and permissions are done via wildcard
62-
// subjects, having access to the calling message is critical
63-
async open({ path, doctype }) {
64-
const mesg: Message = this as any;
65-
console.log(mesg.subject);
62+
private service?: Subscription;
63+
private syncDocs: { [key: string]: SyncDoc } = {};
64+
private interest: { [key: string]: number } = {};
65+
66+
constructor(
67+
private client: Client,
68+
private projects: string,
69+
private createSyncDoc: SyncDocCreator,
70+
) {}
71+
72+
init = async () => {
73+
const self = this;
74+
this.service = await this.client.service<Api>(
75+
"sync.*.open",
76+
{
77+
async open({ path, doctype }) {
78+
const mesg: Message = this as any;
79+
self.open(mesg.subject, path, doctype);
80+
},
6681
},
82+
{ queue: STICKY_QUEUE_GROUP },
83+
);
84+
};
85+
86+
private key = (project_id, path) => {
87+
return `${project_id}/${path}`;
88+
};
89+
90+
private open = (subject: string, path: string, doctype) => {
91+
const project_id = subject.split(".")[1]?.slice("project-".length);
92+
console.log("open", {
93+
subject,
94+
path,
95+
doctype,
96+
project_id,
97+
projects: this.projects,
6798
});
68-
}
99+
if (!isValidUUID(project_id)) {
100+
throw Error("invalid subject");
101+
}
102+
const key = this.key(project_id, path);
103+
if (this.syncDocs[key] === undefined) {
104+
this.syncDocs[key] = this.createSyncDoc({ project_id, path, doctype });
105+
}
106+
this.interest[key] = Date.now();
107+
};
108+
109+
close = () => {
110+
this.service?.close();
111+
delete this.service;
112+
for (const key in this.syncDocs) {
113+
this.syncDocs[key].close();
114+
delete this.syncDocs[key];
115+
}
116+
};
69117
}
70-
*/

src/packages/conat/hub/api/system.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const system = {
2323
deletePassport: authFirst,
2424

2525
adminSalesloftSync: authFirst,
26+
userSalesloftSync: authFirst,
2627
};
2728

2829
export interface System {
@@ -103,6 +104,8 @@ export interface System {
103104
account_ids: string[];
104105
}) => Promise<void>;
105106

107+
userSalesloftSync: (opts: { account_id?: string }) => Promise<void>;
108+
106109
sendEmailVerification: (opts: {
107110
account_id?: string;
108111
only_verify?: boolean;

src/packages/conat/sync/pubsub.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export class PubSub extends EventEmitter {
5353
};
5454

5555
set = (obj) => {
56-
this.client.publish(this.subject, obj);
56+
this.client.publishSync(this.subject, obj);
5757
};
5858

5959
private subscribe = async () => {

src/packages/frontend/account/actions.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,43 @@ export class AccountActions extends Actions<AccountState> {
244244
// @ts-ignore
245245
this.setState({ fragment });
246246
};
247+
248+
addTag = async (tag: string) => {
249+
const store = this.redux.getStore("account");
250+
if (!store) return;
251+
const tags = store.get("tags");
252+
if (tags?.includes(tag)) {
253+
// already tagged
254+
return;
255+
}
256+
const table = this.redux.getTable("account");
257+
if (!table) return;
258+
const v = tags?.toJS() ?? [];
259+
v.push(tag);
260+
table.set({ tags: v });
261+
try {
262+
await webapp_client.conat_client.hub.system.userSalesloftSync({});
263+
} catch (err) {
264+
console.warn(
265+
"WARNING: issue syncing with salesloft after setting tag",
266+
tag,
267+
err,
268+
);
269+
}
270+
};
271+
272+
// delete won't be visible in frontend until a browser refresh...
273+
deleteTag = async (tag: string) => {
274+
const store = this.redux.getStore("account");
275+
if (!store) return;
276+
const tags = store.get("tags");
277+
if (!tags?.includes(tag)) {
278+
// already tagged
279+
return;
280+
}
281+
const table = this.redux.getTable("account");
282+
if (!table) return;
283+
const v = tags.toJS().filter((x) => x != tag);
284+
await webapp_client.async_query({ query: { accounts: { tags: v } } });
285+
};
247286
}

src/packages/frontend/account/table.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class AccountTable extends Table {
4747
ssh_keys: null,
4848
created: null,
4949
unlisted: null,
50-
//tags: null,
50+
tags: null,
5151
tours: null,
5252
purchase_closing_day: null,
5353
email_daily_statements: null,

src/packages/frontend/app/query-params.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,5 @@ export function init_query_params(): void {
7979
// not have session in the URL, so we can share url's without infected
8080
// other user's session.
8181
QueryParams.remove("session");
82+
8283
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
Do something somewhat friendly when a user signs in for the first time,
3+
either after creating an account or being signed out.
4+
5+
For now:
6+
7+
- ensure they are a collab on at least one project
8+
- open the most recent project they actively used and show the +New page
9+
10+
That's it for now.
11+
*/
12+
13+
import { delay } from "awaiting";
14+
import { redux } from "@cocalc/frontend/app-framework";
15+
import { once } from "@cocalc/util/async-utils";
16+
import { webapp_client } from "@cocalc/frontend/webapp-client";
17+
import { cmp } from "@cocalc/util/misc";
18+
import { QueryParams } from "@cocalc/frontend/misc/query-params";
19+
20+
export default async function signInAction() {
21+
const signIn = QueryParams.get("sign-in");
22+
if (signIn == null) {
23+
return;
24+
}
25+
QueryParams.remove("sign-in");
26+
await delay(1); // so projects store is created (not in sync initial load loop)
27+
const project_id = await getProject();
28+
const actions = redux.getActions("projects");
29+
actions.open_project({ project_id, switch_to: true, target: "new" });
30+
await actions.start_project(project_id);
31+
}
32+
33+
async function create(title = "My First Project") {
34+
const project_id = await webapp_client.project_client.create({
35+
title,
36+
description: "",
37+
});
38+
const projects = redux.getStore("projects");
39+
// wait until projects_map is loaded, so we know what projects the users has (probably)
40+
while (projects.getIn(["project_map", project_id]) == null) {
41+
await once(projects, "change");
42+
}
43+
return project_id;
44+
}
45+
46+
async function getProject(): Promise<string> {
47+
const projects = redux.getStore("projects");
48+
// wait until projects_map is loaded, so we know what projects the users has (probably)
49+
while (projects.get("project_map") == null) {
50+
await once(projects, "change");
51+
}
52+
const account = redux.getStore("account");
53+
while (account.get("created") == null) {
54+
await once(account, "change");
55+
}
56+
57+
const created = account.get("created");
58+
let project_map = projects.get("project_map")!;
59+
if (project_map.size == 0) {
60+
// no known projects -- could be a new account, or could be an old account and no *recent* projects
61+
if (
62+
(created?.valueOf() ?? Date.now()) >=
63+
Date.now() - 2 * 24 * 60 * 60 * 1000
64+
) {
65+
// new account -- make a project
66+
return await create("My First Project");
67+
} else {
68+
// old account but no projects -- try loading all.
69+
const projectActions = redux.getActions("projects");
70+
await projectActions.load_all_projects();
71+
project_map = projects.get("project_map")!;
72+
if (project_map.size == 0) {
73+
// still nothing -- just create
74+
return await create();
75+
}
76+
}
77+
}
78+
79+
const account_id = account.get("account_id");
80+
81+
// now there should be at least one project in project_map.
82+
// Is there a non-deleted non-hidden project?
83+
const options: any[] = [];
84+
for (const [_, project] of project_map) {
85+
if (project.get("deleted")) {
86+
continue;
87+
}
88+
if (project.getIn(["users", account_id, "hide"])) {
89+
continue;
90+
}
91+
options.push(project);
92+
}
93+
if (options.length == 0) {
94+
return await create();
95+
}
96+
97+
// Sort the projects by when YOU were last active on the project, or if you were
98+
// never active on any project, by when the projects was last_edited.
99+
const usedByYou = options.filter((x) => x.getIn(["last_active", account_id]));
100+
101+
if (usedByYou.length == 0) {
102+
// you were never active on any project, so just return project most recently edited
103+
options.sort((x, y) => -cmp(x.get("last_edited"), y.get("last_edited")));
104+
return options[0].get("project_id");
105+
}
106+
107+
usedByYou.sort(
108+
(x, y) =>
109+
-cmp(
110+
x.getIn(["last_active", account_id]),
111+
y.getIn(["last_active", account_id]),
112+
),
113+
);
114+
return usedByYou[0].get("project_id");
115+
}

src/packages/frontend/components/sortable-tabs.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@ export function renderTabBar(tabBarProps, DefaultTabBar, styles?) {
156156
return (
157157
<DefaultTabBar {...tabBarProps}>
158158
{(node) => (
159-
<SortableTab key={node.key} id={node.key} style={styles?.[node.key]}>
159+
<SortableTab
160+
key={node.key}
161+
id={node.key}
162+
style={styles?.[node.key] ?? styles?.[""]}
163+
>
160164
{node}
161165
</SortableTab>
162166
)}

0 commit comments

Comments
 (0)