Files
Synapse/scripts/cli-concurrent-edit-stress.mjs
Dasemu 40631362b3 Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:32:15 +02:00

2960 lines
88 KiB
JavaScript
Executable File

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { createRequire } from "node:module";
import { mkdirSync, writeFileSync, appendFileSync, readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
const repoRoot = resolve(new URL("..", import.meta.url).pathname);
const require = createRequire(import.meta.url);
const transit = require(require.resolve("transit-js", { paths: [resolve(repoRoot, "cli")] }));
const transitWriter = transit.writer("json");
const transitReader = transit.reader("json");
export function parseArgs(argv) {
const opts = {
graph: "pi-memory",
page: "CLI Concurrent Stress",
extraPages: 2,
journalPages: 2,
journalStart: new Date().toISOString().slice(0, 10),
todayDate: null,
config: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "cli.edn"),
syncBase: "http://127.0.0.1:18080",
clients: 1,
concurrency: 8,
maxOps: 0,
timeoutMs: 20000,
settleAttempts: 30,
settleMs: 1000,
logFile: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "events.jsonl"),
sync: false,
offline: false,
offlineMs: 2500,
failFast: false,
startSyncServer: true,
graphE2ee: true,
e2eePassword: "11111",
authPath: "/Users/tiensonqin/logseq/auth.json",
syncServerPidFile: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "db-sync-server.pid"),
syncServerLogFile: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "db-sync-server.log"),
syncServerDataDir: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "db-sync-server-data"),
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const next = () => {
i += 1;
if (i >= argv.length) {
throw new Error(`Missing value for ${arg}`);
}
return argv[i];
};
switch (arg) {
case "--graph":
opts.graph = next();
break;
case "--page":
opts.page = next();
break;
case "--extra-pages":
opts.extraPages = Number.parseInt(next(), 10);
break;
case "--journal-pages":
opts.journalPages = Number.parseInt(next(), 10);
break;
case "--journal-start":
opts.journalStart = next();
break;
case "--today-date":
opts.todayDate = next();
break;
case "--config":
opts.config = resolve(next());
break;
case "--root-dir":
opts.rootDir = resolve(next());
break;
case "--sync-base":
opts.syncBase = next();
break;
case "--clients":
opts.clients = Number.parseInt(next(), 10);
break;
case "--concurrency":
opts.concurrency = Number.parseInt(next(), 10);
break;
case "--max-ops":
opts.maxOps = Number.parseInt(next(), 10);
break;
case "--timeout-ms":
opts.timeoutMs = Number.parseInt(next(), 10);
break;
case "--settle-attempts":
opts.settleAttempts = Number.parseInt(next(), 10);
break;
case "--settle-ms":
opts.settleMs = Number.parseInt(next(), 10);
break;
case "--log-file":
opts.logFile = resolve(next());
break;
case "--sync-server-pid-file":
opts.syncServerPidFile = resolve(next());
break;
case "--sync-server-log-file":
opts.syncServerLogFile = resolve(next());
break;
case "--sync-server-data-dir":
opts.syncServerDataDir = resolve(next());
break;
case "--sync":
opts.sync = true;
break;
case "--offline":
opts.offline = true;
break;
case "--offline-ms":
opts.offlineMs = Number.parseInt(next(), 10);
break;
case "--no-start-sync-server":
opts.startSyncServer = false;
break;
case "--no-e2ee":
opts.graphE2ee = false;
break;
case "--auth-path":
opts.authPath = resolve(next());
break;
case "--e2ee-password":
opts.e2eePassword = next();
break;
case "--fail-fast":
opts.failFast = true;
break;
case "--help":
case "-h":
printHelp();
process.exit(0);
break;
default:
throw new Error(`Unknown argument: ${arg}`);
}
}
if (!Number.isInteger(opts.concurrency) || opts.concurrency < 1) {
throw new Error("--concurrency must be a positive integer");
}
if (!Number.isInteger(opts.clients) || opts.clients < 1) {
throw new Error("--clients must be a positive integer");
}
if (opts.clients > 1 && !opts.sync) {
throw new Error("--clients greater than 1 requires --sync");
}
if (!Number.isInteger(opts.maxOps) || opts.maxOps < 0) {
throw new Error("--max-ops must be 0 or a positive integer");
}
if (!Number.isInteger(opts.timeoutMs) || opts.timeoutMs < 1000) {
throw new Error("--timeout-ms must be at least 1000");
}
if (!Number.isInteger(opts.settleAttempts) || opts.settleAttempts < 1) {
throw new Error("--settle-attempts must be a positive integer");
}
if (!Number.isInteger(opts.settleMs) || opts.settleMs < 100) {
throw new Error("--settle-ms must be at least 100");
}
if (!Number.isInteger(opts.extraPages) || opts.extraPages < 0) {
throw new Error("--extra-pages must be 0 or a positive integer");
}
if (!Number.isInteger(opts.journalPages) || opts.journalPages < 0) {
throw new Error("--journal-pages must be 0 or a positive integer");
}
if (!Number.isInteger(opts.offlineMs) || opts.offlineMs < 100) {
throw new Error("--offline-ms must be at least 100");
}
if (opts.todayDate && Number.isNaN(new Date(`${opts.todayDate}T00:00:00.000Z`).getTime())) {
throw new Error("--today-date must be a valid YYYY-MM-DD date");
}
opts.wsUrl = `${opts.syncBase.replace(/^http/, "ws").replace(/\/$/, "")}/sync/%s`;
opts.httpBase = opts.syncBase.replace(/\/$/, "");
return opts;
}
function printHelp() {
console.log(`Usage: node scripts/cli-concurrent-edit-stress.mjs [options]
Runs parallel Logseq CLI writes against a dedicated stress page.
Options:
--graph NAME Graph to mutate. Default: pi-memory
--page NAME Stress page. Default: CLI Concurrent Stress
--extra-pages N Additional normal pages to mutate. Default: 2
--journal-pages N Auto-created journal-style pages to mutate. Default: 2
--journal-start DATE First journal date, YYYY-MM-DD. Default: today
--today-date DATE Override today's journal date, YYYY-MM-DD
--config PATH Dedicated CLI config path under tmp/
--root-dir PATH Dedicated root dir for the seed CLI client
--sync-base URL Local db-sync base URL. Default: http://127.0.0.1:18080
--sync Start db-sync client after verifying local /health
--clients N Independent local clients synced to one remote graph. Default: 1
--offline Simulate offline windows by stopping sync during writes
--offline-ms N Duration of each offline window. Default: 2500
--no-start-sync-server
Do not auto-start local db-sync when --sync is used
--no-e2ee Initialize the stress graph as non-E2EE for local sync
--auth-path PATH Auth JSON for local db-sync. Default: /Users/tiensonqin/logseq/auth.json
--e2ee-password TEXT Password used for local sync upload initialization. Default: 11111
--concurrency N Parallel workers. Default: 8
--max-ops N Stop after N operations. Default: 0, run forever
--timeout-ms N Per-CLI-command timeout. Default: 20000
--settle-attempts N Final sync status polling attempts. Default: 30
--settle-ms N Delay between sync status polls. Default: 1000
--log-file PATH JSONL event log path
--sync-server-pid-file PATH
Local db-sync server pid file
--sync-server-log-file PATH
Local db-sync server log file
--sync-server-data-dir PATH
Local db-sync server data dir
--fail-fast Exit on first CLI error
`);
}
function assertLocalUrl(url, label) {
let parsed;
try {
parsed = new URL(url.replace("%s", "graph-id"));
} catch {
throw new Error(`${label} is not a valid URL: ${url}`);
}
const localHosts = new Set(["127.0.0.1", "localhost", "::1"]);
if (!localHosts.has(parsed.hostname)) {
throw new Error(`${label} must point to localhost/127.0.0.1, got ${url}`);
}
}
function authConfigLines(opts) {
const auth = JSON.parse(readFileSync(opts.authPath, "utf8"));
const lines = [` :auth-path "${opts.authPath}"`];
if (typeof auth["refresh-token"] === "string" && auth["refresh-token"].length > 0) {
lines.push(` :refresh-token ${JSON.stringify(auth["refresh-token"])}`);
}
if (typeof auth["id-token"] === "string" && auth["id-token"].length > 0) {
lines.push(` :id-token ${JSON.stringify(auth["id-token"])}`);
}
if (typeof auth["access-token"] === "string" && auth["access-token"].length > 0) {
lines.push(` :access-token ${JSON.stringify(auth["access-token"])}`);
}
return lines;
}
export function stressConfigText(opts) {
return [
"{",
" :output-format :json",
` :http-base "${opts.httpBase}"`,
` :ws-url "${opts.wsUrl}"`,
...authConfigLines(opts),
"}",
"",
].join("\n");
}
function ensureStressConfig(opts) {
assertLocalUrl(opts.httpBase, "http-base");
assertLocalUrl(opts.wsUrl, "ws-url");
if (opts.rootDir) {
mkdirSync(opts.rootDir, { recursive: true });
}
if (opts.homeDir) {
mkdirSync(opts.homeDir, { recursive: true });
}
mkdirSync(dirname(opts.config), { recursive: true });
writeFileSync(opts.config, stressConfigText(opts), "utf8");
}
function logEvent(opts, event) {
const payload = {
ts: new Date().toISOString(),
graph: opts.graph,
page: opts.page,
...event,
};
appendFileSync(opts.logFile, `${JSON.stringify(payload)}\n`, "utf8");
if (event.level === "error" || event.level === "warn" || event.event === "summary") {
console.log(JSON.stringify(payload));
}
}
const expectedRaceErrorCodes = new Set([
"block-not-found",
"page-not-found",
"recycled-page",
"source-not-found",
"target-not-found",
"upsert-id-not-found",
]);
function expectedCliRace(parsed, context = {}) {
return (
expectedRaceErrorCodes.has(parsed?.error?.code) ||
(context.op === "property-delete-recreate" &&
context.phase === "delete" &&
(parsed?.error?.code === "property-not-found" || parsed?.error?.code === "ambiguous-property-name")) ||
(context.op === "tag-delete-recreate" && context.phase === "delete" && parsed?.error?.code === "tag-not-found")
);
}
export function classifyCliResult(result, parsed, context = {}) {
const ok = result.code === 0 && (!parsed || parsed.status === "ok");
const expectedRace = !ok && expectedCliRace(parsed, context);
return {
ok,
level: ok || expectedRace ? "info" : "error",
outcome: ok ? "ok" : expectedRace ? "race-conflict" : "failed",
expectedRace,
};
}
const rawViewHistoryOps = new Set([
"http-create-property-history",
"http-create-linked-references-view",
"http-create-unlinked-references-view",
"http-delete-view-target-with-related-entities",
]);
function expectedHttpRace(parsed, context = {}) {
const message = parsed?.error?.message || "";
return (
(Number.isInteger(context.id) &&
parsed?.error?.code === "exception" &&
message === "Set block property failed: block or property doesn't exist") ||
(Number.isInteger(context.id) &&
rawViewHistoryOps.has(context.op) &&
parsed?.error?.code === "exception" &&
(message.startsWith("Nothing found for entity id (:block/uuid") ||
message.startsWith("Nothing found for entity id [:block/uuid")))
);
}
export function classifyHttpResult(result, parsed, context = {}) {
const noOp = result.ok && context.requireTruthyResult && !parsed?.result;
const ok = result.ok && !noOp;
const expectedRace = !ok && !noOp && expectedHttpRace(parsed, context);
return {
ok,
level: ok || expectedRace ? "info" : "error",
outcome: ok ? "ok" : expectedRace ? "race-conflict" : noOp ? "no-op" : "failed",
expectedRace,
};
}
export function staleIdsFromContext(context = {}) {
const ids = [];
for (const key of ["id", "targetId", "parentId"]) {
if (Number.isInteger(context[key])) {
ids.push(context[key]);
}
}
if (Array.isArray(context.ids)) {
for (const id of context.ids) {
if (Number.isInteger(id)) {
ids.push(id);
}
}
}
return ids;
}
function runProcess(command, args, opts) {
return new Promise((resolvePromise) => {
const startedAt = Date.now();
const child = spawn(command, args, {
cwd: repoRoot,
env: {
...process.env,
...(opts.env || {}),
LOGSEQ_CLI_CONFIG: opts.config,
LOGSEQ_CLI_GRAPH: opts.graph,
LOGSEQ_CLI_TIMEOUT_MS: String(opts.timeoutMs),
LOGSEQ_CLI_OUTPUT: "json",
},
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
child.kill("SIGTERM");
setTimeout(() => child.kill("SIGKILL"), 1500).unref();
}, opts.timeoutMs + 3000);
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("close", (code, signal) => {
clearTimeout(timer);
resolvePromise({
command,
args,
code,
signal,
stdout: stdout.trim(),
stderr: stderr.trim(),
elapsedMs: Date.now() - startedAt,
});
});
});
}
async function runCli(opts, args, context = {}) {
const command = cliCommand(process.env.LOGSEQ_BIN);
const result = await runProcess(command.command, [...command.args, ...args], opts);
let parsed = null;
if (result.stdout) {
try {
parsed = JSON.parse(result.stdout);
} catch {
parsed = null;
}
}
const classification = classifyCliResult(result, parsed, context);
const event = {
event: "cli",
level: classification.level,
outcome: classification.outcome,
context,
args,
code: result.code,
signal: result.signal,
elapsedMs: result.elapsedMs,
status: parsed?.status,
error: parsed?.error,
stderr: result.stderr || undefined,
stdout: parsed ? undefined : result.stdout,
};
logEvent(opts, event);
if (!classification.ok && !classification.expectedRace && opts.failFast) {
throw new Error(`CLI command failed: logseq ${args.join(" ")}`);
}
return { ...classification, parsed, result };
}
export function cliCommand(bin = null) {
const command = bin || "logseq";
if (command.endsWith(".js")) {
return { command: process.execPath, args: [command] };
}
return { command, args: [] };
}
function cliFetchFailed(response) {
return (
!response.ok &&
(String(response.result?.stderr || "").includes("fetch failed") ||
String(response.result?.stdout || "").includes("fetch failed") ||
response.parsed?.error?.message === "fetch failed")
);
}
async function runSyncStart(opts, context = {}) {
let response = await runCli(opts, ["sync", "start", "--graph", opts.graph], context);
if (cliFetchFailed(response)) {
await delay(500);
await runCli(opts, ["server", "list"], { ...context, op: "server-list-before-sync-start-retry" });
response = await runCli(opts, ["sync", "start", "--graph", opts.graph], {
...context,
retry: "fetch-failed",
});
}
return response;
}
async function invokeThreadApi(opts, state, method, args, context = {}) {
const startedAt = Date.now();
let response = null;
let text = "";
let parsed = null;
let error = null;
const invokeOnce = async () => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), opts.timeoutMs + 3000);
try {
const nextResponse = await fetch(`${state.workerBaseUrl}/v1/invoke`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
method,
argsTransit: transitWriter.write(args),
}),
signal: controller.signal,
});
const nextText = await nextResponse.text();
let nextParsed = null;
if (nextText) {
nextParsed = JSON.parse(nextText);
}
if (nextParsed?.resultTransit) {
nextParsed.result = transitReader.read(nextParsed.resultTransit);
}
return { response: nextResponse, text: nextText, parsed: nextParsed, error: null };
} catch (caught) {
return { response: null, text: "", parsed: null, error: caught };
} finally {
clearTimeout(timer);
}
};
({ response, text, parsed, error } = await invokeOnce());
if (error && String(error instanceof Error ? error.message : error).includes("fetch failed")) {
const refreshed = await refreshWorkerBaseUrl(opts, state, context);
if (refreshed) {
({ response, text, parsed, error } = await invokeOnce());
}
}
const classification = classifyHttpResult({ ok: !error && response?.ok, status: response?.status }, parsed, context);
if (classification.expectedRace) {
for (const id of staleIdsFromContext(context)) {
state.activeIds.delete(id);
state.activeBlockUuids.delete(id);
state.taskIds.delete(id);
}
}
logEvent(opts, {
event: "http",
level: classification.level,
outcome: classification.outcome,
context,
method,
elapsedMs: Date.now() - startedAt,
status: response?.status,
error: error ? { message: error instanceof Error ? error.message : String(error) } : parsed?.error,
stdout: classification.ok ? undefined : text,
});
if (!classification.ok && !classification.expectedRace && opts.failFast) {
throw new Error(`HTTP invoke failed: ${method}`);
}
return { ...classification, parsed, text, error };
}
async function refreshWorkerBaseUrl(opts, state, context = {}) {
const serverList = await runCli(opts, ["server", "list"], {
...context,
op: "refresh-worker-base-url",
client: opts.clientName,
});
const servers = serverList.parsed?.data?.servers || [];
let graphServer = servers.find((server) => server.graph === opts.graph);
if (!graphServer) {
graphServer = await startDbWorkerNode(opts);
}
const baseUrl = graphServer?.["base-url"];
if (!baseUrl) {
return false;
}
if (baseUrl !== state.workerBaseUrl) {
logEvent(opts, {
event: "db-worker-node",
level: "warn",
status: "base-url-refreshed",
client: opts.clientName,
previousBaseUrl: state.workerBaseUrl,
baseUrl,
});
}
state.workerBaseUrl = baseUrl;
state.repo = graphServerRepo(opts, graphServer);
return true;
}
async function applyOutlinerOps(opts, state, ops, options, context = {}) {
return invokeThreadApi(
opts,
state,
"thread-api/apply-outliner-ops",
[state.repo, ops, options || transit.map()],
context,
);
}
async function transactRaw(opts, state, txData, txMeta, context = {}) {
return invokeThreadApi(
opts,
state,
"thread-api/transact",
[state.repo, txData, txMeta || tmap(), null],
context,
);
}
function resultIds(response) {
const result = response.parsed?.data?.result;
if (Array.isArray(result)) {
return result.filter((value) => Number.isInteger(value));
}
if (Number.isInteger(result)) {
return [result];
}
return [];
}
async function fetchJson(url, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return { ok: response.ok, status: response.status, text: await response.text() };
} finally {
clearTimeout(timer);
}
}
function syncBaseUrl(opts) {
return new URL(opts.syncBase.replace(/\/$/, ""));
}
export function syncServerStartArgs(opts) {
const base = syncBaseUrl(opts);
return [
"cli-e2e/scripts/db_sync_server.py",
"start",
"--repo-root",
repoRoot,
"--pid-file",
opts.syncServerPidFile,
"--log-file",
opts.syncServerLogFile,
"--data-dir",
opts.syncServerDataDir,
"--host",
base.hostname,
"--port",
base.port || (base.protocol === "https:" ? "443" : "80"),
"--startup-timeout-s",
"25",
"--auth-path",
opts.authPath,
];
}
export function syncUploadArgs(opts) {
const args = ["sync", "upload", "--graph", opts.graph];
if (opts.graphE2ee !== false) {
args.push("--e2ee-password", opts.e2eePassword);
}
return args;
}
export function syncDownloadArgs(opts) {
const args = ["sync", "download", "--graph", opts.graph];
if (opts.graphE2ee !== false) {
args.push("--e2ee-password", opts.e2eePassword);
}
return args;
}
export function syncEnsureKeysArgs(opts) {
return ["sync", "ensure-keys", "--upload-keys", "--e2ee-password", opts.e2eePassword];
}
export function syncNeedsEnsureKeys(opts) {
return opts.graphE2ee !== false;
}
export function graphValidateArgs(opts) {
return ["graph", "validate", "--graph", opts.graph];
}
export function syncStatusUninitialized(response) {
const status = response.parsed?.data;
return response.ok && status && status["graph-id"] == null;
}
function numberStatusValue(value) {
return Number.isInteger(value) ? value : Number.parseInt(String(value), 10);
}
export function isSettledSyncStatus(response) {
const status = response.parsed?.data;
if (!response.ok || !status) {
return false;
}
const pendingLocal = numberStatusValue(status["pending-local"]);
const pendingServer = numberStatusValue(status["pending-server"]);
const localChecksum = status["local-checksum"];
const remoteChecksum = status["remote-checksum"];
return (
pendingLocal === 0 &&
pendingServer === 0 &&
typeof localChecksum === "string" &&
localChecksum.length > 0 &&
localChecksum === remoteChecksum
);
}
export function isIdleSyncStatus(response) {
const status = response.parsed?.data;
if (!response.ok || !status || status["last-error"] != null) {
return false;
}
const pendingLocal = numberStatusValue(status["pending-local"]);
const pendingServer = numberStatusValue(status["pending-server"]);
const localTx = numberStatusValue(status["local-tx"]);
const remoteTx = numberStatusValue(status["remote-tx"]);
return pendingLocal === 0 && pendingServer === 0 && Number.isInteger(localTx) && localTx === remoteTx;
}
function transitValueGet(value, key) {
if (value && typeof value.get === "function") {
return value.get(key) ?? value.get(kw(key)) ?? value.get(`:${key}`);
}
if (value && typeof value === "object") {
return value[key] ?? value[`:${key}`];
}
return undefined;
}
export function checksumDiagnosticsMatch(result) {
const recomputed = transitValueGet(result, "recomputed-checksum");
const remote = transitValueGet(result, "remote-checksum");
return typeof recomputed === "string" && recomputed.length > 0 && recomputed === remote;
}
export function clientRuntimeOptions(opts, clientIndex) {
const clientNumber = clientIndex + 1;
const clientName = `client-${clientNumber}`;
const baseDir = resolve(dirname(opts.config), "clients", clientName);
const rootDir = clientIndex === 0 ? opts.rootDir || process.env.LOGSEQ_CLI_ROOT_DIR : join(baseDir, "root");
const homeDir = clientIndex === 0 ? opts.homeDir || process.env.HOME : join(baseDir, "home");
const config = clientIndex === 0 ? opts.config : join(baseDir, "cli.edn");
const env = { ...(opts.env || {}) };
if (rootDir) {
env.LOGSEQ_CLI_ROOT_DIR = rootDir;
}
if (homeDir) {
env.HOME = homeDir;
}
return {
...opts,
clientIndex,
clientName,
config,
rootDir,
homeDir,
env,
};
}
async function ensureLocalSyncServer(opts) {
const healthUrl = `${opts.httpBase}/health`;
const health = await fetchJson(healthUrl, 2500).catch((error) => ({
ok: false,
status: "fetch-error",
text: String(error),
}));
if (health.ok) {
logEvent(opts, { event: "db-sync-server", level: "info", status: "healthy", baseUrl: opts.httpBase });
return;
}
if (!opts.startSyncServer) {
throw new Error(`Local db-sync server is not healthy at ${healthUrl}; start it or remove --no-start-sync-server`);
}
logEvent(opts, { event: "db-sync-server", level: "info", status: "starting", baseUrl: opts.httpBase });
const result = await runProcess("python3", syncServerStartArgs(opts), opts);
let parsed = null;
if (result.stdout) {
try {
parsed = JSON.parse(result.stdout);
} catch {
parsed = null;
}
}
if (result.code !== 0 || parsed?.status !== "ok") {
throw new Error(
[
`Failed to start local db-sync server at ${opts.httpBase}.`,
result.stderr || "",
result.stdout || "",
].filter(Boolean).join("\n"),
);
}
logEvent(opts, {
event: "db-sync-server",
level: "info",
status: "started",
pid: parsed.pid,
baseUrl: parsed.base_url,
logFile: parsed.log_file,
});
}
function graphServerRepo(opts, graphServer) {
return graphServer.repo || `logseq_db_${opts.graph}`;
}
async function startDbWorkerNode(opts) {
await runCli(opts, ["server", "start", "--graph", opts.graph], { op: "server-start", client: opts.clientName });
const serverList = await runCli(opts, ["server", "list"], { op: "server-list", client: opts.clientName });
const servers = serverList.parsed?.data?.servers || [];
const graphServer = servers.find((server) => server.graph === opts.graph);
if (!graphServer || !String(graphServer["base-url"] || "").startsWith("http://127.0.0.1:")) {
throw new Error(`No local db-worker-node server found for graph ${opts.graph} on ${opts.clientName || "client-1"}`);
}
logEvent(opts, { event: "db-worker-node", level: "info", client: opts.clientName, server: graphServer });
return graphServer;
}
async function setLocalGraphE2ee(opts, graphServer, graphE2ee) {
const state = {
workerBaseUrl: graphServer["base-url"],
repo: graphServerRepo(opts, graphServer),
activeIds: new Set(),
activeBlockUuids: new Map(),
taskIds: new Set(),
};
const response = await invokeThreadApi(
opts,
state,
"thread-api/transact",
[
state.repo,
[
tmap([
kw("db/ident"),
kw("logseq.kv/graph-rtc-e2ee?"),
kw("kv/value"),
graphE2ee,
]),
],
tmap([kw("outliner-op"), kw("set-kvs")]),
null,
],
{ op: "set-local-graph-e2ee", graphE2ee },
);
if (!response.ok) {
throw new Error(`Failed to set local graph E2EE metadata for ${opts.graph}`);
}
}
async function ensureLocalServers(opts) {
const graphServer = await startDbWorkerNode(opts);
if (opts.sync) {
await ensureLocalSyncServer(opts);
const status = await runCli(opts, ["sync", "status", "--graph", opts.graph], { op: "sync-status-before-start" });
if (syncStatusUninitialized(status)) {
if (opts.graphE2ee === false) {
await setLocalGraphE2ee(opts, graphServer, false);
}
if (syncNeedsEnsureKeys(opts)) {
const keys = await runCli(opts, syncEnsureKeysArgs(opts), { op: "sync-ensure-keys" });
if (!keys.ok) {
throw new Error(`Failed to initialize local sync keys for ${opts.graph}`);
}
}
const upload = await runCli(opts, syncUploadArgs(opts), { op: "sync-upload-initialize" });
if (!upload.ok) {
throw new Error(`Failed to initialize local sync upload for ${opts.graph}`);
}
}
const start = await runSyncStart(opts, { op: "sync-start" });
if (!start.ok) {
throw new Error(`Failed to start local sync for ${opts.graph}`);
}
}
return graphServer;
}
async function ensureReplicaLocalServers(opts) {
await ensureLocalSyncServer(opts);
const download = await runCli(opts, syncDownloadArgs(opts), { op: "sync-download-replica", client: opts.clientName });
if (!download.ok) {
throw new Error(`Failed to download remote sync graph ${opts.graph} for ${opts.clientName}`);
}
const graphServer = await startDbWorkerNode(opts);
const start = await runSyncStart(opts, {
op: "sync-start",
client: opts.clientName,
});
if (!start.ok) {
throw new Error(`Failed to start local sync for ${opts.graph} on ${opts.clientName}`);
}
return graphServer;
}
function choose(set) {
const values = Array.from(set);
if (values.length === 0) {
return null;
}
return values[Math.floor(Math.random() * values.length)];
}
function chooseArray(values) {
if (!Array.isArray(values) || values.length === 0) {
return null;
}
return values[Math.floor(Math.random() * values.length)];
}
function chooseMany(set, maxCount) {
const values = Array.from(set);
values.sort(() => Math.random() - 0.5);
return values.slice(0, Math.max(1, Math.min(maxCount, values.length)));
}
function ednString(value) {
return JSON.stringify(String(value));
}
function ordinalDay(day) {
if (day % 100 >= 11 && day % 100 <= 13) {
return `${day}th`;
}
switch (day % 10) {
case 1:
return `${day}st`;
case 2:
return `${day}nd`;
case 3:
return `${day}rd`;
default:
return `${day}th`;
}
}
function journalPageName(date) {
return `${date.toLocaleString("en-US", { month: "short", timeZone: "UTC" })} ${ordinalDay(date.getUTCDate())}, ${date.getUTCFullYear()}`;
}
function journalPageNameForYmd(ymd) {
const date = new Date(`${ymd}T00:00:00.000Z`);
if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid journal date: ${ymd}`);
}
return journalPageName(date);
}
function localYmd(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
export function todayJournalPageName(opts = {}, now = new Date()) {
return journalPageNameForYmd(opts.todayDate || localYmd(now));
}
export function stressPageNames(opts) {
const pages = [opts.page];
for (let index = 0; index < opts.extraPages; index += 1) {
pages.push(`${opts.page} page ${index + 2}`);
}
const start = new Date(`${opts.journalStart}T00:00:00.000Z`);
if (Number.isNaN(start.getTime())) {
throw new Error(`Invalid --journal-start date: ${opts.journalStart}`);
}
for (let index = 0; index < opts.journalPages; index += 1) {
const date = new Date(start.getTime() + index * 24 * 60 * 60 * 1000);
pages.push(journalPageName(date));
}
return pages;
}
export function bootstrapStressPageNames(opts) {
const pages = [opts.page];
for (let index = 0; index < opts.extraPages; index += 1) {
pages.push(`${opts.page} page ${index + 2}`);
}
return pages;
}
export function mutableStressPageNames(opts) {
const pages = [];
for (let index = 0; index < opts.extraPages; index += 1) {
pages.push(`${opts.page} page ${index + 2}`);
}
return pages.length > 0 ? pages : [opts.page];
}
export function operationWeights(opts = {}) {
const operations = [
["create-root", 18],
["create-child", 18],
["create-tree", 10],
["update", 8],
["move-child", 14],
["move-page", 12],
["move-sibling", 12],
["block-tags-add", 3],
["block-tags-remove", 2],
["block-properties-add", 3],
["block-properties-remove", 2],
["task-create", 3],
["task-update", 2],
["page-upsert", 2],
["page-properties-add", 1],
["page-properties-remove", 1],
["page-delete-restore", 6],
["tag-upsert", 1],
["tag-delete-recreate", 1],
["property-upsert", 1],
["property-delete-recreate", 1],
["http-insert-blocks", 12],
["http-delete-blocks", 12],
["http-move-blocks", 12],
["http-save-block", 4],
["http-move-up-down", 8],
["http-indent-outdent", 8],
["http-apply-template", 1],
["http-create-page", 1],
["http-rename-page", 1],
["http-delete-restore-page", 8],
["http-recycle-delete-page", 4],
["http-upsert-property", 1],
["http-set-block-property", 2],
["http-remove-block-property", 1],
["http-batch-set-property", 2],
["http-batch-remove-property", 1],
["http-batch-delete-property-value", 1],
["http-create-property-history", 2],
["http-create-linked-references-view", 2],
["http-create-unlinked-references-view", 2],
["http-toggle-reaction", 1],
["http-delete-view-target-with-related-entities", 4],
["http-insert-undo-redo", 12],
["delete-single", 18],
["delete-batch", 12],
["delete-update-race", 8],
];
if (opts.sync && opts.offline) {
operations.push(["offline-window", 2]);
operations.push(["offline-today-journal-race", 8]);
if ((opts.clients || 1) > 1) {
operations.push(["multi-client-offline-rebase", 12]);
operations.push(["multi-client-today-journal-race", 10]);
}
}
return operations;
}
export function operationNames(opts = {}) {
return operationWeights(opts).map(([name]) => name);
}
export function uniqueOperationPageTitle(prefix, state, context = {}) {
const workerId = context.workerId ?? "unknown-worker";
const seq = context.seq ?? "unknown-seq";
return `${prefix} ${state.runId} worker-${workerId} seq-${seq} ${crypto.randomUUID()}`;
}
export function offlineTodayJournalClientCount(opts = {}) {
return Math.max(2, opts.clients ?? 1);
}
function liveBlocksQuery(page) {
return `[:find [(pull ?b [:db/id :block/uuid]) ...] :where [?p :block/name ${ednString(page.toLowerCase())}] [?b :block/page ?p] (not [?b :block/name])]`;
}
export function blockByUuidQuery(uuidValue) {
return `[:find ?b . :where [?b :block/uuid #uuid ${ednString(uuidValue)}]]`;
}
function pageByNameQuery(page) {
return `[:find (pull ?p [:db/id :block/uuid :block/name :block/title]) . :where [?p :block/name ${ednString(page.toLowerCase())}]]`;
}
function normalizedUuidString(value) {
if (typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
return value;
}
if (value && typeof value === "object" && typeof value.value === "string") {
return normalizedUuidString(value.value);
}
return null;
}
function pageUuidFromQueryResult(response) {
return normalizedUuidString(response.parsed?.data?.result?.["block/uuid"]);
}
export function blockExistsFromQueryResult(response) {
return Number.isInteger(response.parsed?.data?.result);
}
function liveBlocksFromQueryResult(response) {
const rows = response.parsed?.data?.result;
if (!Array.isArray(rows)) {
return [];
}
return rows
.map((row) => {
const id = row?.["db/id"];
const uuid = normalizedUuidString(row?.["block/uuid"]);
return Number.isInteger(id) && uuid ? { id, uuid } : null;
})
.filter(Boolean);
}
async function findPageUuid(opts, page, context = {}) {
const response = await runCli(
opts,
["query", "--graph", opts.graph, "--query", pageByNameQuery(page)],
{ op: "find-page-uuid", page, ...context },
);
return pageUuidFromQueryResult(response);
}
async function refreshLiveIds(opts, state, reason) {
const ids = new Set();
const blockUuids = new Map();
for (const page of state.pageNames) {
const response = await runCli(
opts,
["query", "--graph", opts.graph, "--query", liveBlocksQuery(page)],
{ op: "refresh-live-ids", reason, page },
);
for (const block of liveBlocksFromQueryResult(response)) {
ids.add(block.id);
blockUuids.set(block.id, block.uuid);
}
}
state.activeIds = ids;
state.activeBlockUuids = blockUuids;
state.lastRefreshSeq = state.nextSeq;
logEvent(opts, {
event: "refresh-live-ids",
level: "info",
reason,
activeIds: state.activeIds.size,
});
}
async function refreshLiveIdsIfNeeded(opts, state, reason) {
if (state.refreshing) {
return;
}
if (state.activeIds.size >= opts.concurrency * 2 && state.nextSeq - state.lastRefreshSeq < 50) {
return;
}
state.refreshing = true;
try {
await refreshLiveIds(opts, state, reason);
} finally {
state.refreshing = false;
}
}
function pruneStaleIds(state, response, context) {
if (!response.expectedRace) {
return;
}
for (const id of staleIdsFromContext(context)) {
state.activeIds.delete(id);
}
}
function weightedOperation(opts) {
const operations = operationWeights(opts);
const total = operations.reduce((sum, [, weight]) => sum + weight, 0);
let cursor = Math.random() * total;
for (const [name, weight] of operations) {
cursor -= weight;
if (cursor <= 0) {
return name;
}
}
return "create-root";
}
async function createBlock(opts, state, content, targetArgs, context) {
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, ...targetArgs, "--content", content],
context,
);
pruneStaleIds(state, response, context);
for (const id of resultIds(response)) {
state.activeIds.add(id);
}
}
function randomPage(state) {
return chooseArray(state.pageNames) || state.pageNames[0];
}
function mutablePage(state) {
return chooseArray(state.mutablePageNames) || state.pageNames[0];
}
function stressTag(state) {
return chooseArray(state.stableTags);
}
function stressProperty(state) {
return chooseArray(state.stableProperties);
}
function stressTagId(state) {
return chooseArray(Array.from(state.stableTagIds.values()));
}
function rawPropertyIdent(state) {
return state.rawPropertyIdent || "user.property/cli-http-prop";
}
function propertyMap(name, value) {
return `{${ednString(name)} ${ednString(value)}}`;
}
function propertyVector(name) {
return `[${ednString(name)}]`;
}
function kw(name) {
return transit.keyword(name);
}
function uuid(value) {
return transit.uuid(value);
}
function tmap(entries = []) {
return transit.map(entries);
}
function localTxMeta(entries = []) {
return tmap([kw("db-sync/tx-id"), uuid(crypto.randomUUID()), kw("local-tx?"), true, ...entries]);
}
function blockMap(entries) {
return tmap(entries.flatMap(([key, value]) => [kw(key), value]));
}
function lookupRef(attr, value) {
return [kw(attr), value];
}
function blockUuidRef(uuidValue) {
return lookupRef("block/uuid", uuid(uuidValue));
}
function chooseActiveUuid(state) {
const id = choose(state.activeIds);
if (!id) {
return null;
}
const uuidValue = state.activeBlockUuids.get(id);
return uuidValue ? { id, uuid: uuidValue } : null;
}
async function blockStillExists(opts, block, context = {}) {
const response = await runCli(
opts,
["query", "--graph", opts.graph, "--query", blockByUuidQuery(block.uuid)],
{ ...context, op: "check-live-block", sourceOp: context.op, id: block.id, uuid: block.uuid },
);
return blockExistsFromQueryResult(response);
}
async function chooseLiveActiveUuid(opts, state, context = {}) {
const candidates = Array.from(state.activeIds)
.map((id) => {
const uuidValue = state.activeBlockUuids.get(id);
return uuidValue ? { id, uuid: uuidValue } : null;
})
.filter(Boolean);
candidates.sort(() => Math.random() - 0.5);
for (const block of candidates.slice(0, 4)) {
if (await blockStillExists(opts, block, context)) {
return block;
}
state.activeIds.delete(block.id);
state.activeBlockUuids.delete(block.id);
logEvent(opts, {
event: "skip-stale-raw-target",
level: "info",
context,
id: block.id,
uuid: block.uuid,
});
}
return null;
}
function chooseDistinctActiveUuids(state) {
const first = chooseActiveUuid(state);
if (!first) {
return null;
}
const candidates = Array.from(state.activeBlockUuids.entries())
.filter(([id]) => id !== first.id)
.map(([id, uuidValue]) => ({ id, uuid: uuidValue }));
const second = chooseArray(candidates);
return second ? [first, second] : null;
}
async function ensureStressMetadata(opts, state) {
for (const page of state.bootstrapPageNames) {
await runCli(opts, ["upsert", "page", "--graph", opts.graph, "--page", page], {
op: "bootstrap-page",
page,
});
}
for (const name of [...state.stableTags, state.volatileTag]) {
await runCli(opts, ["upsert", "tag", "--graph", opts.graph, "--name", name], {
op: "bootstrap-tag",
name,
});
}
for (const name of [...state.stableProperties, state.volatileProperty]) {
await runCli(opts, ["upsert", "property", "--graph", opts.graph, "--name", name, "--type", "default", "--cardinality", "one", "--public", "true"], {
op: "bootstrap-property",
name,
});
}
await refreshStressTagIds(opts, state, "bootstrap");
await applyRawUpsertProperty(opts, state, { op: "bootstrap-http-upsert-property" });
}
async function refreshStressTagIds(opts, state, reason) {
const response = await runCli(opts, ["list", "tag", "--graph", opts.graph], {
op: "refresh-stress-tags",
reason,
});
const items = response.parsed?.data?.items || [];
state.stableTagIds = new Map();
for (const name of state.stableTags) {
const item = items.find((candidate) => candidate?.["block/title"] === name);
const id = item?.["db/id"];
if (Number.isInteger(id)) {
state.stableTagIds.set(name, id);
}
}
}
async function applyRawSaveBlock(opts, state, context, title) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
const response = await applyOutlinerOps(
opts,
state,
[
[
kw("save-block"),
[
blockMap([
["block/uuid", uuid(block.uuid)],
["block/title", title],
]),
tmap(),
],
],
],
tmap(),
{ ...context, id: block.id, uuid: block.uuid },
);
if (!response.ok) {
state.activeIds.delete(block.id);
state.activeBlockUuids.delete(block.id);
}
}
async function applyRawInsertBlocks(opts, state, context, title) {
const target = chooseActiveUuid(state);
if (!target) {
return;
}
const newUuid = crypto.randomUUID();
const block = blockMap([
["block/uuid", uuid(newUuid)],
["block/title", title],
]);
await applyOutlinerOps(
opts,
state,
[[kw("insert-blocks"), [[block], uuid(target.uuid), tmap([kw("sibling?"), false, kw("keep-uuid?"), true])]]],
tmap(),
{ ...context, targetId: target.id, targetUuid: target.uuid, uuid: newUuid },
);
await refreshLiveIds(opts, state, "http-insert-blocks");
}
async function applyRawDeleteBlocks(opts, state, context) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("delete-blocks"), [[uuid(block.uuid)], tmap()]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid },
);
state.activeIds.delete(block.id);
state.activeBlockUuids.delete(block.id);
}
async function applyRawMoveBlocks(opts, state, context) {
const pair = chooseDistinctActiveUuids(state);
if (!pair) {
return;
}
const [block, target] = pair;
await applyOutlinerOps(
opts,
state,
[[kw("move-blocks"), [[uuid(block.uuid)], uuid(target.uuid), tmap([kw("sibling?"), Math.random() < 0.5])]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid, targetId: target.id, targetUuid: target.uuid },
);
}
async function applyRawMoveUpDown(opts, state, context, up) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("move-blocks-up-down"), [[uuid(block.uuid)], up]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid, up },
);
}
async function applyRawIndentOutdent(opts, state, context, indent) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("indent-outdent-blocks"), [[uuid(block.uuid)], indent, tmap()]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid, indent },
);
}
async function applyRawApplyTemplate(opts, state, context, title) {
const target = chooseActiveUuid(state);
if (!target) {
return;
}
const templateUuid = crypto.randomUUID();
const insertedUuid = crypto.randomUUID();
const templateBlock = blockMap([
["block/uuid", uuid(insertedUuid)],
["block/title", title],
]);
await applyOutlinerOps(
opts,
state,
[
[
kw("apply-template"),
[
uuid(templateUuid),
uuid(target.uuid),
tmap([kw("template-blocks"), [templateBlock], kw("sibling?"), false]),
],
],
],
tmap(),
{ ...context, targetId: target.id, targetUuid: target.uuid, templateUuid, uuid: insertedUuid },
);
await refreshLiveIds(opts, state, "http-apply-template");
}
async function applyRawCreatePage(opts, state, context, title) {
await applyOutlinerOps(
opts,
state,
[[kw("create-page"), [title, tmap()]]],
tmap(),
{ ...context, page: title },
);
state.pageNames.push(title);
state.mutablePageNames.push(title);
}
async function applyRawRenamePage(opts, state, context, title) {
const page = mutablePage(state);
const pageUuid = await findPageUuid(opts, page, context);
if (!pageUuid) {
return;
}
const temporaryTitle = `${title} ${Date.now()}`;
await applyOutlinerOps(
opts,
state,
[[kw("rename-page"), [uuid(pageUuid), temporaryTitle]]],
tmap(),
{ ...context, page, pageUuid, phase: "rename-away", temporaryTitle },
);
await applyOutlinerOps(
opts,
state,
[[kw("rename-page"), [uuid(pageUuid), page]]],
tmap(),
{ ...context, page, pageUuid, phase: "rename-back", temporaryTitle },
);
}
async function applyRawDeleteRestorePage(opts, state, context, title) {
const page = title || uniqueOperationPageTitle("CLI HTTP Delete Restore", state, context);
await applyOutlinerOps(
opts,
state,
[[kw("create-page"), [page, tmap()]]],
tmap(),
{ ...context, page, phase: "create", requireTruthyResult: true },
);
const pageUuid = await findPageUuid(opts, page, context);
if (!pageUuid) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("delete-page"), [uuid(pageUuid), tmap()]]],
tmap(),
{ ...context, page, pageUuid, phase: "delete", requireTruthyResult: true },
);
await applyOutlinerOps(
opts,
state,
[[kw("restore-recycled"), [uuid(pageUuid)]]],
tmap(),
{ ...context, page, pageUuid, phase: "restore", requireTruthyResult: true },
);
await refreshLiveIds(opts, state, "http-delete-restore-page");
}
async function applyRawRecycleDeletePage(opts, state, context, title) {
await applyOutlinerOps(
opts,
state,
[[kw("create-page"), [title, tmap()]]],
tmap(),
{ ...context, page: title, phase: "create", requireTruthyResult: true },
);
const pageUuid = await findPageUuid(opts, title, context);
if (!pageUuid) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("delete-page"), [uuid(pageUuid), tmap()]]],
tmap(),
{ ...context, page: title, pageUuid, phase: "delete", requireTruthyResult: true },
);
await applyOutlinerOps(
opts,
state,
[[kw("recycle-delete-permanently"), [uuid(pageUuid)]]],
tmap(),
{ ...context, page: title, pageUuid, phase: "permanent-delete", requireTruthyResult: true },
);
}
async function applyRawUpsertProperty(opts, state, context) {
await applyOutlinerOps(
opts,
state,
[
[
kw("upsert-property"),
[
kw(rawPropertyIdent(state)),
tmap([kw("logseq.property/type"), kw("default"), kw("db/cardinality"), kw("db.cardinality/one")]),
tmap([kw("property-name"), state.rawPropertyName || "cli-http-prop"]),
],
],
],
tmap(),
context,
);
}
async function applyRawSetBlockProperty(opts, state, context, value) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("set-block-property"), [uuid(block.uuid), kw(rawPropertyIdent(state)), value]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid },
);
}
async function applyRawRemoveBlockProperty(opts, state, context) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("remove-block-property"), [uuid(block.uuid), kw(rawPropertyIdent(state))]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid },
);
}
async function applyRawBatchSetProperty(opts, state, context, value) {
const blocks = chooseMany(state.activeIds, 3)
.map((id) => ({ id, uuid: state.activeBlockUuids.get(id) }))
.filter((block) => block.uuid);
if (blocks.length === 0) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("batch-set-property"), [blocks.map((block) => uuid(block.uuid)), kw(rawPropertyIdent(state)), value, tmap()]]],
tmap(),
{ ...context, ids: blocks.map((block) => block.id) },
);
}
async function applyRawBatchRemoveProperty(opts, state, context) {
const blocks = chooseMany(state.activeIds, 3)
.map((id) => ({ id, uuid: state.activeBlockUuids.get(id) }))
.filter((block) => block.uuid);
if (blocks.length === 0) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("batch-remove-property"), [blocks.map((block) => uuid(block.uuid)), kw(rawPropertyIdent(state))]]],
tmap(),
{ ...context, ids: blocks.map((block) => block.id) },
);
}
async function applyRawBatchDeletePropertyValue(opts, state, context) {
const block = chooseActiveUuid(state);
const tagId = stressTagId(state);
if (!block || !tagId) {
return;
}
await applyOutlinerOps(
opts,
state,
[[kw("batch-set-property"), [[uuid(block.uuid)], kw("block/tags"), tagId, tmap()]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid, tagId, phase: "set-tag" },
);
await applyOutlinerOps(
opts,
state,
[[kw("batch-delete-property-value"), [[uuid(block.uuid)], kw("block/tags"), tagId]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid, tagId, phase: "delete-tag-value" },
);
}
function referenceViewBlock(viewUuid, viewsPageUuid, target, featureType, title, order, now) {
return blockMap([
["block/uuid", uuid(viewUuid)],
["block/title", title],
["block/page", blockUuidRef(viewsPageUuid)],
["block/parent", blockUuidRef(viewsPageUuid)],
["block/order", order],
["block/created-at", now],
["block/updated-at", now],
["block/refs", blockUuidRef(target.uuid)],
["logseq.property/view-for", blockUuidRef(target.uuid)],
["logseq.property.view/type", kw("logseq.property.view/type.list")],
["logseq.property.view/group-by-property", kw("block/page")],
["logseq.property.view/feature-type", kw(featureType)],
]);
}
function propertyHistoryBlock(historyUuid, targetRef, statusIdent, now) {
return blockMap([
["block/uuid", uuid(historyUuid)],
["block/created-at", now],
["block/updated-at", now],
["logseq.property.history/block", targetRef],
["logseq.property.history/property", kw("logseq.property/status")],
["logseq.property.history/ref-value", kw(statusIdent)],
]);
}
function statusHistoryValue(context, offset = 0) {
const statuses = ["logseq.property/status.todo", "logseq.property/status.doing", "logseq.property/status.done"];
const seq = Number.isInteger(context.seq) ? context.seq : 0;
return statuses[(seq + offset) % statuses.length];
}
function reactionEmojiId(context) {
const emojiIds = ["+1", "eyes", "tada"];
const seq = Number.isInteger(context.seq) ? context.seq : 0;
return emojiIds[seq % emojiIds.length];
}
export function stressReferenceViewOrder(context = {}, offset = 0) {
const base62Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const seq = Number.isInteger(context.seq) ? context.seq : 0;
const index = Math.abs(seq * 2 + offset);
const digit = base62Digits[index % base62Digits.length];
if (index < base62Digits.length) {
return `a${digit}`;
}
const bucket = base62Digits[Math.floor((index - base62Digits.length) / base62Digits.length)];
return `b${bucket}${digit}`;
}
async function findViewsPageUuid(opts, context = {}) {
return findPageUuid(opts, "$$$views", context);
}
async function createReferenceView(opts, state, context, featureType, title) {
const target = await chooseLiveActiveUuid(opts, state, context);
if (!target) {
return null;
}
const viewsPageUuid = await findViewsPageUuid(opts, context);
if (!viewsPageUuid) {
return null;
}
const now = Date.now();
const viewUuid = crypto.randomUUID();
await transactRaw(
opts,
state,
[referenceViewBlock(viewUuid, viewsPageUuid, target, featureType, title, stressReferenceViewOrder(context), now)],
localTxMeta([kw("outliner-op"), kw("create-view")]),
{ ...context, id: target.id, uuid: target.uuid, viewUuid, featureType },
);
return { target, viewUuid };
}
async function applyRawCreatePropertyHistory(opts, state, context) {
const target = await chooseLiveActiveUuid(opts, state, context);
if (!target) {
return;
}
const viewsPageUuid = await findViewsPageUuid(opts, context);
if (!viewsPageUuid) {
return;
}
const now = Date.now();
const historyUuid = crypto.randomUUID();
await transactRaw(
opts,
state,
[propertyHistoryBlock(historyUuid, blockUuidRef(target.uuid), statusHistoryValue(context), now)],
localTxMeta([kw("outliner-op"), kw("record-property-history")]),
{ ...context, id: target.id, uuid: target.uuid, historyUuid },
);
}
async function applyRawCreateReferenceView(opts, state, context, featureType, title) {
await createReferenceView(opts, state, context, featureType, title);
}
async function applyRawToggleReactionForBlock(opts, state, block, context) {
await applyOutlinerOps(
opts,
state,
[[kw("toggle-reaction"), [uuid(block.uuid), reactionEmojiId(context), null]]],
tmap(),
{ ...context, id: block.id, uuid: block.uuid },
);
}
async function applyRawToggleReaction(opts, state, context) {
const block = chooseActiveUuid(state);
if (!block) {
return;
}
await applyRawToggleReactionForBlock(opts, state, block, context);
}
async function applyRawDeleteViewTargetWithRelatedEntities(opts, state, context) {
const target = await chooseLiveActiveUuid(opts, state, context);
if (!target) {
return;
}
const viewsPageUuid = await findViewsPageUuid(opts, context);
if (!viewsPageUuid) {
return;
}
const now = Date.now();
const linkedViewUuid = crypto.randomUUID();
const unlinkedViewUuid = crypto.randomUUID();
const targetHistoryUuid = crypto.randomUUID();
const linkedViewHistoryUuid = crypto.randomUUID();
const unlinkedViewHistoryUuid = crypto.randomUUID();
await transactRaw(
opts,
state,
[
referenceViewBlock(
linkedViewUuid,
viewsPageUuid,
target,
"linked-references",
"Linked references",
stressReferenceViewOrder(context),
now,
),
referenceViewBlock(
unlinkedViewUuid,
viewsPageUuid,
target,
"unlinked-references",
"Unlinked references",
stressReferenceViewOrder(context, 1),
now,
),
propertyHistoryBlock(targetHistoryUuid, blockUuidRef(target.uuid), statusHistoryValue(context), now),
propertyHistoryBlock(linkedViewHistoryUuid, blockUuidRef(linkedViewUuid), statusHistoryValue(context, 1), now),
propertyHistoryBlock(unlinkedViewHistoryUuid, blockUuidRef(unlinkedViewUuid), statusHistoryValue(context, 2), now),
],
localTxMeta([kw("outliner-op"), kw("create-view")]),
{
...context,
id: target.id,
uuid: target.uuid,
linkedViewUuid,
unlinkedViewUuid,
targetHistoryUuid,
linkedViewHistoryUuid,
unlinkedViewHistoryUuid,
phase: "create-related-entities",
},
);
await applyRawToggleReactionForBlock(opts, state, target, { ...context, phase: "create-reaction" });
await applyOutlinerOps(
opts,
state,
[[kw("delete-blocks"), [[uuid(target.uuid)], tmap()]]],
tmap(),
{ ...context, id: target.id, uuid: target.uuid, phase: "delete-target" },
);
state.activeIds.delete(target.id);
state.activeBlockUuids.delete(target.id);
}
async function applyRawInsertUndoRedo(opts, state, context, title) {
const target = chooseActiveUuid(state);
if (!target) {
return;
}
const newUuid = crypto.randomUUID();
const block = blockMap([
["block/uuid", uuid(newUuid)],
["block/title", title],
]);
const forward = [[kw("insert-blocks"), [[block], uuid(target.uuid), tmap([kw("sibling?"), false, kw("keep-uuid?"), true])]]];
const inverse = [[kw("delete-blocks"), [[uuid(newUuid)], tmap()]]];
await applyOutlinerOps(opts, state, forward, tmap(), {
...context,
phase: "redo-initial",
targetId: target.id,
targetUuid: target.uuid,
uuid: newUuid,
});
await applyOutlinerOps(opts, state, inverse, tmap(), {
...context,
phase: "undo",
targetId: target.id,
targetUuid: target.uuid,
uuid: newUuid,
});
await applyOutlinerOps(opts, state, forward, tmap(), {
...context,
phase: "redo",
targetId: target.id,
targetUuid: target.uuid,
uuid: newUuid,
});
await refreshLiveIds(opts, state, "http-insert-undo-redo");
}
async function offlineWindow(opts, state, context) {
if (state.offlineRunning) {
return;
}
state.offlineRunning = true;
try {
await runCli(opts, ["sync", "stop", "--graph", opts.graph], { ...context, phase: "stop" });
logEvent(opts, {
event: "offline-window",
level: "warn",
status: "offline",
context,
durationMs: opts.offlineMs,
});
await delay(opts.offlineMs);
const start = await runSyncStart(opts, { ...context, phase: "start" });
if (!start.ok && opts.failFast) {
throw new Error(`Failed to restart sync after offline window for ${opts.graph}`);
}
} finally {
state.offlineRunning = false;
}
}
async function offlineTodayJournalRace(opts, state, context) {
if (state.offlineRunning) {
return;
}
state.offlineRunning = true;
const page = todayJournalPageName(opts);
if (!state.pageNames.includes(page)) {
state.pageNames.push(page);
}
const clientCount = offlineTodayJournalClientCount(opts);
try {
await runCli(opts, ["sync", "stop", "--graph", opts.graph], { ...context, phase: "stop", page });
logEvent(opts, {
event: "offline-today-journal-race",
level: "warn",
status: "offline",
context: { ...context, page, clientCount },
durationMs: opts.offlineMs,
});
const responses = await Promise.all(
Array.from({ length: clientCount }, (_, index) => {
const clientId = index + 1;
return runCli(
opts,
[
"upsert",
"block",
"--graph",
opts.graph,
"--target-page",
page,
"--content",
`offline today journal ${state.runId} client=${clientId} seq=${context.seq}`,
],
{ ...context, phase: "write", page, clientId, clientCount },
);
}),
);
for (const response of responses) {
for (const id of resultIds(response)) {
state.activeIds.add(id);
}
}
await delay(opts.offlineMs);
const start = await runSyncStart(opts, { ...context, phase: "start", page });
if (!start.ok && opts.failFast) {
throw new Error(`Failed to restart sync after offline today journal race for ${opts.graph}`);
}
await refreshLiveIds(opts, state, "offline-today-journal-race");
} finally {
state.offlineRunning = false;
}
}
async function waitForSyncSettled(opts, context = {}, required = false) {
if (!opts.sync) {
return true;
}
let last = null;
for (let attempt = 1; attempt <= opts.settleAttempts; attempt += 1) {
const status = await runCli(opts, ["sync", "status", "--graph", opts.graph], {
...context,
op: context.op || "sync-status-settle",
attempt,
client: opts.clientName,
});
last = status;
const settled = required ? isSettledSyncStatus(status) : isIdleSyncStatus(status);
if (settled) {
return true;
}
await delay(opts.settleMs);
}
logEvent(opts, {
event: "sync-settle",
level: required ? "error" : "warn",
context,
status: last?.parsed?.data,
});
if (required) {
throw new Error(`Sync did not settle for ${opts.graph} on ${opts.clientName || "client-1"}`);
}
return false;
}
async function validateClientChecksum(client) {
if (!client.opts.sync) {
return;
}
const response = await invokeThreadApi(
client.opts,
client.state,
"thread-api/recompute-checksum-diagnostics",
[client.state.repo],
{ op: "final-recompute-checksum", client: client.opts.clientName },
);
const diagnostics = response.parsed?.result;
const ok = response.ok && checksumDiagnosticsMatch(diagnostics);
logEvent(client.opts, {
event: "checksum-diagnostics",
level: ok ? "info" : "error",
context: { op: "final-recompute-checksum", client: client.opts.clientName },
recomputedChecksum: transitValueGet(diagnostics, "recomputed-checksum"),
localChecksum: transitValueGet(diagnostics, "local-checksum"),
remoteChecksum: transitValueGet(diagnostics, "remote-checksum"),
});
if (!ok) {
throw new Error(`Final checksum validation failed for ${client.opts.graph} on ${client.opts.clientName}`);
}
}
const localRebaseOps = [
"create-root",
"create-child",
"update",
"move-child",
"move-sibling",
"delete-single",
"http-insert-blocks",
"http-delete-blocks",
"http-move-blocks",
"http-insert-undo-redo",
];
function chooseDistinctClients(clients) {
if (!Array.isArray(clients) || clients.length < 2) {
return null;
}
const first = chooseArray(clients);
const candidates = clients.filter((client) => client !== first);
return [first, chooseArray(candidates)];
}
async function multiClientOfflineRebase(opts, cluster, context) {
if (cluster.offlineRunning) {
return;
}
const pair = chooseDistinctClients(cluster.clients);
if (!pair) {
return;
}
const [offlineClient, onlineClient] = pair;
cluster.offlineRunning = true;
try {
await runCli(offlineClient.opts, ["sync", "stop", "--graph", offlineClient.opts.graph], {
...context,
phase: "stop-offline-client",
client: offlineClient.opts.clientName,
});
logEvent(opts, {
event: "multi-client-offline-rebase",
level: "warn",
context: {
...context,
offlineClient: offlineClient.opts.clientName,
onlineClient: onlineClient.opts.clientName,
},
durationMs: opts.offlineMs,
});
for (let index = 0; index < 3; index += 1) {
const op = chooseArray(localRebaseOps);
offlineClient.state.nextSeq += 1;
await runOperation(offlineClient.opts, offlineClient.state, context.workerId, context.seq * 10 + index, op);
}
for (let index = 0; index < 3; index += 1) {
const op = chooseArray(localRebaseOps);
onlineClient.state.nextSeq += 1;
await runOperation(onlineClient.opts, onlineClient.state, context.workerId, context.seq * 10 + index + 3, op);
}
await waitForSyncSettled(
onlineClient.opts,
{ ...context, op: "multi-client-online-settle", onlineClient: onlineClient.opts.clientName },
false,
);
await delay(opts.offlineMs);
const start = await runSyncStart(offlineClient.opts, {
...context,
phase: "start-offline-client",
client: offlineClient.opts.clientName,
});
if (!start.ok && opts.failFast) {
throw new Error(`Failed to restart sync for ${offlineClient.opts.clientName}`);
}
await waitForSyncSettled(
offlineClient.opts,
{ ...context, op: "multi-client-offline-settle", offlineClient: offlineClient.opts.clientName },
false,
);
await Promise.all([
refreshLiveIds(offlineClient.opts, offlineClient.state, "multi-client-offline-rebase"),
refreshLiveIds(onlineClient.opts, onlineClient.state, "multi-client-offline-rebase"),
]);
} finally {
cluster.offlineRunning = false;
}
}
async function multiClientTodayJournalRace(opts, cluster, context) {
if (cluster.offlineRunning) {
return;
}
const pair = chooseDistinctClients(cluster.clients);
if (!pair) {
return;
}
const [first, second] = pair;
const page = todayJournalPageName(opts);
cluster.offlineRunning = true;
try {
for (const client of [first, second]) {
if (!client.state.pageNames.includes(page)) {
client.state.pageNames.push(page);
}
await runCli(client.opts, ["sync", "stop", "--graph", client.opts.graph], {
...context,
phase: "stop",
client: client.opts.clientName,
page,
});
}
await Promise.all(
[first, second].map((client, index) =>
runCli(
client.opts,
[
"upsert",
"block",
"--graph",
client.opts.graph,
"--target-page",
page,
"--content",
`multi-client offline today journal ${client.state.runId} client=${client.opts.clientName} seq=${context.seq}`,
],
{ ...context, phase: "offline-write", client: client.opts.clientName, page, index },
),
),
);
const firstStart = await runSyncStart(first.opts, {
...context,
phase: "start-first",
client: first.opts.clientName,
page,
});
if (!firstStart.ok && opts.failFast) {
throw new Error(`Failed to start sync for ${first.opts.clientName}`);
}
await waitForSyncSettled(first.opts, { ...context, op: "multi-client-today-first-settle", page }, false);
await delay(opts.offlineMs);
const secondStart = await runSyncStart(second.opts, {
...context,
phase: "start-second",
client: second.opts.clientName,
page,
});
if (!secondStart.ok && opts.failFast) {
throw new Error(`Failed to start sync for ${second.opts.clientName}`);
}
await waitForSyncSettled(second.opts, { ...context, op: "multi-client-today-second-settle", page }, false);
await Promise.all([
refreshLiveIds(first.opts, first.state, "multi-client-today-journal-race"),
refreshLiveIds(second.opts, second.state, "multi-client-today-journal-race"),
]);
} finally {
cluster.offlineRunning = false;
}
}
async function runOperation(opts, state, workerId, seq, forcedOp = null) {
const op = forcedOp || weightedOperation(opts);
const stamp = `${state.runId} worker=${workerId} seq=${seq} op=${op}`;
await refreshLiveIdsIfNeeded(opts, state, "operation");
if (state.activeIds.size < opts.concurrency && !["create-root", "offline-window", "offline-today-journal-race"].includes(op)) {
await createBlock(opts, state, `seed ${stamp}`, ["--target-page", randomPage(state)], {
workerId,
seq,
op: "seed",
});
return;
}
switch (op) {
case "create-root":
await createBlock(opts, state, `root ${stamp}`, ["--target-page", randomPage(state)], { workerId, seq, op });
return;
case "create-child": {
const parentId = choose(state.activeIds);
await createBlock(
opts,
state,
`child ${stamp} parent=${parentId}`,
["--target-id", String(parentId), "--pos", Math.random() < 0.5 ? "first-child" : "last-child"],
{
workerId,
seq,
op,
parentId,
},
);
return;
}
case "create-tree": {
const page = randomPage(state);
const blocks = `[
{:block/title ${ednString(`tree root ${stamp}`)}
:block/children [{:block/title ${ednString(`tree child ${stamp}`)}}]}
{:block/title ${ednString(`tree sibling ${stamp}`)}}]`;
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--target-page", page, "--blocks", blocks],
{ workerId, seq, op, page },
);
for (const id of resultIds(response)) {
state.activeIds.add(id);
}
return;
}
case "update": {
const id = choose(state.activeIds);
const context = { workerId, seq, op, id };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--content", `updated ${stamp} id=${id}`],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "move-child": {
const id = choose(state.activeIds);
const targetId = choose(state.activeIds);
if (!id || !targetId || id === targetId) {
return;
}
const context = { workerId, seq, op, id, targetId };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--target-id", String(targetId), "--pos", "last-child"],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "move-page": {
const id = choose(state.activeIds);
const page = randomPage(state);
if (!id) {
return;
}
const context = { workerId, seq, op, id, page };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--target-page", page],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "move-sibling": {
const id = choose(state.activeIds);
const targetId = choose(state.activeIds);
if (!id || !targetId || id === targetId) {
return;
}
const context = { workerId, seq, op, id, targetId };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--target-id", String(targetId), "--pos", "sibling"],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "block-tags-add": {
const id = choose(state.activeIds);
const tag = stressTag(state);
if (!id || !tag) {
return;
}
const context = { workerId, seq, op, id, tag };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--update-tags", `[${ednString(tag)}]`],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "block-tags-remove": {
const id = choose(state.activeIds);
const tag = stressTag(state);
if (!id || !tag) {
return;
}
const context = { workerId, seq, op, id, tag };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--remove-tags", `[${ednString(tag)}]`],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "block-properties-add": {
const id = choose(state.activeIds);
const property = stressProperty(state);
if (!id || !property) {
return;
}
const context = { workerId, seq, op, id, property };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--update-properties", propertyMap(property, `value ${stamp}`)],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "block-properties-remove": {
const id = choose(state.activeIds);
const property = stressProperty(state);
if (!id || !property) {
return;
}
const context = { workerId, seq, op, id, property };
const response = await runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--remove-properties", propertyVector(property)],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "task-create": {
const page = randomPage(state);
const response = await runCli(
opts,
[
"upsert",
"task",
"--graph",
opts.graph,
"--target-page",
page,
"--content",
`task ${stamp}`,
"--status",
"todo",
"--priority",
"high",
],
{ workerId, seq, op, page },
);
for (const id of resultIds(response)) {
state.activeIds.add(id);
state.taskIds.add(id);
}
return;
}
case "task-update": {
const id = choose(state.taskIds.size > 0 ? state.taskIds : state.activeIds);
if (!id) {
return;
}
const context = { workerId, seq, op, id };
const response = await runCli(
opts,
["upsert", "task", "--graph", opts.graph, "--id", String(id), "--status", "doing", "--priority", "low"],
context,
);
pruneStaleIds(state, response, context);
return;
}
case "page-upsert": {
await runCli(opts, ["upsert", "page", "--graph", opts.graph, "--page", randomPage(state)], {
workerId,
seq,
op,
});
return;
}
case "page-properties-add": {
const page = randomPage(state);
const property = stressProperty(state);
await runCli(
opts,
["upsert", "page", "--graph", opts.graph, "--page", page, "--update-properties", propertyMap(property, `page ${stamp}`)],
{ workerId, seq, op, page, property },
);
return;
}
case "page-properties-remove": {
const page = randomPage(state);
const property = stressProperty(state);
await runCli(
opts,
["upsert", "page", "--graph", opts.graph, "--page", page, "--remove-properties", propertyVector(property)],
{ workerId, seq, op, page, property },
);
return;
}
case "page-delete-restore": {
const page = mutablePage(state);
await runCli(opts, ["remove", "page", "--graph", opts.graph, "--page", page], {
workerId,
seq,
op,
page,
phase: "delete",
});
await runCli(opts, ["upsert", "page", "--graph", opts.graph, "--page", page, "--restore"], {
workerId,
seq,
op,
page,
phase: "restore",
});
await refreshLiveIds(opts, state, "page-delete-restore");
return;
}
case "tag-upsert": {
const tag = chooseArray([...state.stableTags, state.volatileTag]);
await runCli(opts, ["upsert", "tag", "--graph", opts.graph, "--name", tag], {
workerId,
seq,
op,
tag,
});
return;
}
case "tag-delete-recreate": {
const tag = state.volatileTag;
await runCli(opts, ["remove", "tag", "--graph", opts.graph, "--name", tag], {
workerId,
seq,
op,
tag,
phase: "delete",
});
await runCli(opts, ["upsert", "tag", "--graph", opts.graph, "--name", tag], {
workerId,
seq,
op,
tag,
phase: "recreate",
});
return;
}
case "property-upsert": {
const property = chooseArray([...state.stableProperties, state.volatileProperty]);
await runCli(
opts,
["upsert", "property", "--graph", opts.graph, "--name", property, "--type", "default", "--cardinality", "one", "--public", "true"],
{ workerId, seq, op, property },
);
return;
}
case "property-delete-recreate": {
const property = state.volatileProperty;
await runCli(opts, ["remove", "property", "--graph", opts.graph, "--name", property], {
workerId,
seq,
op,
property,
phase: "delete",
});
await runCli(
opts,
["upsert", "property", "--graph", opts.graph, "--name", property, "--type", "default", "--cardinality", "one", "--public", "true"],
{ workerId, seq, op, property, phase: "recreate" },
);
return;
}
case "http-insert-blocks":
await applyRawInsertBlocks(opts, state, { workerId, seq, op }, `http insert ${stamp}`);
return;
case "http-delete-blocks":
await applyRawDeleteBlocks(opts, state, { workerId, seq, op });
return;
case "http-move-blocks":
await applyRawMoveBlocks(opts, state, { workerId, seq, op });
return;
case "http-save-block":
await applyRawSaveBlock(opts, state, { workerId, seq, op }, `http save ${stamp}`);
return;
case "http-move-up-down":
await applyRawMoveUpDown(opts, state, { workerId, seq, op }, Math.random() < 0.5);
return;
case "http-indent-outdent":
await applyRawIndentOutdent(opts, state, { workerId, seq, op }, Math.random() < 0.5);
return;
case "http-apply-template":
await applyRawApplyTemplate(opts, state, { workerId, seq, op }, `http template ${stamp}`);
return;
case "http-create-page":
await applyRawCreatePage(
opts,
state,
{ workerId, seq, op },
uniqueOperationPageTitle("CLI HTTP Page", state, { workerId, seq, op }),
);
return;
case "http-rename-page":
await applyRawRenamePage(opts, state, { workerId, seq, op }, `CLI HTTP Rename ${state.runId} ${seq}`);
return;
case "http-delete-restore-page":
await applyRawDeleteRestorePage(opts, state, { workerId, seq, op });
return;
case "http-recycle-delete-page":
await applyRawRecycleDeletePage(
opts,
state,
{ workerId, seq, op },
uniqueOperationPageTitle("CLI HTTP Recycle", state, { workerId, seq, op }),
);
return;
case "http-upsert-property":
await applyRawUpsertProperty(opts, state, { workerId, seq, op });
return;
case "http-set-block-property":
await applyRawSetBlockProperty(opts, state, { workerId, seq, op }, `http property ${stamp}`);
return;
case "http-remove-block-property":
await applyRawRemoveBlockProperty(opts, state, { workerId, seq, op });
return;
case "http-batch-set-property":
await applyRawBatchSetProperty(opts, state, { workerId, seq, op }, `http batch ${stamp}`);
return;
case "http-batch-remove-property":
await applyRawBatchRemoveProperty(opts, state, { workerId, seq, op });
return;
case "http-batch-delete-property-value":
await applyRawBatchDeletePropertyValue(opts, state, { workerId, seq, op });
return;
case "http-create-property-history":
await applyRawCreatePropertyHistory(opts, state, { workerId, seq, op });
return;
case "http-create-linked-references-view":
await applyRawCreateReferenceView(opts, state, { workerId, seq, op }, "linked-references", "Linked references");
return;
case "http-create-unlinked-references-view":
await applyRawCreateReferenceView(opts, state, { workerId, seq, op }, "unlinked-references", "Unlinked references");
return;
case "http-toggle-reaction":
await applyRawToggleReaction(opts, state, { workerId, seq, op });
return;
case "http-delete-view-target-with-related-entities":
await applyRawDeleteViewTargetWithRelatedEntities(opts, state, { workerId, seq, op });
return;
case "http-insert-undo-redo":
await applyRawInsertUndoRedo(opts, state, { workerId, seq, op }, `http undo redo ${stamp}`);
return;
case "offline-window":
await offlineWindow(opts, state, { workerId, seq, op });
return;
case "offline-today-journal-race":
await offlineTodayJournalRace(opts, state, { workerId, seq, op });
return;
case "delete-single": {
const id = choose(state.activeIds);
if (!id) {
return;
}
const context = { workerId, seq, op, id };
const response = await runCli(opts, ["remove", "block", "--graph", opts.graph, "--id", String(id)], context);
pruneStaleIds(state, response, context);
state.activeIds.delete(id);
return;
}
case "delete-batch": {
const ids = chooseMany(state.activeIds, 4);
if (ids.length === 0) {
return;
}
const context = {
workerId,
seq,
op,
ids,
};
const response = await runCli(opts, ["remove", "block", "--graph", opts.graph, "--id", JSON.stringify(ids)], context);
pruneStaleIds(state, response, context);
for (const id of ids) {
state.activeIds.delete(id);
}
return;
}
case "delete-update-race": {
const id = choose(state.activeIds);
if (!id) {
return;
}
const deleteContext = {
workerId,
seq,
op: `${op}:delete`,
id,
};
const updateContext = { workerId, seq, op: `${op}:update`, id };
const responses = await Promise.all([
runCli(opts, ["remove", "block", "--graph", opts.graph, "--id", String(id)], deleteContext),
runCli(
opts,
["upsert", "block", "--graph", opts.graph, "--id", String(id), "--content", `race update ${stamp} id=${id}`],
updateContext,
),
]);
pruneStaleIds(state, responses[0], deleteContext);
pruneStaleIds(state, responses[1], updateContext);
state.activeIds.delete(id);
return;
}
default:
throw new Error(`Unhandled operation: ${op}`);
}
}
async function worker(opts, state, workerId) {
let localSeq = 0;
while (!state.stop) {
const seq = state.nextSeq;
state.nextSeq += 1;
if (opts.maxOps > 0 && seq >= opts.maxOps) {
state.stop = true;
break;
}
try {
await runOperation(opts, state, workerId, localSeq);
} catch (error) {
logEvent(opts, {
event: "worker-error",
level: "error",
workerId,
seq: localSeq,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
if (opts.failFast) {
state.stop = true;
throw error;
}
}
localSeq += 1;
if (localSeq % 25 === 0) {
logEvent(opts, {
event: "summary",
level: "info",
workerId,
localSeq,
totalOpsStarted: state.nextSeq,
activeIds: state.activeIds.size,
});
}
await delay(Math.floor(Math.random() * 40));
}
}
async function clusterWorker(opts, cluster, workerId) {
let localSeq = 0;
while (!cluster.stop) {
const seq = cluster.nextSeq;
cluster.nextSeq += 1;
if (opts.maxOps > 0 && seq >= opts.maxOps) {
cluster.stop = true;
break;
}
const op = weightedOperation(opts);
try {
if (op === "multi-client-offline-rebase") {
await multiClientOfflineRebase(opts, cluster, { workerId, seq, op });
} else if (op === "multi-client-today-journal-race") {
await multiClientTodayJournalRace(opts, cluster, { workerId, seq, op });
} else {
const client = chooseArray(cluster.clients);
client.state.nextSeq += 1;
await runOperation(client.opts, client.state, workerId, localSeq, op);
}
} catch (error) {
logEvent(opts, {
event: "worker-error",
level: "error",
workerId,
seq: localSeq,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
if (opts.failFast) {
cluster.stop = true;
throw error;
}
}
localSeq += 1;
if (localSeq % 25 === 0) {
logEvent(opts, {
event: "summary",
level: "info",
workerId,
localSeq,
totalOpsStarted: cluster.nextSeq,
activeIds: cluster.clients.reduce((sum, client) => sum + client.state.activeIds.size, 0),
clients: cluster.clients.length,
});
}
await delay(Math.floor(Math.random() * 40));
}
}
function createStressState(opts, runId) {
const state = {
runId,
pageNames: stressPageNames(opts),
bootstrapPageNames: bootstrapStressPageNames(opts),
mutablePageNames: [],
stableTags: ["cli-stress-tag-a", "cli-stress-tag-b"],
stableTagIds: new Map(),
stableProperties: ["cli-stress-prop-a", "cli-stress-prop-b"],
volatileTag: "cli-stress-volatile-tag",
volatileProperty: "cli-stress-volatile-prop",
rawPropertyIdent: "user.property/cli-http-prop",
rawPropertyName: "cli-http-prop",
activeIds: new Set(),
activeBlockUuids: new Map(),
taskIds: new Set(),
nextSeq: 0,
lastRefreshSeq: -Infinity,
refreshing: false,
offlineRunning: false,
stop: false,
};
state.mutablePageNames = mutableStressPageNames(opts);
return state;
}
async function validateClientGraph(client, phase) {
const validation = await runCli(client.opts, graphValidateArgs(client.opts), {
op: `${phase}-graph-validate`,
client: client.opts.clientName,
});
if (!validation.ok) {
throw new Error(`${phase} graph validation failed for ${client.opts.graph} on ${client.opts.clientName}`);
}
}
async function buildSeedClient(opts, runId) {
const clientOpts = clientRuntimeOptions(opts, 0);
ensureStressConfig(clientOpts);
const graphServer = await ensureLocalServers(clientOpts);
const state = createStressState(clientOpts, runId);
state.workerBaseUrl = graphServer["base-url"];
state.repo = graphServerRepo(clientOpts, graphServer);
await validateClientGraph({ opts: clientOpts }, "preflight");
await ensureStressMetadata(clientOpts, state);
await refreshLiveIds(clientOpts, state, "startup");
await waitForSyncSettled(clientOpts, { op: "seed-startup-settle", client: clientOpts.clientName }, false);
return { opts: clientOpts, state, graphServer };
}
async function buildReplicaClient(opts, clientIndex, runId) {
const clientOpts = clientRuntimeOptions(opts, clientIndex);
ensureStressConfig(clientOpts);
const graphServer = await ensureReplicaLocalServers(clientOpts);
const state = createStressState(clientOpts, runId);
state.workerBaseUrl = graphServer["base-url"];
state.repo = graphServerRepo(clientOpts, graphServer);
await validateClientGraph({ opts: clientOpts }, "preflight");
await waitForSyncSettled(clientOpts, { op: "replica-startup-settle", client: clientOpts.clientName }, false);
await refreshStressTagIds(clientOpts, state, "replica-startup");
await refreshLiveIds(clientOpts, state, "replica-startup");
return { opts: clientOpts, state, graphServer };
}
async function buildStressClients(opts, runId) {
const clients = [await buildSeedClient(opts, runId)];
for (let index = 1; index < opts.clients; index += 1) {
clients.push(await buildReplicaClient(opts, index, runId));
}
return clients;
}
async function main() {
const opts = parseArgs(process.argv.slice(2));
mkdirSync(dirname(opts.logFile), { recursive: true });
const runId = `stress-${Date.now()}`;
logEvent(opts, {
event: "start",
level: "info",
runId,
config: opts.config,
httpBase: opts.httpBase,
wsUrl: opts.wsUrl,
clients: opts.clients,
concurrency: opts.concurrency,
maxOps: opts.maxOps,
sync: opts.sync,
offline: opts.offline,
graphE2ee: opts.graphE2ee,
pageNames: stressPageNames(opts),
});
const clients = await buildStressClients(opts, runId);
let totalOpsStarted = clients[0]?.state.nextSeq || 0;
if (clients.length === 1) {
await Promise.all(Array.from({ length: opts.concurrency }, (_, index) => worker(clients[0].opts, clients[0].state, index + 1)));
totalOpsStarted = clients[0].state.nextSeq;
} else {
const cluster = {
clients,
nextSeq: 0,
stop: false,
offlineRunning: false,
};
await Promise.all(Array.from({ length: opts.concurrency }, (_, index) => clusterWorker(opts, cluster, index + 1)));
totalOpsStarted = cluster.nextSeq;
}
for (const client of clients) {
await waitForSyncSettled(client.opts, { op: "final-sync-settle", client: client.opts.clientName }, true);
await validateClientChecksum(client);
await validateClientGraph(client, "final");
}
logEvent(opts, {
event: "stop",
level: "info",
runId,
totalOpsStarted,
activeIds: clients.reduce((sum, client) => sum + client.state.activeIds.size, 0),
clients: clients.length,
});
}
function isMainModule() {
return process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
}
if (isMainModule()) {
main().catch((error) => {
const opts = (() => {
try {
return parseArgs(process.argv.slice(2));
} catch {
return {
graph: "pi-memory",
page: "CLI Concurrent Stress",
logFile: resolve(repoRoot, "tmp", "cli-concurrent-edit-stress", "events.jsonl"),
};
}
})();
mkdirSync(dirname(opts.logFile), { recursive: true });
logEvent(opts, {
event: "fatal",
level: "error",
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
process.exitCode = 1;
});
}