Skip to content

Commit 2ee721d

Browse files
Merge branch 'master' into i18n-pt_br-eu
2 parents a0129dd + 92554d0 commit 2ee721d

File tree

121 files changed

+6072
-7071
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

121 files changed

+6072
-7071
lines changed

src/.claude/settings.local.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm tsc:*)",
5+
"Bash(pnpm build:*)",
6+
"Bash(git add:*)",
7+
"Bash(git commit:*)",
8+
"Bash(node:*)",
9+
"Bash(grep:*)",
10+
"Bash(find:*)",
11+
"WebFetch(domain:github.com)",
12+
"WebFetch(domain:cocalc.com)",
13+
"WebFetch(domain:doc.cocalc.com)",
14+
"Bash(npm show:*)",
15+
"Bash(prettier -w:*)",
16+
"Bash(npx tsc:*)",
17+
"Bash(gh pr view:*)",
18+
"Bash(gh:*)"
19+
],
20+
"deny": []
21+
}
22+
}

src/CLAUDE.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
# CoCalc Source Repository
6+
7+
* This is the source code of CoCalc in a Git repository
8+
* It is a complex JavaScript/TypeScript SaaS application
9+
* CoCalc is organized as a monorepository (multi-packages) in the subdirectory "./packages"
10+
* The packages are managed as a pnpm workspace in "./packages/pnpm-workspace.yaml"
11+
12+
## Code Style
13+
14+
- Everything is written in TypeScript code
15+
- Indentation: 2-spaces
16+
- All .js and .ts files are formatted by the tool prettier
17+
- Add suitable types when you write code
18+
- Variable name styles are "camelCase" for local and "FOO_BAR" for global variables. If you edit older code not following these guidlines, adjust this rule to fit the files style.
19+
- Some older code is JavaScript or CoffeeScript, which will be translated to TypeScript
20+
- Use ES modules (import/export) syntax, not CommonJS (require)
21+
- Organize the list of imports in such a way: installed npm packages are on top, newline, then are imports from @cocalc's code base. Sorted alphabetically.
22+
23+
## Development Commands
24+
25+
### Essential Commands
26+
- `pnpm build-dev` - Build all packages for development
27+
- `pnpm clean` - Clean all node_modules and dist directories
28+
- `pnpm database` - Start PostgreSQL database server
29+
- `pnpm hub` - Start the main hub server
30+
- `pnpm psql` - Connect to the PostgreSQL database
31+
- `pnpm test` - Run full test suite
32+
- `pnpm test-parallel` - Run tests in parallel across packages
33+
- `pnpm depcheck` - Check for dependency issues
34+
35+
### Package-Specific Commands
36+
- `cd packages/[package] && pnpm tsc` - Watch TypeScript compilation for a specific package
37+
- `cd packages/[package] && pnpm test` - Run tests for a specific package
38+
- `cd packages/[package] && pnpm build` - Build a specific package
39+
40+
### Development Setup
41+
1. Start database: `pnpm database`
42+
2. Start hub: `pnpm hub`
43+
3. For TypeScript changes, run `pnpm tsc` in the relevant package directory
44+
45+
## Architecture Overview
46+
47+
### Package Structure
48+
CoCalc is organized as a monorepo with key packages:
49+
50+
- **frontend** - React/TypeScript frontend application using Redux-style stores and actions
51+
- **backend** - Node.js backend services and utilities
52+
- **hub** - Main server orchestrating the entire system
53+
- **database** - PostgreSQL database layer with queries and schema
54+
- **util** - Shared utilities and types used across packages
55+
- **comm** - Communication layer including WebSocket types
56+
- **conat** - CoCalc's container/compute orchestration system
57+
- **sync** - Real-time synchronization system for collaborative editing
58+
- **project** - Project-level services and management
59+
- **static** - Static assets and build configuration
60+
- **next** - Next.js server components
61+
62+
### Key Architectural Patterns
63+
64+
#### Frontend Architecture
65+
- **Redux-style State Management**: Uses custom stores and actions pattern (see `packages/frontend/app-framework/actions-and-stores.ts`)
66+
- **TypeScript React Components**: All frontend code is TypeScript with proper typing
67+
- **Modular Store System**: Each feature has its own store/actions (AccountStore, BillingStore, etc.)
68+
- **WebSocket Communication**: Real-time communication with backend via WebSocket messages
69+
70+
#### Backend Architecture
71+
- **PostgreSQL Database**: Primary data store with sophisticated querying system
72+
- **WebSocket Messaging**: Real-time communication between frontend and backend
73+
- **Conat System**: Container orchestration for compute servers
74+
- **Event-Driven Architecture**: Extensive use of EventEmitter patterns
75+
- **Microservice-like Packages**: Each package handles specific functionality
76+
77+
#### Communication Patterns
78+
- **WebSocket Messages**: Primary communication method (see `packages/comm/websocket/types.ts`)
79+
- **Database Queries**: Structured query system with typed interfaces
80+
- **Event Emitters**: Inter-service communication within backend
81+
- **REST-like APIs**: Some HTTP endpoints for specific operations
82+
83+
### Key Technologies
84+
- **TypeScript**: Primary language for all new code
85+
- **React**: Frontend framework
86+
- **PostgreSQL**: Database
87+
- **Node.js**: Backend runtime
88+
- **WebSockets**: Real-time communication
89+
- **pnpm**: Package manager and workspace management
90+
- **Jest**: Testing framework
91+
- **SASS**: CSS preprocessing
92+
93+
### Database Schema
94+
- Comprehensive schema in `packages/util/db-schema`
95+
- Query abstractions in `packages/database/postgres/`
96+
- Type-safe database operations with TypeScript interfaces
97+
98+
### Testing
99+
- **Jest**: Primary testing framework
100+
- **ts-jest**: TypeScript support for Jest
101+
- **jsdom**: Browser environment simulation for frontend tests
102+
- Test files use `.test.ts` or `.spec.ts` extensions
103+
- Each package has its own jest.config.js
104+
105+
### Import Patterns
106+
- Use absolute imports with `@cocalc/` prefix for cross-package imports
107+
- Example: `import { cmp } from "@cocalc/util/misc"`
108+
- Type imports: `import type { Foo } from "./bar"`
109+
- Destructure imports when possible
110+
111+
### Development Workflow
112+
1. Changes to TypeScript require compilation (`pnpm tsc` in relevant package)
113+
2. Database must be running before starting hub
114+
3. Hub coordinates all services and should be restarted after changes
115+
4. Use `pnpm clean && pnpm build-dev` when switching branches or after major changes
116+
117+
# Workflow
118+
- Be sure to typecheck when you're done making a series of code changes
119+
- Prefer running single tests, and not the whole test suite, for performance
120+
121+
## Git Workflow
122+
123+
- Prefix git commits with the package and general area. e.g. 'frontend/latex: ...' if it concerns latex editor changes in the packages/frontend/... code.
124+
- When pushing a new branch to Github, track it upstream. e.g. `git push --set-upstream origin feature-foo` for branch "feature-foo".
125+
126+
# important-instruction-reminders
127+
- Do what has been asked; nothing more, nothing less.
128+
- NEVER create files unless they're absolutely necessary for achieving your goal.
129+
- ALWAYS prefer editing an existing file to creating a new one.
130+
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.

src/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@
1818
"version-check": "pip3 install typing_extensions mypy || pip3 install --break-system-packages typing_extensions mypy && ./workspaces.py version-check && mypy scripts/check_npm_packages.py",
1919
"test-parallel": "unset DEBUG && pnpm run version-check && cd packages && pnpm run -r --parallel test",
2020
"test": "unset DEBUG && pnpm run depcheck && pnpm run version-check && ./workspaces.py test",
21-
"test-github-ci": "unset DEBUG && pnpm run depcheck && pnpm run version-check && ./workspaces.py test --exclude=jupyter --retries=1",
21+
"test-github-ci": "unset DEBUG && pnpm run depcheck && pnpm run version-check && ./workspaces.py test --exclude=jupyter,file-server --retries=1",
2222
"depcheck": "cd packages && pnpm run -r --parallel depcheck",
2323
"prettier-all": "cd packages/",
2424
"local-ci": "./scripts/ci.sh",
25-
"conat-server": "cd packages/server && pnpm conat-server",
2625
"conat-connections": "cd packages/backend && pnpm conat-connections",
2726
"conat-watch": "cd packages/backend && pnpm conat-watch",
2827
"conat-inventory": "cd packages/backend && pnpm conat-inventory"

src/packages/backend/get-listing.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
/*
7-
Server directory listing through the HTTP server and Websocket API.
7+
This is used by backends to serve directory listings to clients:
88
99
{files:[..., {size:?,name:?,mtime:?,isdir:?}]}
1010

src/packages/backend/logger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ function initTransports() {
128128
// Similar as in debug source code, except I stuck a timestamp
129129
// at the beginning, which I like... except also aware of
130130
// non-printf formatting.
131-
const line = `${new Date().toISOString()}: ${myFormat(...args)}\n`;
131+
const line = `${new Date().toISOString()} (${process.pid}):${myFormat(...args)}\n`;
132132

133133
if (transports.console) {
134134
// the console transport:

src/packages/conat/core/client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,10 @@ export class Client extends EventEmitter {
510510
reconnection: true,
511511
});
512512

513-
this.conn.on("info", (info) => {
513+
this.conn.on("info", (info, ack) => {
514+
if (typeof ack == "function") {
515+
ack();
516+
}
514517
const firstTime = this.info == null;
515518
this.info = info;
516519
this.emit("info", info);

src/packages/conat/core/server.ts

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {
5353
import { Patterns } from "./patterns";
5454
import { is_array } from "@cocalc/util/misc";
5555
import { UsageMonitor } from "@cocalc/conat/monitor/usage";
56-
import { once } from "@cocalc/util/async-utils";
56+
import { once, until } from "@cocalc/util/async-utils";
5757
import {
5858
clusterLink,
5959
type ClusterLink,
@@ -74,6 +74,7 @@ import { type SysConatServer, sysApiSubject, sysApi } from "./sys";
7474
import { forkedConatServer } from "./start-server";
7575
import { stickyChoice } from "./sticky";
7676
import { EventEmitter } from "events";
77+
import { Metrics } from "../types";
7778

7879
const logger = getLogger("conat:core:server");
7980

@@ -310,6 +311,10 @@ export class ConatServer extends EventEmitter {
310311
});
311312
};
312313

314+
public getUsage = (): Metrics => {
315+
return this.usage.getMetrics();
316+
};
317+
313318
// this is for the Kubernetes health check -- I haven't
314319
// thought at all about what to do here, really.
315320
// Hopefully experience can teach us.
@@ -602,7 +607,9 @@ export class ConatServer extends EventEmitter {
602607
return;
603608
}
604609
if (!(await this.isAllowed({ user, subject, type: "sub" }))) {
605-
const message = `permission denied subscribing to '${subject}' from ${JSON.stringify(user)}`;
610+
const message = `permission denied subscribing to '${subject}' from ${JSON.stringify(
611+
user,
612+
)}`;
606613
this.log(message);
607614
throw new ConatError(message, {
608615
code: 403,
@@ -706,7 +713,9 @@ export class ConatServer extends EventEmitter {
706713
}
707714

708715
if (!(await this.isAllowed({ user: from, subject, type: "pub" }))) {
709-
const message = `permission denied publishing to '${subject}' from ${JSON.stringify(from)}`;
716+
const message = `permission denied publishing to '${subject}' from ${JSON.stringify(
717+
from,
718+
)}`;
710719
this.log(message);
711720
throw new ConatError(message, {
712721
// this is the http code for permission denied, and having this
@@ -931,7 +940,7 @@ export class ConatServer extends EventEmitter {
931940
this.subscriptions[id] = new Set<string>();
932941
}
933942

934-
socket.emit("info", { ...this.info(), user });
943+
this.sendInfo(socket, user);
935944

936945
socket.on("stats", ({ recv0 }) => {
937946
const s = this.stats[socket.id];
@@ -950,7 +959,9 @@ export class ConatServer extends EventEmitter {
950959
return;
951960
}
952961
if (!(await this.isAllowed({ user, subject, type: "pub" }))) {
953-
const message = `permission denied waiting for interest in '${subject}' from ${JSON.stringify(user)}`;
962+
const message = `permission denied waiting for interest in '${subject}' from ${JSON.stringify(
963+
user,
964+
)}`;
954965
this.log(message);
955966
respond({ error: message, code: 403 });
956967
}
@@ -1075,6 +1086,38 @@ export class ConatServer extends EventEmitter {
10751086
});
10761087
};
10771088

1089+
sendInfo = async (socket, user) => {
1090+
// we send info with an ack because I think sometimes the initial "info"
1091+
// message gets dropped, leaving a broken hanging connection that never
1092+
// does anything (until the user explicitly refreshes their browser).
1093+
// I did see what is probably this in production frequently.
1094+
try {
1095+
await until(
1096+
async () => {
1097+
if (!socket.conn?.readyState.startsWith("o")) {
1098+
// logger.debug(`failed to send "info" message to ${socket.id}`);
1099+
// readyState not defined or not opened or opening, so connection must
1100+
// have been closed before success.
1101+
return true;
1102+
}
1103+
try {
1104+
await socket
1105+
.timeout(7500)
1106+
.emitWithAck("info", { ...this.info(), user });
1107+
return true;
1108+
} catch (err) {
1109+
// logger.debug(`error sending "info" message to ${socket.id}`, err);
1110+
return false;
1111+
}
1112+
},
1113+
{ min: 5000, max: 30000, timeout: 120_000 },
1114+
);
1115+
} catch {
1116+
// never ack'd "info" after a few minutes -- could just be an old client,
1117+
// so don't do anything at this point.
1118+
}
1119+
};
1120+
10781121
address = () => getServerAddress(this.options);
10791122

10801123
// create new client in the same process connected to this server.
@@ -1338,8 +1381,8 @@ export class ConatServer extends EventEmitter {
13381381
const usage = async () => {
13391382
return { [this.id]: this.usage.stats() };
13401383
};
1341-
// user has to explicitly refresh there browser after
1342-
// being disconnected this way
1384+
// user has to explicitly refresh their browser after
1385+
// being disconnected this way:
13431386
const disconnect = async (ids: string | string[]) => {
13441387
if (typeof ids == "string") {
13451388
ids = [ids];
@@ -1791,7 +1834,9 @@ export function updateSticky(update: StickyUpdate, sticky: Sticky): boolean {
17911834
function getServerAddress(options: Options) {
17921835
const port = options.port;
17931836
const path = options.path?.slice(0, -"/conat".length) ?? "";
1794-
return `http${options.ssl || port == 443 ? "s" : ""}://${options.clusterIpAddress ?? "localhost"}:${port}${path}`;
1837+
return `http${options.ssl || port == 443 ? "s" : ""}://${
1838+
options.clusterIpAddress ?? "localhost"
1839+
}:${port}${path}`;
17951840
}
17961841

17971842
/*

0 commit comments

Comments
 (0)