Skip to content

Commit 346a484

Browse files
committed
Merge branch 'master' into fs2
2 parents d663508 + 612d127 commit 346a484

28 files changed

+500
-241
lines changed

src/.claude/settings.local.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(pnpm tsc:*)",
5+
"Bash(pnpm build:*)",
6+
"Bash(git add:*)",
7+
"Bash(git commit:*)"
8+
],
9+
"deny": []
10+
}
11+
}

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/packages/conat/core/server.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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
@@ -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
}
@@ -1791,7 +1802,9 @@ export function updateSticky(update: StickyUpdate, sticky: Sticky): boolean {
17911802
function getServerAddress(options: Options) {
17921803
const port = options.port;
17931804
const path = options.path?.slice(0, -"/conat".length) ?? "";
1794-
return `http${options.ssl || port == 443 ? "s" : ""}://${options.clusterIpAddress ?? "localhost"}:${port}${path}`;
1805+
return `http${options.ssl || port == 443 ? "s" : ""}://${
1806+
options.clusterIpAddress ?? "localhost"
1807+
}:${port}${path}`;
17951808
}
17961809

17971810
/*

src/packages/conat/monitor/usage.ts

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import json from "json-stable-stringify";
21
import { EventEmitter } from "events";
3-
import type { JSONValue } from "@cocalc/util/types";
4-
import { ConatError } from "@cocalc/conat/core/client";
2+
import json from "json-stable-stringify";
3+
54
import { getLogger } from "@cocalc/conat/client";
5+
import { ConatError } from "@cocalc/conat/core/client";
6+
import type { JSONValue } from "@cocalc/util/types";
7+
import { Metrics } from "../types";
68

79
const logger = getLogger("monitor:usage");
810

@@ -17,6 +19,9 @@ export class UsageMonitor extends EventEmitter {
1719
private options: Options;
1820
private total = 0;
1921
private perUser: { [user: string]: number } = {};
22+
// metrics will be picked up periodically and exposed via e.g. prometheus
23+
private countDeny = 0;
24+
private metrics: Metrics = {};
2025

2126
constructor(options: Options) {
2227
super();
@@ -38,27 +43,53 @@ export class UsageMonitor extends EventEmitter {
3843

3944
private initLogging = () => {
4045
const { log } = this.options;
41-
if (log == null) {
42-
return;
43-
}
46+
47+
// Record metrics for all events (even if logging is disabled)
4448
this.on("total", (total, limit) => {
45-
log("usage", this.options.resource, { total, limit });
49+
this.metrics["total:count"] = total;
50+
this.metrics["total:limit"] = limit;
51+
if (log) {
52+
log("usage", this.options.resource, { total, limit });
53+
}
4654
});
4755
this.on("add", (user, count, limit) => {
48-
log("usage", this.options.resource, "add", { user, count, limit });
56+
// this.metrics["add:count"] = count;
57+
// this.metrics["add:limit"] = limit;
58+
if (log) {
59+
log("usage", this.options.resource, "add", { user, count, limit });
60+
}
4961
});
5062
this.on("delete", (user, count, limit) => {
51-
log("usage", this.options.resource, "delete", { user, count, limit });
63+
// this.metrics["delete:count"] = count;
64+
// this.metrics["delete:limit"] = limit;
65+
if (log) {
66+
log("usage", this.options.resource, "delete", { user, count, limit });
67+
}
5268
});
5369
this.on("deny", (user, limit, type) => {
54-
log("usage", this.options.resource, "not allowed due to hitting limit", {
55-
type,
56-
user,
57-
limit,
58-
});
70+
this.countDeny += 1;
71+
this.metrics["deny:count"] = this.countDeny;
72+
this.metrics["deny:limit"] = limit;
73+
if (log) {
74+
log(
75+
"usage",
76+
this.options.resource,
77+
"not allowed due to hitting limit",
78+
{
79+
type,
80+
user,
81+
limit,
82+
},
83+
);
84+
}
5985
});
6086
};
6187

88+
// we return a copy
89+
getMetrics = () => {
90+
return { ...this.metrics };
91+
};
92+
6293
add = (user: JSONValue) => {
6394
const u = this.toJson(user);
6495
let count = this.perUser[u] ?? 0;

src/packages/conat/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export interface Location {
99

1010
path?: string;
1111
}
12+
13+
type EventType = "total" | "add" | "delete" | "deny";
14+
type ValueType = "count" | "limit";
15+
type MetricKey = `${EventType}:${ValueType}`;
16+
export type Metrics = { [K in MetricKey]?: number };

src/packages/file-server/btrfs/subvolume.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ export class Subvolume {
159159
await sudo({ command: "chmod", args: ["a-w", this.snapshotsDir] });
160160
};
161161

162-
createSnapshot = async (name: string) => {
162+
createSnapshot = async (name?: string) => {
163+
name ??= new Date().toISOString();
163164
logger.debug("createSnapshot", { name, subvolume: this.name });
164165
await this.makeSnapshotsDir();
165166
await sudo({
@@ -293,17 +294,26 @@ export class Subvolume {
293294
};
294295

295296
bupRestore = async (path: string) => {
297+
// path -- branch/revision/path/to/dir
298+
if (path.startsWith("/")) {
299+
path = path.slice(1);
300+
}
296301
path = normalize(path);
297-
// outdir -- path relative to subvolume
298-
// path -- /branch/revision/path/to/dir
302+
// ... but to avoid potential data loss, we make a snapshot before deleting it.
303+
await this.createSnapshot();
304+
const i = path.indexOf("/"); // remove the commit name
305+
await sudo({
306+
command: "rm",
307+
args: ["-rf", join(this.path, path.slice(i + 1))],
308+
});
299309
await sudo({
300310
command: "bup",
301311
args: [
302312
"-d",
303313
this.filesystem.bup,
304314
"restore",
305315
"-C",
306-
this.path, //join(this.path, outdir),
316+
this.path,
307317
join(`/${this.name}`, path),
308318
"--quiet",
309319
],

src/packages/file-server/btrfs/test/setup.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ import { chmod, mkdtemp, mkdir, rm } from "node:fs/promises";
77
import { tmpdir } from "node:os";
88
import { join } from "path";
99
import { until } from "@cocalc/util/async-utils";
10-
export { sudo } from "../util";
10+
import { sudo } from "../util";
11+
export { sudo };
1112
export { delay } from "awaiting";
1213

1314
export let fs: Filesystem;
1415
let tempDir;
1516

17+
const TEMP_PREFIX = "cocalc-test-btrfs-";
18+
1619
export async function before() {
17-
tempDir = await mkdtemp(join(tmpdir(), "cocalc-test-btrfs-"));
20+
try {
21+
const command = `umount ${join(tmpdir(), TEMP_PREFIX)}*/mnt`;
22+
// attempt to unmount any mounts left from previous runs
23+
await sudo({ command, bash: true });
24+
} catch {}
25+
tempDir = await mkdtemp(join(tmpdir(), TEMP_PREFIX));
1826
// Set world read/write/execute
1927
await chmod(tempDir, 0o777);
2028
const mount = join(tempDir, "mnt");

0 commit comments

Comments
 (0)