Skip to content

Commit 5dd64cd

Browse files
committed
Merge remote-tracking branch 'origin/master' into explorer-merge-search-terminal
2 parents ac186cc + 3ea9b0b commit 5dd64cd

40 files changed

+1077
-128
lines changed

src/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"nats-server-verbose": "cd ${COCALC_ROOT:=$INIT_CWD}/packages/backend && node -e \"require('@cocalc/backend/nats/install').main()\" && node -e \"require('@cocalc/backend/nats/conf').main()\" && node -e \"require('@cocalc/backend/nats/server').main({verbose:true})\"",
2626
"nats-cli": "cd ${COCALC_ROOT:=$INIT_CWD}/packages/backend && node -e \"require('@cocalc/backend/nats/cli').main()\"",
2727
"nats-sys": "cd ${COCALC_ROOT:=$INIT_CWD}/packages/backend && node -e \"require('@cocalc/backend/nats/cli').main({user:'sys'})\"",
28+
"nats-tiered-storage": "cd ${COCALC_ROOT:=$INIT_CWD}/packages/server && DEBUG=cocalc:* DEBUG_CONSOLE=yes node -e \"require('@cocalc/server/nats/tiered-storage').init()\"",
2829
"local-ci": "./scripts/ci.sh"
2930
},
3031
"repository": {

src/packages/backend/nats/cli.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ import { join } from "path";
1010
import { spawnSync } from "node:child_process";
1111
import { natsServerUrl } from "./conf";
1212

13+
const natsBin = join(data, "nats", "bin");
14+
1315
export function natsCoCalcUserEnv({ user = natsUser }: { user?: string } = {}) {
1416
return {
1517
NATS_URL: natsServerUrl,
1618
NATS_PASSWORD: natsPassword,
1719
NATS_USER: user ?? natsUser,
20+
PATH: `${natsBin}:${process.env.PATH}`,
1821
};
1922
}
2023

@@ -37,17 +40,16 @@ function params({ user }) {
3740

3841
export function main({ user = natsUser }: { user?: string } = {}) {
3942
let { command, args, env } = params({ user });
40-
const PATH0 = join(data, "nats", "bin");
4143
console.log("# Use CoCalc config of NATS (nats and nsc) via this subshell:");
4244
console.log(
4345
JSON.stringify(
44-
{ ...env, NATS_PASSWORD: "xxx", PATH: PATH0 + ":..." },
46+
{ ...env, NATS_PASSWORD: "xxx", PATH: natsBin + ":..." },
4547
undefined,
4648
2,
4749
),
4850
);
4951
spawnSync(command, args, {
50-
env: { ...env, PATH: `${PATH0}:${process.env.PATH}` },
52+
env: { ...env, PATH: `${natsBin}:${process.env.PATH}` },
5153
stdio: "inherit",
5254
});
5355
}

src/packages/frontend/client/console.ts

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export function setup_global_cocalc(client): void {
4545

4646
const cocalc: any = window.cc ?? {};
4747
cocalc.client = client;
48+
cocalc.nats = client.nats_client;
4849
cocalc.misc = require("@cocalc/util/misc");
4950
cocalc.immutable = require("immutable");
5051
cocalc.done = cocalc.misc.done;

src/packages/frontend/cspell.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
"isdir",
5555
"mintime",
5656
"PoweroffOutlined",
57-
"immutablejs"
57+
"immutablejs",
58+
"reuseinflight"
5859
],
5960
"flagWords": [],
6061
"ignorePaths": ["node_modules/**", "dist/**", "dist-ts/**", "build/**"],

src/packages/frontend/customize.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ export interface CustomizeState {
177177
selectable_llms: List<string>;
178178
default_llm?: string;
179179
user_defined_llm: boolean;
180+
llm_default_quota?: number;
180181

181182
insecure_test_mode?: boolean;
182183

@@ -334,7 +335,7 @@ function process_customize(obj) {
334335
for (const k in site_settings_conf) {
335336
const v = site_settings_conf[k];
336337
obj[k] =
337-
obj[k] != null ? obj[k] : (v.to_val?.(v.default, obj_orig) ?? v.default);
338+
obj[k] != null ? obj[k] : v.to_val?.(v.default, obj_orig) ?? v.default;
338339
}
339340
// the llm markup special case
340341
obj.llm_markup = obj_orig._llm_markup ?? 30;

src/packages/frontend/frame-editors/latex-editor/actions.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -664,16 +664,24 @@ export class Actions extends BaseActions<LatexEditorState> {
664664
}
665665

666666
public async explicit_save() {
667-
const account: any = this.redux.getStore("account");
667+
const account = this.redux.getStore("account");
668668
if (
669-
account == null ||
670-
!account.getIn(["editor_settings", "build_on_save"]) ||
669+
!account?.getIn(["editor_settings", "build_on_save"]) ||
671670
!this.is_likely_master()
672671
) {
673-
await this.save_all(true);
672+
// kicks off a save of all relevant files
673+
// Obviously, do not make this save_all(true), because
674+
// that would end up calling this very function again
675+
// crashing the browser in an INFINITE RECURSSION
676+
// (this was a bug for a while!).
677+
// Also, the save of the related files is NOT
678+
// explicit -- the user is only explicitly saving this
679+
// file. Explicit save is mainly about deleting trailing
680+
// whitespace and launching builds.
681+
await this.save_all(false);
674682
return;
675683
}
676-
await this.build(); // kicks off a save of all relevant files
684+
await this.build();
677685
}
678686

679687
// used by generic framework – this is bound to the instance, otherwise "this" is undefined, hence

src/packages/frontend/frame-editors/terminal-editor/nats-terminal-connection.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ export class NatsTerminalConnection extends EventEmitter {
236236
private setReady = async () => {
237237
// wait until after render loop of terminal before allowing writing,
238238
// or we get corruption.
239-
await delay(1);
239+
await delay(250);
240240
this.setState("running");
241241
this.emit("ready");
242242
};

src/packages/frontend/nats/client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import Cookies from "js-cookie";
6060
import { ACCOUNT_ID_COOKIE } from "@cocalc/frontend/client/client";
6161
import { isConnected, waitUntilConnected } from "@cocalc/nats/util";
6262
import { info as refCacheInfo } from "@cocalc/util/refcache";
63+
import * as tieredStorage from "@cocalc/nats/tiered-storage/client";
6364

6465
const NATS_STATS_INTERVAL = 2500;
6566

@@ -673,6 +674,8 @@ export class NatsClient extends EventEmitter {
673674
waitUntilConnected = async () => await waitUntilConnected();
674675

675676
refCacheInfo = () => refCacheInfo();
677+
678+
tieredStorage = tieredStorage;
676679
}
677680

678681
function setDeleted({ project_id, path, deleted }) {

src/packages/frontend/project/explorer/file-listing/no-files.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default function NoFiles({
106106
margin: "0 auto",
107107
height: "80px",
108108
fontSize: "24px",
109+
padding: "30px",
109110
}}
110111
onClick={(): void => {
111112
if (file_search.length === 0) {

src/packages/frontend/purchases/all-quotas-config.tsx

+9-4
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ export default function AllQuotasConfig() {
5353
const lastFetchedQuotasRef = useRef<ServiceQuota[] | null>(null);
5454
const [changed, setChanged] = useState<boolean>(false);
5555
const selectableLLMs = useTypedRedux("customize", "selectable_llms");
56+
// The 10 is just for the initial rollout, the value is customizable
57+
const llm_default_quota =
58+
useTypedRedux("customize", "llm_default_quota") ?? 10;
5659

5760
const getQuotas = async () => {
5861
let quotas, charges;
@@ -74,17 +77,19 @@ export default function AllQuotasConfig() {
7477
const spec = QUOTA_SPEC[service];
7578
if (spec.noSet) continue;
7679
const llmModel = service2model_core(service);
77-
if (llmModel != null) {
80+
const isLLM = llmModel != null;
81+
if (isLLM) {
7882
// We do not show those models, which can't be selected by users OR are free in the first place
7983
const cost = LLM_COST[llmModel];
8084
if (!selectableLLMs.includes(llmModel) || cost?.free === true) {
8185
continue;
8286
}
8387
}
88+
const defaultQuota: number = isLLM ? llm_default_quota : 0;
8489
w[service] = {
8590
current: charges[service] ?? 0,
8691
service: service as Service,
87-
quota: services[service] ?? 0,
92+
quota: services[service] ?? defaultQuota,
8893
};
8994
}
9095
try {
@@ -154,8 +159,8 @@ export default function AllQuotasConfig() {
154159
dataIndex: "quota",
155160
align: "center" as "center",
156161
render: (quota: number, _record: ServiceQuota, index: number) => {
157-
const presets =
158-
QUOTA_SPEC[_record.service]?.category === "ai" ? PRESETS_LLM : PRESETS;
162+
const isLLM = QUOTA_SPEC[_record.service]?.category === "ai";
163+
const presets = isLLM ? PRESETS_LLM : PRESETS;
159164

160165
return (
161166
<Dropdown

src/packages/hub/hub.ts

+7
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import initExpressApp from "./servers/express-app";
4444
import {
4545
loadNatsConfiguration,
4646
initNatsDatabaseServer,
47+
initNatsTieredStorage,
4748
initNatsServer,
4849
} from "@cocalc/server/nats";
4950
import initHttpRedirect from "./servers/http-redirect";
@@ -194,6 +195,11 @@ async function startServer(): Promise<void> {
194195
if (program.natsDatabaseServer) {
195196
await initNatsDatabaseServer();
196197
}
198+
if (program.natsTieredStorage) {
199+
// currently there must be exactly ONE of these, running on the same
200+
// node as the nats-server. E.g., for development it's just part of the server.
201+
await initNatsTieredStorage();
202+
}
197203

198204
if (program.websocketServer) {
199205
// Initialize the version server -- must happen after updating schema
@@ -525,6 +531,7 @@ async function main(): Promise<void> {
525531
program.websocketServer =
526532
program.natsServer =
527533
program.natsDatabaseServer =
534+
program.natsTieredStorage =
528535
program.proxyServer =
529536
program.nextServer =
530537
program.mentions =

src/packages/nats/sync/general-kv.ts

+63-5
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ import { headers as createHeaders } from "@nats-io/nats-core";
127127
import type { MsgHdrs } from "@nats-io/nats-core";
128128
import type { ValueType } from "@cocalc/nats/types";
129129
import { isConnected, waitUntilConnected } from "@cocalc/nats/util";
130+
import { ENFORCE_LIMITS_THROTTLE_MS } from "./stream";
131+
import { asyncDebounce } from "@cocalc/util/async-utils";
132+
import { waitUntilReady } from "@cocalc/nats/tiered-storage/client";
130133

131134
const PUBLISH_TIMEOUT = 15000;
132135

@@ -143,10 +146,6 @@ const CONNECTION_CHECK_INTERVAL = 5000;
143146
// which is convenient for consistency. This is not consistent with NATS's
144147
// own KV store limit naming.
145148

146-
// Significant throttling is VERY, VERY important, since purging old messages frequently
147-
// seems to put a very significant load on NATS!
148-
const ENFORCE_LIMITS_THROTTLE_MS = process.env.COCALC_TEST_MODE ? 100 : 30000;
149-
150149
export interface KVLimits {
151150
// How many keys may be in the KV store. Oldest keys will be removed
152151
// if the key-value store exceeds this size. -1 for unlimited.
@@ -231,6 +230,7 @@ export class GeneralKV<T = any> extends EventEmitter {
231230
if (this.all != null) {
232231
return;
233232
}
233+
await waitUntilReady(this.name);
234234
const kvm = new Kvm(this.env.nc);
235235
await waitUntilConnected();
236236
this.kv = await kvm.create(this.name, {
@@ -355,6 +355,9 @@ export class GeneralKV<T = any> extends EventEmitter {
355355
};
356356

357357
private restartWatch = () => {
358+
// this triggers the end of the "for await (const x of this.watch) {"
359+
// loop in startWatch, which results in another watch starting,
360+
// assuming the object isn't closed.
358361
this.watch?.stop();
359362
};
360363

@@ -499,7 +502,16 @@ export class GeneralKV<T = any> extends EventEmitter {
499502
};
500503

501504
private monitorWatch = async () => {
502-
this.env.nc.on?.("reconnect", this.restartWatch);
505+
if (this.env.nc.on != null) {
506+
this.env.nc.on("reconnect", this.restartWatch);
507+
this.env.nc.on("status", ({ type }) => {
508+
if (type == "reconnect") {
509+
this.ensureWatchIsValid();
510+
}
511+
});
512+
} else {
513+
this.checkWatchOnReconnect();
514+
}
503515
while (this.revisions != null) {
504516
if (!(await isConnected())) {
505517
await waitUntilConnected();
@@ -511,6 +523,52 @@ export class GeneralKV<T = any> extends EventEmitter {
511523
}
512524
};
513525

526+
private ensureWatchIsValid = asyncDebounce(
527+
async () => {
528+
await waitUntilConnected();
529+
await delay(2000);
530+
const isValid = await this.isWatchStillValid();
531+
if (!isValid) {
532+
if (this.kv == null) {
533+
return;
534+
}
535+
console.log(`nats kv: ${this.name} -- watch not valid, so recreating`);
536+
await this.restartWatch();
537+
}
538+
},
539+
3000,
540+
{ leading: false, trailing: true },
541+
);
542+
543+
private isWatchStillValid = async () => {
544+
await waitUntilConnected();
545+
if (this.kv == null || this.watch == null) {
546+
return false;
547+
}
548+
try {
549+
await this.watch._data.info();
550+
return true;
551+
} catch (err) {
552+
console.log(`nats: watch info error -- ${err}`);
553+
return false;
554+
}
555+
};
556+
557+
private checkWatchOnReconnect = async () => {
558+
while (this.kv != null) {
559+
try {
560+
for await (const { type } of await this.env.nc.status()) {
561+
if (type == "reconnect") {
562+
await this.ensureWatchIsValid();
563+
}
564+
}
565+
} catch {
566+
await delay(15000);
567+
await this.ensureWatchIsValid();
568+
}
569+
}
570+
};
571+
514572
close = () => {
515573
if (this.revisions == null) {
516574
// already closed

0 commit comments

Comments
 (0)