goldsync 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +5 -0
- package/README.md +129 -0
- package/assets/GoldSync.rbxmx +2516 -0
- package/bin/goldsync-agent.mjs +54 -0
- package/bin/install-companion.mjs +96 -0
- package/package.json +42 -0
- package/src/broker.mjs +306 -0
- package/src/companion.mjs +220 -0
- package/src/config.mjs +165 -0
- package/src/git-conflicts.mjs +127 -0
- package/src/rojo-session.mjs +165 -0
- package/src/server.mjs +308 -0
- package/src/session-marker.mjs +27 -0
- package/src/store.mjs +337 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { CompanionAgent } from "../src/companion.mjs";
|
|
7
|
+
|
|
8
|
+
const dataDirectory = process.env.GOLDSYNC_DATA_DIRECTORY
|
|
9
|
+
? path.resolve(process.env.GOLDSYNC_DATA_DIRECTORY)
|
|
10
|
+
: path.join(process.env.LOCALAPPDATA, "GoldSync");
|
|
11
|
+
const pidFile = path.join(dataDirectory, "agent.pid");
|
|
12
|
+
const logFile = path.join(dataDirectory, "agent.log");
|
|
13
|
+
|
|
14
|
+
await mkdir(dataDirectory, { recursive: true });
|
|
15
|
+
try {
|
|
16
|
+
const existingPid = Number(await readFile(pidFile, "utf8"));
|
|
17
|
+
if (Number.isInteger(existingPid)) {
|
|
18
|
+
process.kill(existingPid, 0);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// The previous agent is not running.
|
|
23
|
+
}
|
|
24
|
+
await writeFile(pidFile, String(process.pid));
|
|
25
|
+
|
|
26
|
+
function log(message) {
|
|
27
|
+
appendFile(logFile, `[${new Date().toISOString()}] ${message}\n`).catch(() => {});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const agent = new CompanionAgent({
|
|
31
|
+
registryFile: path.join(dataDirectory, "projects.json"),
|
|
32
|
+
log,
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
await agent.start();
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log(error.stack ?? error.message);
|
|
38
|
+
await rm(pidFile, { force: true });
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
log("GoldSync companion started");
|
|
42
|
+
|
|
43
|
+
let stopping = false;
|
|
44
|
+
async function stop() {
|
|
45
|
+
if (stopping) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
stopping = true;
|
|
49
|
+
await agent.stop();
|
|
50
|
+
await rm(pidFile, { force: true });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.once("SIGINT", () => stop().then(() => process.exit(0)));
|
|
54
|
+
process.once("SIGTERM", () => stop().then(() => process.exit(0)));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { access, copyFile, cp, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { findProjectConfig, registerProject } from "../src/companion.mjs";
|
|
9
|
+
|
|
10
|
+
const command = process.argv[2];
|
|
11
|
+
if (command === "--help" || command === "-h") {
|
|
12
|
+
console.log("Run 'goldsync install' from a repository containing goldsync.project.json.");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
}
|
|
15
|
+
if (command === "--version" || command === "-v") {
|
|
16
|
+
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
17
|
+
console.log(packageJson.version);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
if (command && command !== "install" && !command.startsWith("--")) {
|
|
21
|
+
throw new Error(`Unknown GoldSync command: ${command}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (process.platform !== "win32") {
|
|
25
|
+
throw new Error("The automatic GoldSync companion installer currently supports Windows only.");
|
|
26
|
+
}
|
|
27
|
+
if (!process.env.LOCALAPPDATA || !process.env.APPDATA) {
|
|
28
|
+
throw new Error("Windows application-data folders are unavailable.");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
|
+
const configIndex = process.argv.indexOf("--config");
|
|
33
|
+
const projectIndex = process.argv.indexOf("--project");
|
|
34
|
+
const projectDirectory = projectIndex === -1 ? process.cwd() : path.resolve(process.argv[projectIndex + 1]);
|
|
35
|
+
const configPath = configIndex === -1
|
|
36
|
+
? await findProjectConfig(projectDirectory)
|
|
37
|
+
: path.resolve(process.argv[configIndex + 1]);
|
|
38
|
+
|
|
39
|
+
const dataDirectory = path.join(process.env.LOCALAPPDATA, "GoldSync");
|
|
40
|
+
const runtimeDirectory = path.join(dataDirectory, "runtime");
|
|
41
|
+
await mkdir(path.join(runtimeDirectory, "bin"), { recursive: true });
|
|
42
|
+
await cp(path.join(packageDirectory, "src"), path.join(runtimeDirectory, "src"), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
force: true,
|
|
45
|
+
});
|
|
46
|
+
await copyFile(path.join(packageDirectory, "bin", "goldsync-agent.mjs"), path.join(runtimeDirectory, "bin", "goldsync-agent.mjs"));
|
|
47
|
+
await copyFile(path.join(packageDirectory, "package.json"), path.join(runtimeDirectory, "package.json"));
|
|
48
|
+
const runtimeNode = path.join(runtimeDirectory, `node-${process.versions.node}.exe`);
|
|
49
|
+
try {
|
|
50
|
+
await access(runtimeNode);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT") {
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
await copyFile(process.execPath, runtimeNode);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const pluginSource = path.join(packageDirectory, "assets", "GoldSync.rbxmx");
|
|
59
|
+
const developmentPluginSource = path.join(packageDirectory, "extension", "assets", "GoldSync.rbxmx");
|
|
60
|
+
const pluginDirectory = path.join(process.env.LOCALAPPDATA, "Roblox", "Plugins");
|
|
61
|
+
await mkdir(pluginDirectory, { recursive: true });
|
|
62
|
+
try {
|
|
63
|
+
await copyFile(pluginSource, path.join(pluginDirectory, "GoldSync.rbxmx"));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (error.code !== "ENOENT") {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
await copyFile(developmentPluginSource, path.join(pluginDirectory, "GoldSync.rbxmx"));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const config = await registerProject(path.join(dataDirectory, "projects.json"), configPath);
|
|
72
|
+
const installedAgent = path.join(runtimeDirectory, "bin", "goldsync-agent.mjs");
|
|
73
|
+
const startupCommand = `"${runtimeNode}" "${installedAgent}"`;
|
|
74
|
+
const vbsString = `"${startupCommand.replaceAll('"', '""')}"`;
|
|
75
|
+
const startupFile = path.join(
|
|
76
|
+
process.env.APPDATA,
|
|
77
|
+
"Microsoft",
|
|
78
|
+
"Windows",
|
|
79
|
+
"Start Menu",
|
|
80
|
+
"Programs",
|
|
81
|
+
"Startup",
|
|
82
|
+
"GoldSync.vbs",
|
|
83
|
+
);
|
|
84
|
+
await mkdir(path.dirname(startupFile), { recursive: true });
|
|
85
|
+
await writeFile(
|
|
86
|
+
startupFile,
|
|
87
|
+
`Set GoldSyncShell = CreateObject("WScript.Shell")\r\nGoldSyncShell.Run ${vbsString}, 0, False\r\n`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
spawn(runtimeNode, [installedAgent], {
|
|
91
|
+
detached: true,
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
}).unref();
|
|
95
|
+
|
|
96
|
+
console.log(`GoldSync is installed for ${config.name}. Restart Roblox Studio once.`);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "goldsync",
|
|
3
|
+
"version": "0.1.11",
|
|
4
|
+
"description": "Automatic native Roblox asset synchronization for Rojo projects.",
|
|
5
|
+
"author": "tay",
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE.txt",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"goldsync": "bin/install-companion.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"assets/GoldSync.rbxmx",
|
|
13
|
+
"bin/goldsync-agent.mjs",
|
|
14
|
+
"bin/install-companion.mjs",
|
|
15
|
+
"LICENSE.txt",
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"package:companion": "node scripts/package-companion.mjs",
|
|
22
|
+
"prepack": "node scripts/prepare-npm-package.mjs",
|
|
23
|
+
"postpack": "node scripts/cleanup-npm-package.mjs"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"os": [
|
|
29
|
+
"win32"
|
|
30
|
+
],
|
|
31
|
+
"keywords": [
|
|
32
|
+
"roblox",
|
|
33
|
+
"rojo",
|
|
34
|
+
"rbxm",
|
|
35
|
+
"sync",
|
|
36
|
+
"zed",
|
|
37
|
+
"cursor"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/broker.mjs
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { loadConfig } from "./config.mjs";
|
|
4
|
+
import { GitConflictStore } from "./git-conflicts.mjs";
|
|
5
|
+
import { findRojoSession } from "./rojo-session.mjs";
|
|
6
|
+
import { GoldSyncServer } from "./server.mjs";
|
|
7
|
+
import { readSessionMarker, writeSessionMarker } from "./session-marker.mjs";
|
|
8
|
+
import { SnapshotStore } from "./store.mjs";
|
|
9
|
+
|
|
10
|
+
const MAX_MANAGEMENT_BODY_BYTES = 64 * 1024;
|
|
11
|
+
const MANAGEMENT_CLIENTS = new Set(["vscode-extension", "goldsync-companion"]);
|
|
12
|
+
|
|
13
|
+
function sendJson(response, status, value) {
|
|
14
|
+
const body = Buffer.from(JSON.stringify(value));
|
|
15
|
+
response.writeHead(status, {
|
|
16
|
+
"Content-Type": "application/json",
|
|
17
|
+
"Content-Length": body.length,
|
|
18
|
+
"Cache-Control": "no-store",
|
|
19
|
+
});
|
|
20
|
+
response.end(body);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readJson(request) {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
let size = 0;
|
|
26
|
+
for await (const chunk of request) {
|
|
27
|
+
size += chunk.length;
|
|
28
|
+
if (size > MAX_MANAGEMENT_BODY_BYTES) {
|
|
29
|
+
const error = new Error("management request is too large");
|
|
30
|
+
error.statusCode = 413;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
chunks.push(chunk);
|
|
34
|
+
}
|
|
35
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
class WorkspaceRuntime {
|
|
39
|
+
constructor(config, configPath, modifiedAt) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
this.configPath = configPath;
|
|
42
|
+
this.modifiedAt = modifiedAt;
|
|
43
|
+
this.clients = new Map();
|
|
44
|
+
this.rojoConnected = false;
|
|
45
|
+
this.rojoPort = null;
|
|
46
|
+
this.rojoError = null;
|
|
47
|
+
this.checkingRojo = false;
|
|
48
|
+
this.conflictPinnedBy = null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async start() {
|
|
52
|
+
this.sessionToken = await readSessionMarker(this.config.sessionMarkerFile);
|
|
53
|
+
if (this.sessionToken === null) {
|
|
54
|
+
await writeSessionMarker(this.config.sessionMarkerFile, "");
|
|
55
|
+
}
|
|
56
|
+
this.store = new SnapshotStore(this.config);
|
|
57
|
+
await this.store.initialize();
|
|
58
|
+
this.gitConflicts = new GitConflictStore(this.config);
|
|
59
|
+
await this.gitConflicts.initialize();
|
|
60
|
+
this.gitConflicts.syncRoots(this.store.getRoots());
|
|
61
|
+
this.server = new GoldSyncServer(this.config, this.store, this.gitConflicts);
|
|
62
|
+
this.server.setSessionToken(this.sessionToken);
|
|
63
|
+
this.server.setSyncEnabled(false);
|
|
64
|
+
await this.server.start({ listen: false });
|
|
65
|
+
await this.checkRojo();
|
|
66
|
+
this.rojoTimer = setInterval(() => this.checkRojo(), 750);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async checkRojo() {
|
|
70
|
+
if (this.checkingRojo) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.checkingRojo = true;
|
|
74
|
+
try {
|
|
75
|
+
const session = await findRojoSession(this.config.rojo);
|
|
76
|
+
this.rojoConnected = session !== null && !session.ambiguous;
|
|
77
|
+
this.rojoPort = this.rojoConnected ? session.port : null;
|
|
78
|
+
this.rojoError = session?.ambiguous
|
|
79
|
+
? `multiple Rojo sessions for ${this.config.rojo.projectName} are active on ports ${session.ports.join(", ")}`
|
|
80
|
+
: null;
|
|
81
|
+
if (this.rojoConnected) {
|
|
82
|
+
const sessionToken = `${this.config.projectId}:${session.sessionId}`;
|
|
83
|
+
if (sessionToken !== this.sessionToken) {
|
|
84
|
+
await writeSessionMarker(this.config.sessionMarkerFile, sessionToken);
|
|
85
|
+
this.sessionToken = sessionToken;
|
|
86
|
+
this.server.setSessionToken(sessionToken);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.rojoConnected = false;
|
|
91
|
+
this.rojoPort = null;
|
|
92
|
+
this.rojoError = error.message;
|
|
93
|
+
}
|
|
94
|
+
this.server.setSyncEnabled(this.rojoConnected);
|
|
95
|
+
this.checkingRojo = false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
touch(clientId) {
|
|
99
|
+
this.clients.set(clientId, Date.now());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
removeExpiredClients(cutoff) {
|
|
103
|
+
for (const [clientId, lastSeen] of this.clients) {
|
|
104
|
+
if (lastSeen < cutoff) {
|
|
105
|
+
this.clients.delete(clientId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async stop() {
|
|
111
|
+
clearInterval(this.rojoTimer);
|
|
112
|
+
await this.server.stop();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async summary() {
|
|
116
|
+
const conflicts = [...(await this.gitConflicts.list()).values()].map((conflict) => ({
|
|
117
|
+
id: conflict.id,
|
|
118
|
+
file: conflict.file,
|
|
119
|
+
token: conflict.token,
|
|
120
|
+
}));
|
|
121
|
+
if (conflicts.length === 0) {
|
|
122
|
+
this.conflictPinnedBy = null;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
name: this.config.name,
|
|
126
|
+
projectId: this.config.projectId,
|
|
127
|
+
workspaceId: this.config.workspaceId,
|
|
128
|
+
configPath: this.configPath,
|
|
129
|
+
rojoConnected: this.rojoConnected,
|
|
130
|
+
rojoPort: this.rojoPort,
|
|
131
|
+
rojoError: this.rojoError,
|
|
132
|
+
conflictPinned: this.conflictPinnedBy !== null && this.clients.has(this.conflictPinnedBy),
|
|
133
|
+
conflicts,
|
|
134
|
+
clients: this.clients.size,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async pinConflicts(clientId) {
|
|
139
|
+
if (!this.clients.has(clientId)) {
|
|
140
|
+
const error = new Error("the requesting editor workspace is no longer registered");
|
|
141
|
+
error.statusCode = 403;
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
if ((await this.gitConflicts.list()).size === 0) {
|
|
145
|
+
const error = new Error("this workspace no longer has GoldSync merge conflicts");
|
|
146
|
+
error.statusCode = 409;
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
this.conflictPinnedBy = clientId;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isConflictPinned() {
|
|
153
|
+
return this.conflictPinnedBy !== null && this.clients.has(this.conflictPinnedBy);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export class GoldSyncBroker {
|
|
158
|
+
constructor(options = {}) {
|
|
159
|
+
this.host = options.host ?? "127.0.0.1";
|
|
160
|
+
this.port = options.port ?? 34873;
|
|
161
|
+
this.leaseMs = options.leaseMs ?? 15000;
|
|
162
|
+
this.workspaces = new Map();
|
|
163
|
+
this.server = createServer((request, response) => {
|
|
164
|
+
this.handle(request, response).catch((error) => {
|
|
165
|
+
sendJson(response, error.statusCode ?? 500, { error: error.message });
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async start() {
|
|
171
|
+
await new Promise((resolve, reject) => {
|
|
172
|
+
this.server.once("error", reject);
|
|
173
|
+
this.server.listen(this.port, this.host, () => {
|
|
174
|
+
this.server.off("error", reject);
|
|
175
|
+
resolve();
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
this.reaper = setInterval(() => this.reap(), 5000);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async register(configPath, clientId) {
|
|
182
|
+
if (typeof configPath !== "string" || configPath.length === 0) {
|
|
183
|
+
throw new Error("configPath is required");
|
|
184
|
+
}
|
|
185
|
+
if (typeof clientId !== "string" || clientId.length === 0 || clientId.length > 128) {
|
|
186
|
+
throw new Error("clientId is required");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const modifiedAt = (await stat(configPath)).mtimeMs;
|
|
190
|
+
let runtime = [...this.workspaces.values()].find(
|
|
191
|
+
(candidate) => candidate.configPath.toLowerCase() === configPath.toLowerCase(),
|
|
192
|
+
);
|
|
193
|
+
if (runtime && runtime.modifiedAt === modifiedAt) {
|
|
194
|
+
runtime.touch(clientId);
|
|
195
|
+
this.lastRegistration = Date.now();
|
|
196
|
+
return runtime.summary();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const config = await loadConfig(configPath);
|
|
200
|
+
runtime = this.workspaces.get(config.workspaceId);
|
|
201
|
+
if (!runtime || runtime.modifiedAt !== modifiedAt) {
|
|
202
|
+
if (runtime) {
|
|
203
|
+
await runtime.stop();
|
|
204
|
+
}
|
|
205
|
+
runtime = new WorkspaceRuntime(config, configPath, modifiedAt);
|
|
206
|
+
await runtime.start();
|
|
207
|
+
this.workspaces.set(config.workspaceId, runtime);
|
|
208
|
+
console.log(`[GoldSync] registered ${config.name} (${config.workspaceId})`);
|
|
209
|
+
}
|
|
210
|
+
runtime.touch(clientId);
|
|
211
|
+
this.lastRegistration = Date.now();
|
|
212
|
+
return runtime.summary();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async reap() {
|
|
216
|
+
const cutoff = Date.now() - this.leaseMs;
|
|
217
|
+
for (const [workspaceId, runtime] of this.workspaces) {
|
|
218
|
+
runtime.removeExpiredClients(cutoff);
|
|
219
|
+
if (runtime.clients.size === 0) {
|
|
220
|
+
await runtime.stop();
|
|
221
|
+
this.workspaces.delete(workspaceId);
|
|
222
|
+
console.log(`[GoldSync] released ${runtime.config.name} (${workspaceId})`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async stop() {
|
|
228
|
+
clearInterval(this.reaper);
|
|
229
|
+
for (const runtime of this.workspaces.values()) {
|
|
230
|
+
await runtime.stop();
|
|
231
|
+
}
|
|
232
|
+
this.workspaces.clear();
|
|
233
|
+
if (this.server.listening) {
|
|
234
|
+
await new Promise((resolve) => this.server.close(resolve));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async handle(request, response) {
|
|
239
|
+
const url = new URL(request.url, `http://${this.host}:${this.port}`);
|
|
240
|
+
const managementRequest = MANAGEMENT_CLIENTS.has(request.headers["x-goldsync-client"]);
|
|
241
|
+
if (managementRequest && request.method === "GET" && url.pathname === "/v1/health") {
|
|
242
|
+
sendJson(response, 200, {
|
|
243
|
+
version: 1,
|
|
244
|
+
broker: true,
|
|
245
|
+
workspaces: await Promise.all([...this.workspaces.values()].map((runtime) => runtime.summary())),
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (managementRequest && request.method === "POST" && url.pathname === "/v1/workspaces/register") {
|
|
250
|
+
const body = await readJson(request);
|
|
251
|
+
sendJson(response, 200, await this.register(body.configPath, body.clientId));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (managementRequest && request.method === "POST" && url.pathname === "/v1/workspaces/pin-conflicts") {
|
|
255
|
+
const body = await readJson(request);
|
|
256
|
+
const runtime = this.workspaces.get(body.workspaceId);
|
|
257
|
+
if (!runtime) {
|
|
258
|
+
sendJson(response, 404, { error: "GoldSync workspace is no longer open" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
await runtime.pinConflicts(body.clientId);
|
|
262
|
+
for (const candidate of this.workspaces.values()) {
|
|
263
|
+
if (candidate !== runtime) {
|
|
264
|
+
candidate.conflictPinnedBy = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
sendJson(response, 200, await runtime.summary());
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (request.headers["x-goldsync-client"] !== "studio-plugin") {
|
|
272
|
+
sendJson(response, 403, { error: "request was not sent by GoldSync" });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const candidates = [...this.workspaces.values()];
|
|
277
|
+
const sessionToken = request.headers["x-goldsync-session"];
|
|
278
|
+
if (typeof sessionToken !== "string" || sessionToken.length === 0) {
|
|
279
|
+
sendJson(response, 409, {
|
|
280
|
+
error: "this Studio place is not connected to a GoldSync-enabled Rojo session",
|
|
281
|
+
});
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const matching = candidates.filter((runtime) => runtime.sessionToken === sessionToken);
|
|
285
|
+
const connected = matching.filter((runtime) => runtime.rojoConnected);
|
|
286
|
+
const pinned = matching.filter((runtime) => runtime.isConflictPinned());
|
|
287
|
+
const ambiguous = matching.find((runtime) => runtime.rojoError);
|
|
288
|
+
const selected = connected.length === 1 ? connected[0] : connected.length === 0 && pinned.length === 1 ? pinned[0] : null;
|
|
289
|
+
if (!selected) {
|
|
290
|
+
sendJson(response, candidates.length === 0 ? 404 : 409, {
|
|
291
|
+
error:
|
|
292
|
+
candidates.length === 0
|
|
293
|
+
? "no GoldSync project is registered on this computer"
|
|
294
|
+
: matching.length === 0
|
|
295
|
+
? "this Studio place belongs to a different or expired Rojo session"
|
|
296
|
+
: connected.length > 1
|
|
297
|
+
? "multiple GoldSync workspaces claim the same Rojo session"
|
|
298
|
+
: ambiguous
|
|
299
|
+
? ambiguous.rojoError
|
|
300
|
+
: "Rojo is offline; open the GoldSync conflict viewer from a supported editor",
|
|
301
|
+
});
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
await selected.server.handle(request, response);
|
|
305
|
+
}
|
|
306
|
+
}
|