@crowdedkingdoms/crowdyjs 9.0.0 → 10.0.0
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/MIGRATION.md +56 -34
- package/README.md +83 -50
- package/dist/crowdy-client.d.ts +3 -0
- package/dist/crowdy-client.d.ts.map +1 -1
- package/dist/crowdy-client.js +2 -0
- package/dist/domains/playerCodeProjects.d.ts +25 -0
- package/dist/domains/playerCodeProjects.d.ts.map +1 -0
- package/dist/domains/playerCodeProjects.js +299 -0
- package/dist/domains/playerCompute.d.ts +1 -1
- package/dist/domains/playerCompute.js +1 -1
- package/dist/generated/graphql.d.ts +792 -7
- package/dist/generated/graphql.d.ts.map +1 -1
- package/dist/generated/graphql.js +57 -0
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/kit/npcs.d.ts.map +1 -1
- package/dist/kit/social.d.ts.map +1 -1
- package/dist/live-coding/rust-analysis.d.ts.map +1 -1
- package/dist/live-coding/rust-analysis.js +64 -1
- package/dist/live-coding/worker-transport.d.ts +3 -0
- package/dist/live-coding/worker-transport.d.ts.map +1 -1
- package/dist/live-coding/worker-transport.js +13 -0
- package/dist/mod-studio/controller.d.ts +188 -0
- package/dist/mod-studio/controller.d.ts.map +1 -0
- package/dist/mod-studio/controller.js +915 -0
- package/dist/mod-studio/diagnostics.d.ts +22 -0
- package/dist/mod-studio/diagnostics.d.ts.map +1 -0
- package/dist/mod-studio/diagnostics.js +141 -0
- package/dist/mod-studio/dom-shell.d.ts +55 -0
- package/dist/mod-studio/dom-shell.d.ts.map +1 -0
- package/dist/mod-studio/dom-shell.js +527 -0
- package/dist/mod-studio/editor.d.ts +17 -0
- package/dist/mod-studio/editor.d.ts.map +1 -0
- package/dist/mod-studio/editor.js +1 -0
- package/dist/mod-studio/index.d.ts +10 -0
- package/dist/mod-studio/index.d.ts.map +1 -0
- package/dist/mod-studio/index.js +9 -0
- package/dist/mod-studio/models.d.ts +149 -0
- package/dist/mod-studio/models.d.ts.map +1 -0
- package/dist/mod-studio/models.js +62 -0
- package/dist/mod-studio/monaco-editor.d.ts +16 -0
- package/dist/mod-studio/monaco-editor.d.ts.map +1 -0
- package/dist/mod-studio/monaco-editor.js +548 -0
- package/dist/mod-studio/mount.d.ts +17 -0
- package/dist/mod-studio/mount.d.ts.map +1 -0
- package/dist/mod-studio/mount.js +86 -0
- package/dist/mod-studio/starter-projects.d.ts +11 -0
- package/dist/mod-studio/starter-projects.d.ts.map +1 -0
- package/dist/mod-studio/starter-projects.js +102 -0
- package/dist/mod-studio/styles.d.ts +2 -0
- package/dist/mod-studio/styles.d.ts.map +1 -0
- package/dist/mod-studio/styles.js +14 -0
- package/dist/mod-studio/textarea-editor.d.ts +8 -0
- package/dist/mod-studio/textarea-editor.d.ts.map +1 -0
- package/dist/mod-studio/textarea-editor.js +71 -0
- package/package.json +4 -4
- package/dist/live-coding/ide.d.ts +0 -23
- package/dist/live-coding/ide.d.ts.map +0 -1
- package/dist/live-coding/ide.js +0 -514
- package/dist/live-coding/index.d.ts +0 -8
- package/dist/live-coding/index.d.ts.map +0 -1
- package/dist/live-coding/index.js +0 -7
- package/dist/live-coding/live-coding-controller.d.ts +0 -75
- package/dist/live-coding/live-coding-controller.d.ts.map +0 -1
- package/dist/live-coding/live-coding-controller.js +0 -141
- package/dist/live-coding/mount.d.ts +0 -26
- package/dist/live-coding/mount.d.ts.map +0 -1
- package/dist/live-coding/mount.js +0 -108
- package/dist/live-coding/templates.d.ts +0 -17
- package/dist/live-coding/templates.d.ts.map +0 -1
- package/dist/live-coding/templates.js +0 -59
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { PlayerCodeBroker, } from '../player-runtime/player-code-broker.js';
|
|
2
|
+
import { parseRustcDiagnostics } from './diagnostics.js';
|
|
3
|
+
import { cloneModStudioProject, modStudioFileKey, normalizeModStudioPath, projectTargets, ModStudioOfflineError, ModStudioRevisionConflictError, } from './models.js';
|
|
4
|
+
import { createModStudioStarterProject, } from './starter-projects.js';
|
|
5
|
+
class OperationCancelledError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Headless project-first Mod Studio driver. It owns optimistic project saves,
|
|
9
|
+
* file CRUD, deployment ordering, runtime polling, and client hot swaps; the
|
|
10
|
+
* DOM mount is only a view over this state.
|
|
11
|
+
*/
|
|
12
|
+
export class ModStudioController {
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.state = {
|
|
16
|
+
projects: [],
|
|
17
|
+
project: null,
|
|
18
|
+
personalLibraryFiles: [],
|
|
19
|
+
commonFiles: [],
|
|
20
|
+
openFiles: [],
|
|
21
|
+
activeFile: null,
|
|
22
|
+
saveState: 'SAVED',
|
|
23
|
+
runtime: { phase: 'IDLE' },
|
|
24
|
+
buildOutput: '',
|
|
25
|
+
authoritativeDiagnostics: [],
|
|
26
|
+
localDiagnostics: [],
|
|
27
|
+
runs: [],
|
|
28
|
+
logs: [],
|
|
29
|
+
usage: null,
|
|
30
|
+
wallet: null,
|
|
31
|
+
invokeResult: null,
|
|
32
|
+
};
|
|
33
|
+
this.listeners = new Set();
|
|
34
|
+
this.autosaveTimer = null;
|
|
35
|
+
this.retryTimer = null;
|
|
36
|
+
this.savePromise = null;
|
|
37
|
+
this.editGeneration = 0;
|
|
38
|
+
this.persistedGeneration = 0;
|
|
39
|
+
this.conflictRemote = null;
|
|
40
|
+
this.broker = null;
|
|
41
|
+
this.operationGeneration = 0;
|
|
42
|
+
this.visibleSurfaces = new Set();
|
|
43
|
+
this.surfaceTimers = new Map();
|
|
44
|
+
this.pageVisible = true;
|
|
45
|
+
this.destroyed = false;
|
|
46
|
+
if (options.onStateChange)
|
|
47
|
+
this.listeners.add(options.onStateChange);
|
|
48
|
+
}
|
|
49
|
+
getState() {
|
|
50
|
+
return this.state;
|
|
51
|
+
}
|
|
52
|
+
subscribe(listener) {
|
|
53
|
+
this.listeners.add(listener);
|
|
54
|
+
listener(this.state);
|
|
55
|
+
return () => this.listeners.delete(listener);
|
|
56
|
+
}
|
|
57
|
+
canTarget(target, action) {
|
|
58
|
+
const permission = this.options.targetPermissions?.[target];
|
|
59
|
+
return permission
|
|
60
|
+
? action === 'write'
|
|
61
|
+
? permission.canWrite
|
|
62
|
+
: permission.canRun
|
|
63
|
+
: true;
|
|
64
|
+
}
|
|
65
|
+
async initialize() {
|
|
66
|
+
this.ensureAlive();
|
|
67
|
+
const scope = this.scope();
|
|
68
|
+
let loaded;
|
|
69
|
+
try {
|
|
70
|
+
loaded = await Promise.all([
|
|
71
|
+
this.options.projectProvider.listProjects(scope),
|
|
72
|
+
this.options.projectProvider.listPersonalLibraryFiles(scope),
|
|
73
|
+
this.options.projectProvider.listCommonFiles(scope),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error instanceof ModStudioOfflineError ||
|
|
78
|
+
this.options.isOnline?.() === false) {
|
|
79
|
+
this.update({
|
|
80
|
+
saveState: 'OFFLINE',
|
|
81
|
+
saveMessage: errorMessage(error),
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
const [projects, personalLibraryFiles, commonFiles] = loaded;
|
|
88
|
+
this.update({
|
|
89
|
+
projects,
|
|
90
|
+
personalLibraryFiles,
|
|
91
|
+
commonFiles,
|
|
92
|
+
saveState: 'SAVED',
|
|
93
|
+
saveMessage: undefined,
|
|
94
|
+
});
|
|
95
|
+
const first = projects.find((project) => project.projectId === this.options.initialProjectId) ?? projects[0];
|
|
96
|
+
if (first)
|
|
97
|
+
await this.loadProject(first.projectId);
|
|
98
|
+
}
|
|
99
|
+
async createProject(options) {
|
|
100
|
+
this.ensureAlive();
|
|
101
|
+
if (this.state.project && !(await this.saveNow())) {
|
|
102
|
+
throw new Error('Resolve or retry the current project save before creating another');
|
|
103
|
+
}
|
|
104
|
+
const input = createModStudioStarterProject({
|
|
105
|
+
...options,
|
|
106
|
+
...this.scope(),
|
|
107
|
+
});
|
|
108
|
+
const project = await this.options.projectProvider.createProject(input);
|
|
109
|
+
this.installProject(project);
|
|
110
|
+
this.update({
|
|
111
|
+
projects: upsertSummary(this.state.projects, summaryOf(project)),
|
|
112
|
+
saveState: 'SAVED',
|
|
113
|
+
saveMessage: undefined,
|
|
114
|
+
});
|
|
115
|
+
return project;
|
|
116
|
+
}
|
|
117
|
+
async switchProject(projectId) {
|
|
118
|
+
if (this.state.project?.projectId === projectId)
|
|
119
|
+
return;
|
|
120
|
+
if (this.state.project && !(await this.saveNow())) {
|
|
121
|
+
throw new Error('Resolve or retry the current project save before switching');
|
|
122
|
+
}
|
|
123
|
+
await this.loadProject(projectId);
|
|
124
|
+
}
|
|
125
|
+
async loadProject(projectId) {
|
|
126
|
+
const project = await this.options.projectProvider.getProject({
|
|
127
|
+
...this.scope(),
|
|
128
|
+
projectId,
|
|
129
|
+
});
|
|
130
|
+
this.installProject(project);
|
|
131
|
+
}
|
|
132
|
+
installProject(project) {
|
|
133
|
+
++this.operationGeneration;
|
|
134
|
+
this.stopSurfacePolling();
|
|
135
|
+
this.broker?.stop();
|
|
136
|
+
this.broker = null;
|
|
137
|
+
this.clearSaveTimers();
|
|
138
|
+
this.editGeneration = 0;
|
|
139
|
+
this.persistedGeneration = 0;
|
|
140
|
+
this.conflictRemote = null;
|
|
141
|
+
const clone = cloneModStudioProject(project);
|
|
142
|
+
const preferred = clone.files.find((file) => file.path === 'src/lib.rs') ?? clone.files[0];
|
|
143
|
+
const activeFile = preferred ? projectFileRef(preferred) : null;
|
|
144
|
+
this.update({
|
|
145
|
+
project: clone,
|
|
146
|
+
openFiles: activeFile ? [activeFile] : [],
|
|
147
|
+
activeFile,
|
|
148
|
+
saveState: 'SAVED',
|
|
149
|
+
saveMessage: undefined,
|
|
150
|
+
runtime: { phase: 'IDLE' },
|
|
151
|
+
buildOutput: '',
|
|
152
|
+
authoritativeDiagnostics: [],
|
|
153
|
+
localDiagnostics: [],
|
|
154
|
+
runs: [],
|
|
155
|
+
logs: [],
|
|
156
|
+
invokeResult: null,
|
|
157
|
+
});
|
|
158
|
+
this.restartVisibleSurfacePolling();
|
|
159
|
+
}
|
|
160
|
+
openFile(ref) {
|
|
161
|
+
this.requireFile(ref);
|
|
162
|
+
const exists = this.state.openFiles.some((entry) => sameFileRef(entry, ref));
|
|
163
|
+
this.update({
|
|
164
|
+
openFiles: exists ? this.state.openFiles : [...this.state.openFiles, ref],
|
|
165
|
+
activeFile: ref,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
closeFile(ref) {
|
|
169
|
+
const openFiles = this.state.openFiles.filter((entry) => !sameFileRef(entry, ref));
|
|
170
|
+
const activeFile = this.state.activeFile && sameFileRef(this.state.activeFile, ref)
|
|
171
|
+
? openFiles.at(-1) ?? null
|
|
172
|
+
: this.state.activeFile;
|
|
173
|
+
this.update({ openFiles, activeFile });
|
|
174
|
+
}
|
|
175
|
+
fileContent(ref) {
|
|
176
|
+
return this.requireFile(ref).content;
|
|
177
|
+
}
|
|
178
|
+
addFile(target, path, content = '') {
|
|
179
|
+
const project = this.requireProject();
|
|
180
|
+
this.assertProjectTarget(project, target);
|
|
181
|
+
const normalized = normalizeModStudioPath(path);
|
|
182
|
+
if (project.files.some((file) => file.target === target && file.path === normalized)) {
|
|
183
|
+
throw new Error(`${target}:${normalized} already exists`);
|
|
184
|
+
}
|
|
185
|
+
project.files.push({ target, path: normalized, content });
|
|
186
|
+
project.files.sort(compareProjectFile);
|
|
187
|
+
const ref = {
|
|
188
|
+
source: 'PROJECT',
|
|
189
|
+
target,
|
|
190
|
+
path: normalized,
|
|
191
|
+
};
|
|
192
|
+
this.markEdited();
|
|
193
|
+
this.openFile(ref);
|
|
194
|
+
}
|
|
195
|
+
renameFile(target, path, nextPath) {
|
|
196
|
+
const project = this.requireProject();
|
|
197
|
+
const normalized = normalizeModStudioPath(path);
|
|
198
|
+
const renamed = normalizeModStudioPath(nextPath);
|
|
199
|
+
const file = project.files.find((entry) => entry.target === target && entry.path === normalized);
|
|
200
|
+
if (!file)
|
|
201
|
+
throw new Error(`${target}:${normalized} does not exist`);
|
|
202
|
+
if (project.files.some((entry) => entry.target === target && entry.path === renamed)) {
|
|
203
|
+
throw new Error(`${target}:${renamed} already exists`);
|
|
204
|
+
}
|
|
205
|
+
file.path = renamed;
|
|
206
|
+
project.files.sort(compareProjectFile);
|
|
207
|
+
const replaceRef = (ref) => ref.source === 'PROJECT' &&
|
|
208
|
+
ref.target === target &&
|
|
209
|
+
ref.path === normalized
|
|
210
|
+
? { ...ref, path: renamed }
|
|
211
|
+
: ref;
|
|
212
|
+
this.update({
|
|
213
|
+
openFiles: this.state.openFiles.map(replaceRef),
|
|
214
|
+
activeFile: this.state.activeFile
|
|
215
|
+
? replaceRef(this.state.activeFile)
|
|
216
|
+
: null,
|
|
217
|
+
});
|
|
218
|
+
this.markEdited();
|
|
219
|
+
}
|
|
220
|
+
deleteFile(target, path) {
|
|
221
|
+
const project = this.requireProject();
|
|
222
|
+
const normalized = normalizeModStudioPath(path);
|
|
223
|
+
const index = project.files.findIndex((entry) => entry.target === target && entry.path === normalized);
|
|
224
|
+
if (index < 0)
|
|
225
|
+
throw new Error(`${target}:${normalized} does not exist`);
|
|
226
|
+
project.files.splice(index, 1);
|
|
227
|
+
this.closeFile({ source: 'PROJECT', target, path: normalized });
|
|
228
|
+
this.markEdited();
|
|
229
|
+
}
|
|
230
|
+
async importReferenceFile(reference, destinationPath = reference.path) {
|
|
231
|
+
this.requireProject();
|
|
232
|
+
if (!(await this.saveNow())) {
|
|
233
|
+
throw new Error('Resolve the current project save before importing a file');
|
|
234
|
+
}
|
|
235
|
+
const project = this.requireProject();
|
|
236
|
+
const saved = await this.options.projectProvider.importReferenceFile({
|
|
237
|
+
...this.scope(),
|
|
238
|
+
projectId: project.projectId,
|
|
239
|
+
expectedRevisionId: project.revision.id,
|
|
240
|
+
source: reference.source,
|
|
241
|
+
referenceId: reference.id,
|
|
242
|
+
destinationPath,
|
|
243
|
+
});
|
|
244
|
+
this.installProject(saved);
|
|
245
|
+
this.update({
|
|
246
|
+
projects: upsertSummary(this.state.projects, summaryOf(saved)),
|
|
247
|
+
});
|
|
248
|
+
const imported = saved.files.find((file) => file.target === reference.target &&
|
|
249
|
+
file.path === normalizeModStudioPath(destinationPath));
|
|
250
|
+
if (imported)
|
|
251
|
+
this.openFile(projectFileRef(imported));
|
|
252
|
+
}
|
|
253
|
+
async saveProjectFileToLibrary(target, path, title) {
|
|
254
|
+
const file = this.requireProject().files.find((entry) => entry.target === target &&
|
|
255
|
+
entry.path === normalizeModStudioPath(path));
|
|
256
|
+
if (!file)
|
|
257
|
+
throw new Error(`${target}:${path} does not exist`);
|
|
258
|
+
const saved = await this.options.projectProvider.savePersonalLibraryFile({
|
|
259
|
+
...this.scope(),
|
|
260
|
+
title: title?.trim() || file.path.split('/').at(-1) || file.path,
|
|
261
|
+
target,
|
|
262
|
+
path: file.path,
|
|
263
|
+
content: file.content,
|
|
264
|
+
});
|
|
265
|
+
this.update({
|
|
266
|
+
personalLibraryFiles: upsertReference(this.state.personalLibraryFiles, saved),
|
|
267
|
+
});
|
|
268
|
+
return saved;
|
|
269
|
+
}
|
|
270
|
+
updateFile(target, path, content) {
|
|
271
|
+
const project = this.requireProject();
|
|
272
|
+
const normalized = normalizeModStudioPath(path);
|
|
273
|
+
const file = project.files.find((entry) => entry.target === target && entry.path === normalized);
|
|
274
|
+
if (!file)
|
|
275
|
+
throw new Error(`${target}:${normalized} does not exist`);
|
|
276
|
+
if (file.content === content)
|
|
277
|
+
return;
|
|
278
|
+
file.content = content;
|
|
279
|
+
this.markEdited();
|
|
280
|
+
}
|
|
281
|
+
updateSettings(patch) {
|
|
282
|
+
const project = this.requireProject();
|
|
283
|
+
project.metadata = {
|
|
284
|
+
...project.metadata,
|
|
285
|
+
...patch,
|
|
286
|
+
pairingPreference: patch.pairingPreference ?? project.metadata.pairingPreference,
|
|
287
|
+
};
|
|
288
|
+
this.markEdited();
|
|
289
|
+
}
|
|
290
|
+
setPairingPreference(preference) {
|
|
291
|
+
this.updateSettings({ pairingPreference: preference });
|
|
292
|
+
}
|
|
293
|
+
setLocalDiagnostics(diagnostics) {
|
|
294
|
+
this.update({ localDiagnostics: [...diagnostics] });
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Flush all edits in one optimistic-concurrency save. If edits arrive while
|
|
298
|
+
* the request is in flight, a second atomic save follows with the new
|
|
299
|
+
* revision instead of overwriting local content with the earlier response.
|
|
300
|
+
*/
|
|
301
|
+
async saveNow() {
|
|
302
|
+
this.ensureAlive();
|
|
303
|
+
this.clearTimer('autosave');
|
|
304
|
+
if (!this.state.project)
|
|
305
|
+
return true;
|
|
306
|
+
if (this.savePromise) {
|
|
307
|
+
await this.savePromise;
|
|
308
|
+
if (this.persistedGeneration === this.editGeneration)
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
this.savePromise = this.performSaveLoop();
|
|
312
|
+
try {
|
|
313
|
+
return await this.savePromise;
|
|
314
|
+
}
|
|
315
|
+
finally {
|
|
316
|
+
this.savePromise = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async retrySave() {
|
|
320
|
+
this.clearTimer('retry');
|
|
321
|
+
if (!this.state.project) {
|
|
322
|
+
this.update({ saveState: 'SAVING', saveMessage: undefined });
|
|
323
|
+
await this.initialize();
|
|
324
|
+
return this.state.saveState !== 'OFFLINE';
|
|
325
|
+
}
|
|
326
|
+
if (this.state.saveState === 'CONFLICT')
|
|
327
|
+
return false;
|
|
328
|
+
this.update({ saveState: 'SAVING', saveMessage: undefined });
|
|
329
|
+
return this.saveNow();
|
|
330
|
+
}
|
|
331
|
+
async acceptRemoteConflict() {
|
|
332
|
+
if (this.state.saveState !== 'CONFLICT')
|
|
333
|
+
return;
|
|
334
|
+
const remote = this.conflictRemote ??
|
|
335
|
+
(await this.options.projectProvider.getProject({
|
|
336
|
+
...this.scope(),
|
|
337
|
+
projectId: this.requireProject().projectId,
|
|
338
|
+
}));
|
|
339
|
+
this.installProject(remote);
|
|
340
|
+
}
|
|
341
|
+
async overwriteConflict() {
|
|
342
|
+
if (this.state.saveState !== 'CONFLICT')
|
|
343
|
+
return this.saveNow();
|
|
344
|
+
const project = this.requireProject();
|
|
345
|
+
const remote = this.conflictRemote ??
|
|
346
|
+
(await this.options.projectProvider.getProject({
|
|
347
|
+
...this.scope(),
|
|
348
|
+
projectId: project.projectId,
|
|
349
|
+
}));
|
|
350
|
+
project.revision = { ...remote.revision };
|
|
351
|
+
this.conflictRemote = null;
|
|
352
|
+
this.update({ saveState: 'SAVING', saveMessage: undefined });
|
|
353
|
+
return this.saveNow();
|
|
354
|
+
}
|
|
355
|
+
async testDraft() {
|
|
356
|
+
await this.deployProject(true);
|
|
357
|
+
}
|
|
358
|
+
async deployLive() {
|
|
359
|
+
await this.deployProject(false);
|
|
360
|
+
}
|
|
361
|
+
async deployProject(draft) {
|
|
362
|
+
this.requireProject();
|
|
363
|
+
if (!(await this.saveNow())) {
|
|
364
|
+
this.setRuntime('ERROR', 'Project must be saved before it can be built');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
const project = this.requireProject();
|
|
368
|
+
const operation = ++this.operationGeneration;
|
|
369
|
+
this.stopSurfacePolling();
|
|
370
|
+
this.update({
|
|
371
|
+
runtime: { phase: draft ? 'TESTING_DRAFT' : 'DEPLOYING_LIVE' },
|
|
372
|
+
buildOutput: '',
|
|
373
|
+
authoritativeDiagnostics: [],
|
|
374
|
+
});
|
|
375
|
+
try {
|
|
376
|
+
const targets = projectTargets(project.kind);
|
|
377
|
+
if (targets.length === 1) {
|
|
378
|
+
const target = targets[0];
|
|
379
|
+
const compiled = await this.compileTarget(project, target, draft, operation);
|
|
380
|
+
if (!compiled)
|
|
381
|
+
return;
|
|
382
|
+
if (target === 'SERVER') {
|
|
383
|
+
await this.enableServer(compiled.name, operation);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
await this.runClient(compiled, operation);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
// Compile the client first so a client failure never publishes a new
|
|
391
|
+
// server version or alters the currently live pairing requirement.
|
|
392
|
+
const client = await this.compileTarget(project, 'CLIENT', draft, operation);
|
|
393
|
+
if (!client)
|
|
394
|
+
return;
|
|
395
|
+
const server = await this.compileTarget(project, 'SERVER', draft, operation);
|
|
396
|
+
if (!server)
|
|
397
|
+
return;
|
|
398
|
+
this.checkOperation(operation);
|
|
399
|
+
const requiredClientName = project.metadata.pairingPreference === 'REQUIRED'
|
|
400
|
+
? client.name
|
|
401
|
+
: null;
|
|
402
|
+
await this.options.playerCompute.setRequires({
|
|
403
|
+
...this.scope(),
|
|
404
|
+
serverName: server.name,
|
|
405
|
+
requiredClientName,
|
|
406
|
+
});
|
|
407
|
+
this.checkOperation(operation);
|
|
408
|
+
await this.enableServer(server.name, operation);
|
|
409
|
+
await this.runClient(client, operation);
|
|
410
|
+
}
|
|
411
|
+
this.update({
|
|
412
|
+
runtime: {
|
|
413
|
+
phase: 'RUNNING',
|
|
414
|
+
message: draft ? 'Draft test is running' : 'Project is live',
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
await this.refreshSurface('usage').catch(() => { });
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
if (error instanceof OperationCancelledError)
|
|
421
|
+
return;
|
|
422
|
+
this.setRuntime('ERROR', errorMessage(error));
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (operation === this.operationGeneration) {
|
|
426
|
+
this.restartVisibleSurfacePolling();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async compileTarget(project, target, draft, operation) {
|
|
431
|
+
if (!this.canTarget(target, 'write')) {
|
|
432
|
+
throw new Error(`${target} authoring is unavailable on this grid`);
|
|
433
|
+
}
|
|
434
|
+
const name = moduleNameFor(project, target);
|
|
435
|
+
const files = project.files.filter((file) => file.target === target);
|
|
436
|
+
if (files.length === 0)
|
|
437
|
+
throw new Error(`${target} has no project files`);
|
|
438
|
+
this.update({
|
|
439
|
+
runtime: {
|
|
440
|
+
phase: 'COMPILING',
|
|
441
|
+
target,
|
|
442
|
+
message: `Submitting ${name}`,
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
// This is the sole project→legacy wire conversion. Project state, provider
|
|
446
|
+
// contracts, editors, and templates all use typed files.
|
|
447
|
+
const sourceFilesJson = JSON.stringify(Object.fromEntries(files.map((file) => [file.path, file.content])));
|
|
448
|
+
const deployed = await this.options.playerCompute.deploy({
|
|
449
|
+
...this.scope(),
|
|
450
|
+
name,
|
|
451
|
+
target: target,
|
|
452
|
+
sourceFilesJson,
|
|
453
|
+
sdkVersion: project.sdkVersion,
|
|
454
|
+
abiVersion: project.abiVersion,
|
|
455
|
+
tickHz: target === 'SERVER' ? 1 : undefined,
|
|
456
|
+
draft,
|
|
457
|
+
});
|
|
458
|
+
this.checkOperation(operation);
|
|
459
|
+
const limit = this.options.compilePollLimit ?? 60;
|
|
460
|
+
const pollMs = this.options.compilePollMs ?? 1500;
|
|
461
|
+
for (let attempt = 0; attempt < limit; attempt++) {
|
|
462
|
+
const versions = await this.options.playerCompute.versions({
|
|
463
|
+
...this.scope(),
|
|
464
|
+
name,
|
|
465
|
+
});
|
|
466
|
+
this.checkOperation(operation);
|
|
467
|
+
const version = versions.find((candidate) => candidate.versionId === deployed.versionId);
|
|
468
|
+
const status = version?.compileStatus.toLowerCase();
|
|
469
|
+
if (status === 'succeeded' || status === 'success') {
|
|
470
|
+
this.recordBuild(target, version?.compileLog ?? '');
|
|
471
|
+
return { target, name, versionId: deployed.versionId };
|
|
472
|
+
}
|
|
473
|
+
if (status === 'failed' || status === 'error') {
|
|
474
|
+
const log = version?.compileLog ?? 'Compilation failed without output';
|
|
475
|
+
this.recordBuild(target, log);
|
|
476
|
+
this.update({
|
|
477
|
+
runtime: {
|
|
478
|
+
phase: 'COMPILE_FAILED',
|
|
479
|
+
target,
|
|
480
|
+
message: `${name} failed to compile`,
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
await this.sleep(pollMs);
|
|
486
|
+
this.checkOperation(operation);
|
|
487
|
+
}
|
|
488
|
+
const timeout = `Compilation timed out after ${limit} polls`;
|
|
489
|
+
this.recordBuild(target, timeout);
|
|
490
|
+
this.update({
|
|
491
|
+
runtime: { phase: 'COMPILE_FAILED', target, message: timeout },
|
|
492
|
+
});
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
recordBuild(target, log) {
|
|
496
|
+
const section = `## ${target}\n${log || 'Compiled successfully.'}`;
|
|
497
|
+
const authoritativeDiagnostics = [
|
|
498
|
+
...this.state.authoritativeDiagnostics.filter((diagnostic) => diagnostic.target !== target),
|
|
499
|
+
...parseRustcDiagnostics(log, target),
|
|
500
|
+
];
|
|
501
|
+
this.update({
|
|
502
|
+
buildOutput: [this.state.buildOutput, section].filter(Boolean).join('\n\n'),
|
|
503
|
+
authoritativeDiagnostics,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
async enableServer(name, operation) {
|
|
507
|
+
if (!this.canTarget('SERVER', 'run')) {
|
|
508
|
+
throw new Error(`${name} compiled successfully, but run_server_code is unavailable on this grid`);
|
|
509
|
+
}
|
|
510
|
+
this.update({
|
|
511
|
+
runtime: { phase: 'ENABLING', target: 'SERVER', message: `Enabling ${name}` },
|
|
512
|
+
});
|
|
513
|
+
await this.options.playerCompute.setEnabled({
|
|
514
|
+
...this.scope(),
|
|
515
|
+
name,
|
|
516
|
+
enabled: true,
|
|
517
|
+
});
|
|
518
|
+
this.checkOperation(operation);
|
|
519
|
+
}
|
|
520
|
+
async runClient(compiled, operation) {
|
|
521
|
+
if (!this.canTarget('CLIENT', 'run')) {
|
|
522
|
+
throw new Error(`${compiled.name} compiled successfully, but run_client_code is unavailable on this grid`);
|
|
523
|
+
}
|
|
524
|
+
const runtime = this.clientRuntimeOptions();
|
|
525
|
+
const artifact = await this.options.playerCompute.artifactBytes({
|
|
526
|
+
...this.scope(),
|
|
527
|
+
name: compiled.name,
|
|
528
|
+
versionId: compiled.versionId,
|
|
529
|
+
});
|
|
530
|
+
this.checkOperation(operation);
|
|
531
|
+
if (artifact.versionId !== compiled.versionId ||
|
|
532
|
+
!artifact.artifactHash ||
|
|
533
|
+
artifact.bytes.byteLength === 0) {
|
|
534
|
+
throw new Error('Client artifact did not match the compiled project version');
|
|
535
|
+
}
|
|
536
|
+
const brokerOptions = {
|
|
537
|
+
...runtime,
|
|
538
|
+
artifactHash: artifact.artifactHash,
|
|
539
|
+
fuelPerDispatch: artifact.fuelPerDispatch,
|
|
540
|
+
onPresentation: this.options.onPresentation,
|
|
541
|
+
tickIntervalMs: this.options.clientTickIntervalMs ?? 1000,
|
|
542
|
+
};
|
|
543
|
+
const broker = this.options.brokerFactory?.(brokerOptions) ??
|
|
544
|
+
new PlayerCodeBroker(brokerOptions);
|
|
545
|
+
await broker.start(artifact.bytes);
|
|
546
|
+
this.checkOperation(operation);
|
|
547
|
+
const previous = this.broker;
|
|
548
|
+
this.broker = broker;
|
|
549
|
+
previous?.stop();
|
|
550
|
+
}
|
|
551
|
+
async stopProject() {
|
|
552
|
+
const project = this.requireProject();
|
|
553
|
+
++this.operationGeneration;
|
|
554
|
+
this.stopSurfacePolling();
|
|
555
|
+
this.update({ runtime: { phase: 'STOPPING' } });
|
|
556
|
+
const failures = [];
|
|
557
|
+
let serverStopped = null;
|
|
558
|
+
let clientStopped = null;
|
|
559
|
+
if (projectTargets(project.kind).includes('CLIENT')) {
|
|
560
|
+
clientStopped = true;
|
|
561
|
+
try {
|
|
562
|
+
this.broker?.stop();
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
clientStopped = false;
|
|
566
|
+
failures.push(`Client: ${errorMessage(error)}`);
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
this.broker = null;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (projectTargets(project.kind).includes('SERVER')) {
|
|
573
|
+
serverStopped = false;
|
|
574
|
+
try {
|
|
575
|
+
await this.options.playerCompute.setEnabled({
|
|
576
|
+
...this.scope(),
|
|
577
|
+
name: moduleNameFor(project, 'SERVER'),
|
|
578
|
+
enabled: false,
|
|
579
|
+
});
|
|
580
|
+
serverStopped = true;
|
|
581
|
+
}
|
|
582
|
+
catch (error) {
|
|
583
|
+
failures.push(`Server: ${errorMessage(error)}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const result = { serverStopped, clientStopped, failures };
|
|
587
|
+
this.update({
|
|
588
|
+
runtime: failures.length === 0
|
|
589
|
+
? { phase: 'STOPPED', message: 'Project stopped' }
|
|
590
|
+
: {
|
|
591
|
+
phase: 'PARTIAL_FAILURE',
|
|
592
|
+
message: failures.join(' · '),
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
async invoke(exportName, paramsJson) {
|
|
598
|
+
const project = this.requireProject();
|
|
599
|
+
if (!projectTargets(project.kind).includes('SERVER')) {
|
|
600
|
+
throw new Error('Invoke requires a SERVER target');
|
|
601
|
+
}
|
|
602
|
+
const result = await this.options.playerCompute.invoke({
|
|
603
|
+
...this.scope(),
|
|
604
|
+
moduleName: moduleNameFor(project, 'SERVER'),
|
|
605
|
+
exportName: exportName.trim() || 'invoke',
|
|
606
|
+
paramsJson: paramsJson?.trim() || null,
|
|
607
|
+
});
|
|
608
|
+
this.update({ invokeResult: result });
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
setSurfaceVisible(surface, visible) {
|
|
612
|
+
if (visible) {
|
|
613
|
+
this.visibleSurfaces.add(surface);
|
|
614
|
+
if (this.pageVisible) {
|
|
615
|
+
void this.refreshSurface(surface).catch(() => { });
|
|
616
|
+
this.scheduleSurfacePoll(surface);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
this.visibleSurfaces.delete(surface);
|
|
621
|
+
this.clearSurfaceTimer(surface);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
setPageVisible(visible) {
|
|
625
|
+
if (this.pageVisible === visible)
|
|
626
|
+
return;
|
|
627
|
+
this.pageVisible = visible;
|
|
628
|
+
if (visible)
|
|
629
|
+
this.restartVisibleSurfacePolling();
|
|
630
|
+
else
|
|
631
|
+
this.stopSurfacePolling();
|
|
632
|
+
}
|
|
633
|
+
async refreshSurface(surface) {
|
|
634
|
+
if (!this.state.project)
|
|
635
|
+
return;
|
|
636
|
+
const serverName = this.state.project.metadata.serverModuleName;
|
|
637
|
+
if (surface === 'runs') {
|
|
638
|
+
this.update({
|
|
639
|
+
runs: await this.options.playerCompute.runs({
|
|
640
|
+
...this.scope(),
|
|
641
|
+
...(serverName ? { moduleName: serverName } : {}),
|
|
642
|
+
limit: 50,
|
|
643
|
+
offset: 0,
|
|
644
|
+
}),
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (surface === 'logs') {
|
|
649
|
+
this.update({
|
|
650
|
+
logs: await this.options.playerCompute.logs({
|
|
651
|
+
...this.scope(),
|
|
652
|
+
...(serverName ? { moduleName: serverName } : {}),
|
|
653
|
+
limit: 50,
|
|
654
|
+
}),
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
const [usage, wallet] = await Promise.all([
|
|
659
|
+
this.options.playerCompute.usage({ appId: this.options.appId }),
|
|
660
|
+
this.options.playerWallet?.balance() ?? Promise.resolve(null),
|
|
661
|
+
]);
|
|
662
|
+
this.update({ usage: normalizeUsage(usage), wallet });
|
|
663
|
+
}
|
|
664
|
+
destroy() {
|
|
665
|
+
if (this.destroyed)
|
|
666
|
+
return;
|
|
667
|
+
this.destroyed = true;
|
|
668
|
+
++this.operationGeneration;
|
|
669
|
+
this.clearSaveTimers();
|
|
670
|
+
this.stopSurfacePolling();
|
|
671
|
+
this.broker?.stop();
|
|
672
|
+
this.broker = null;
|
|
673
|
+
this.listeners.clear();
|
|
674
|
+
}
|
|
675
|
+
async performSaveLoop() {
|
|
676
|
+
while (this.persistedGeneration !== this.editGeneration) {
|
|
677
|
+
const project = this.requireProject();
|
|
678
|
+
const savingGeneration = this.editGeneration;
|
|
679
|
+
const snapshot = cloneModStudioProject(project);
|
|
680
|
+
this.update({ saveState: 'SAVING', saveMessage: undefined });
|
|
681
|
+
try {
|
|
682
|
+
const saved = await this.options.projectProvider.saveProject({
|
|
683
|
+
...this.scope(),
|
|
684
|
+
projectId: snapshot.projectId,
|
|
685
|
+
expectedRevisionId: snapshot.revision.id,
|
|
686
|
+
metadata: snapshot.metadata,
|
|
687
|
+
files: snapshot.files,
|
|
688
|
+
});
|
|
689
|
+
this.persistedGeneration = savingGeneration;
|
|
690
|
+
if (this.state.project?.projectId !== saved.projectId)
|
|
691
|
+
return true;
|
|
692
|
+
if (this.editGeneration === savingGeneration) {
|
|
693
|
+
this.state.project = cloneModStudioProject(saved);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// Preserve newer local edits while advancing the revision precondition.
|
|
697
|
+
this.state.project.revision = { ...saved.revision };
|
|
698
|
+
this.state.project.updatedAt = saved.updatedAt;
|
|
699
|
+
}
|
|
700
|
+
this.update({
|
|
701
|
+
projects: upsertSummary(this.state.projects, summaryOf(saved)),
|
|
702
|
+
saveState: this.persistedGeneration === this.editGeneration ? 'SAVED' : 'SAVING',
|
|
703
|
+
saveMessage: undefined,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
catch (error) {
|
|
707
|
+
if (error instanceof ModStudioRevisionConflictError) {
|
|
708
|
+
this.conflictRemote = error.remoteProject ?? null;
|
|
709
|
+
this.update({ saveState: 'CONFLICT', saveMessage: error.message });
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
if (error instanceof ModStudioOfflineError ||
|
|
713
|
+
this.options.isOnline?.() === false) {
|
|
714
|
+
this.update({ saveState: 'OFFLINE', saveMessage: errorMessage(error) });
|
|
715
|
+
this.scheduleRetry();
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
this.update({ saveState: 'OFFLINE', saveMessage: errorMessage(error) });
|
|
719
|
+
throw error;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
this.update({ saveState: 'SAVED', saveMessage: undefined });
|
|
723
|
+
return true;
|
|
724
|
+
}
|
|
725
|
+
markEdited() {
|
|
726
|
+
this.editGeneration++;
|
|
727
|
+
this.update({ saveState: 'SAVING', saveMessage: undefined });
|
|
728
|
+
this.clearTimer('autosave');
|
|
729
|
+
this.autosaveTimer = setTimeout(() => {
|
|
730
|
+
this.autosaveTimer = null;
|
|
731
|
+
void this.saveNow().catch(() => { });
|
|
732
|
+
}, this.options.autosaveMs ?? 700);
|
|
733
|
+
}
|
|
734
|
+
scheduleRetry() {
|
|
735
|
+
this.clearTimer('retry');
|
|
736
|
+
if (this.destroyed)
|
|
737
|
+
return;
|
|
738
|
+
this.retryTimer = setTimeout(() => {
|
|
739
|
+
this.retryTimer = null;
|
|
740
|
+
void this.retrySave().catch(() => { });
|
|
741
|
+
}, this.options.retryMs ?? 3000);
|
|
742
|
+
}
|
|
743
|
+
scheduleSurfacePoll(surface) {
|
|
744
|
+
this.clearSurfaceTimer(surface);
|
|
745
|
+
if (!this.pageVisible ||
|
|
746
|
+
!this.visibleSurfaces.has(surface) ||
|
|
747
|
+
this.destroyed) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const timer = setTimeout(() => {
|
|
751
|
+
this.surfaceTimers.delete(surface);
|
|
752
|
+
void this.refreshSurface(surface)
|
|
753
|
+
.catch(() => { })
|
|
754
|
+
.finally(() => this.scheduleSurfacePoll(surface));
|
|
755
|
+
}, this.options.monitorPollMs ?? 5000);
|
|
756
|
+
this.surfaceTimers.set(surface, timer);
|
|
757
|
+
}
|
|
758
|
+
restartVisibleSurfacePolling() {
|
|
759
|
+
if (!this.pageVisible || this.destroyed)
|
|
760
|
+
return;
|
|
761
|
+
for (const surface of this.visibleSurfaces) {
|
|
762
|
+
void this.refreshSurface(surface).catch(() => { });
|
|
763
|
+
this.scheduleSurfacePoll(surface);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
stopSurfacePolling() {
|
|
767
|
+
for (const timer of this.surfaceTimers.values())
|
|
768
|
+
clearTimeout(timer);
|
|
769
|
+
this.surfaceTimers.clear();
|
|
770
|
+
}
|
|
771
|
+
clearSurfaceTimer(surface) {
|
|
772
|
+
const timer = this.surfaceTimers.get(surface);
|
|
773
|
+
if (timer)
|
|
774
|
+
clearTimeout(timer);
|
|
775
|
+
this.surfaceTimers.delete(surface);
|
|
776
|
+
}
|
|
777
|
+
clearSaveTimers() {
|
|
778
|
+
this.clearTimer('autosave');
|
|
779
|
+
this.clearTimer('retry');
|
|
780
|
+
}
|
|
781
|
+
clearTimer(kind) {
|
|
782
|
+
const timer = kind === 'autosave' ? this.autosaveTimer : this.retryTimer;
|
|
783
|
+
if (timer)
|
|
784
|
+
clearTimeout(timer);
|
|
785
|
+
if (kind === 'autosave')
|
|
786
|
+
this.autosaveTimer = null;
|
|
787
|
+
else
|
|
788
|
+
this.retryTimer = null;
|
|
789
|
+
}
|
|
790
|
+
clientRuntimeOptions() {
|
|
791
|
+
if (!this.options.workerUrl || !this.options.grid || !this.options.onHostCall) {
|
|
792
|
+
throw new Error('CLIENT projects require workerUrl, grid, and an allow-listed onHostCall router');
|
|
793
|
+
}
|
|
794
|
+
return {
|
|
795
|
+
workerUrl: this.options.workerUrl,
|
|
796
|
+
grid: this.options.grid,
|
|
797
|
+
onHostCall: this.options.onHostCall,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
checkOperation(generation) {
|
|
801
|
+
if (generation !== this.operationGeneration || this.destroyed) {
|
|
802
|
+
throw new OperationCancelledError();
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
sleep(ms) {
|
|
806
|
+
return (this.options.sleep ??
|
|
807
|
+
((delay) => new Promise((resolve) => setTimeout(resolve, delay))))(ms);
|
|
808
|
+
}
|
|
809
|
+
requireProject() {
|
|
810
|
+
if (!this.state.project)
|
|
811
|
+
throw new Error('No Mod Studio project is open');
|
|
812
|
+
return this.state.project;
|
|
813
|
+
}
|
|
814
|
+
requireFile(ref) {
|
|
815
|
+
if (ref.source === 'PROJECT') {
|
|
816
|
+
const file = this.requireProject().files.find((entry) => entry.target === ref.target &&
|
|
817
|
+
entry.path === normalizeModStudioPath(ref.path));
|
|
818
|
+
if (file)
|
|
819
|
+
return file;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
const files = ref.source === 'PERSONAL_LIBRARY'
|
|
823
|
+
? this.state.personalLibraryFiles
|
|
824
|
+
: this.state.commonFiles;
|
|
825
|
+
const file = files.find((entry) => ref.referenceId
|
|
826
|
+
? entry.id === ref.referenceId
|
|
827
|
+
: entry.path === ref.path && entry.target === ref.target);
|
|
828
|
+
if (file)
|
|
829
|
+
return file;
|
|
830
|
+
}
|
|
831
|
+
throw new Error(`File is not loaded: ${ref.source}:${ref.path}`);
|
|
832
|
+
}
|
|
833
|
+
assertProjectTarget(project, target) {
|
|
834
|
+
if (!projectTargets(project.kind).includes(target)) {
|
|
835
|
+
throw new Error(`${project.kind} projects do not have a ${target} target`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
scope() {
|
|
839
|
+
return { appId: this.options.appId, gridId: this.options.gridId };
|
|
840
|
+
}
|
|
841
|
+
setRuntime(phase, message) {
|
|
842
|
+
this.update({ runtime: { phase, message } });
|
|
843
|
+
}
|
|
844
|
+
update(patch) {
|
|
845
|
+
this.state = { ...this.state, ...patch };
|
|
846
|
+
for (const listener of this.listeners)
|
|
847
|
+
listener(this.state);
|
|
848
|
+
}
|
|
849
|
+
ensureAlive() {
|
|
850
|
+
if (this.destroyed)
|
|
851
|
+
throw new Error('ModStudioController is destroyed');
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
function projectFileRef(file) {
|
|
855
|
+
return { source: 'PROJECT', target: file.target, path: file.path };
|
|
856
|
+
}
|
|
857
|
+
function sameFileRef(a, b) {
|
|
858
|
+
return (a.source === b.source &&
|
|
859
|
+
a.target === b.target &&
|
|
860
|
+
a.path === b.path &&
|
|
861
|
+
a.referenceId === b.referenceId);
|
|
862
|
+
}
|
|
863
|
+
function compareProjectFile(a, b) {
|
|
864
|
+
return modStudioFileKey(a.target, a.path).localeCompare(modStudioFileKey(b.target, b.path));
|
|
865
|
+
}
|
|
866
|
+
function moduleNameFor(project, target) {
|
|
867
|
+
const name = target === 'SERVER'
|
|
868
|
+
? project.metadata.serverModuleName
|
|
869
|
+
: project.metadata.clientModuleName;
|
|
870
|
+
if (!name?.trim()) {
|
|
871
|
+
throw new Error(`${target} module name is required in Project settings`);
|
|
872
|
+
}
|
|
873
|
+
return name.trim();
|
|
874
|
+
}
|
|
875
|
+
function summaryOf(project) {
|
|
876
|
+
return {
|
|
877
|
+
projectId: project.projectId,
|
|
878
|
+
name: project.metadata.name,
|
|
879
|
+
kind: project.kind,
|
|
880
|
+
revisionId: project.revision.id,
|
|
881
|
+
...(project.metadata.serverModuleName
|
|
882
|
+
? { serverModuleName: project.metadata.serverModuleName }
|
|
883
|
+
: {}),
|
|
884
|
+
...(project.metadata.clientModuleName
|
|
885
|
+
? { clientModuleName: project.metadata.clientModuleName }
|
|
886
|
+
: {}),
|
|
887
|
+
updatedAt: project.updatedAt,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function upsertSummary(projects, next) {
|
|
891
|
+
const result = projects.filter((project) => project.projectId !== next.projectId);
|
|
892
|
+
result.push(next);
|
|
893
|
+
return result.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
894
|
+
}
|
|
895
|
+
function upsertReference(files, next) {
|
|
896
|
+
return [
|
|
897
|
+
next,
|
|
898
|
+
...files.filter((file) => file.source !== next.source || file.id !== next.id),
|
|
899
|
+
];
|
|
900
|
+
}
|
|
901
|
+
function normalizeUsage(value) {
|
|
902
|
+
return {
|
|
903
|
+
hourUnitsUsed: String(value.hourUnitsUsed),
|
|
904
|
+
dayUnitsUsed: String(value.dayUnitsUsed),
|
|
905
|
+
unitsPerHour: value.unitsPerHour == null ? null : String(value.unitsPerHour),
|
|
906
|
+
unitsPerDay: value.unitsPerDay == null ? null : String(value.unitsPerDay),
|
|
907
|
+
compilesThisHour: value.compilesThisHour,
|
|
908
|
+
maxCompilesPerHour: value.maxCompilesPerHour,
|
|
909
|
+
gateStatus: value.gateStatus,
|
|
910
|
+
gateReason: value.gateReason ?? null,
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function errorMessage(error) {
|
|
914
|
+
return error instanceof Error ? error.message : String(error);
|
|
915
|
+
}
|