@crowdedkingdoms/crowdyjs 17.12.0-dev.1 → 17.14.0-dev.1

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.
Files changed (70) hide show
  1. package/MIGRATION.md +127 -0
  2. package/README.md +99 -4
  3. package/dist/crowdy-dsh/bridge.d.ts.map +1 -1
  4. package/dist/crowdy-dsh/bridge.js +2 -0
  5. package/dist/crowdy-studio/controller.d.ts +64 -8
  6. package/dist/crowdy-studio/controller.d.ts.map +1 -1
  7. package/dist/crowdy-studio/controller.js +345 -46
  8. package/dist/crowdy-studio/dom-shell.d.ts.map +1 -1
  9. package/dist/crowdy-studio/dom-shell.js +16 -9
  10. package/dist/crowdy-studio/embed/hud-layer.d.ts +2 -1
  11. package/dist/crowdy-studio/embed/hud-layer.d.ts.map +1 -1
  12. package/dist/crowdy-studio/embed/panel.d.ts +9 -6
  13. package/dist/crowdy-studio/embed/panel.d.ts.map +1 -1
  14. package/dist/crowdy-studio/embed/panel.js +4 -2
  15. package/dist/crowdy-studio/index.d.ts +2 -2
  16. package/dist/crowdy-studio/index.d.ts.map +1 -1
  17. package/dist/crowdy-studio/models.d.ts +1 -1
  18. package/dist/crowdy-studio/starter-projects.d.ts +22 -0
  19. package/dist/crowdy-studio/starter-projects.d.ts.map +1 -1
  20. package/dist/crowdy-studio/starter-projects.js +129 -5
  21. package/dist/domains/exec.d.ts +222 -20
  22. package/dist/domains/exec.d.ts.map +1 -1
  23. package/dist/domains/exec.js +215 -21
  24. package/dist/domains/marketplace.d.ts +15 -3
  25. package/dist/domains/marketplace.d.ts.map +1 -1
  26. package/dist/domains/marketplace.js +17 -3
  27. package/dist/errors.d.ts +3 -1
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/generated/graphql.d.ts +422 -0
  30. package/dist/generated/graphql.d.ts.map +1 -1
  31. package/dist/generated/graphql.js +23 -11
  32. package/dist/grid-mods/exec-client-halves.d.ts +140 -0
  33. package/dist/grid-mods/exec-client-halves.d.ts.map +1 -0
  34. package/dist/grid-mods/exec-client-halves.js +341 -0
  35. package/dist/grid-mods/grid-host-calls.d.ts +3 -1
  36. package/dist/grid-mods/grid-host-calls.d.ts.map +1 -1
  37. package/dist/grid-mods/grid-host-calls.js +3 -1
  38. package/dist/grid-mods/index.d.ts +1 -0
  39. package/dist/grid-mods/index.d.ts.map +1 -1
  40. package/dist/grid-mods/index.js +1 -0
  41. package/dist/grid-mods/mod-runtime.d.ts +10 -2
  42. package/dist/grid-mods/mod-runtime.d.ts.map +1 -1
  43. package/dist/grid-mods/mod-runtime.js +2 -0
  44. package/dist/grid-scope.d.ts +1 -0
  45. package/dist/grid-scope.d.ts.map +1 -1
  46. package/dist/grid-scope.js +1 -0
  47. package/dist/index.d.ts +4 -4
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +4 -3
  50. package/dist/kit/npcs.d.ts.map +1 -1
  51. package/dist/kit/social.d.ts.map +1 -1
  52. package/dist/live-coding/rust-analysis.js +13 -1
  53. package/dist/player-runtime/glue-runtime.d.ts +19 -6
  54. package/dist/player-runtime/glue-runtime.d.ts.map +1 -1
  55. package/dist/player-runtime/glue-runtime.js +40 -16
  56. package/dist/player-runtime/player-code-broker.d.ts +29 -0
  57. package/dist/player-runtime/player-code-broker.d.ts.map +1 -1
  58. package/dist/player-runtime/player-code-broker.js +41 -8
  59. package/dist/player-runtime/player-glue-worker.d.ts +2 -1
  60. package/dist/player-runtime/player-glue-worker.d.ts.map +1 -1
  61. package/dist/player-runtime/player-glue-worker.js +3 -1
  62. package/dist/stores/actors.d.ts +12 -2
  63. package/dist/stores/actors.d.ts.map +1 -1
  64. package/dist/stores/actors.js +43 -18
  65. package/dist/stores/chunks.d.ts +12 -0
  66. package/dist/stores/chunks.d.ts.map +1 -1
  67. package/dist/stores/chunks.js +112 -39
  68. package/dist/stores/durable.d.ts.map +1 -1
  69. package/dist/stores/durable.js +2 -1
  70. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { PlayerCodeBroker, } from '../player-runtime/player-code-broker.js';
2
+ import { CrowdyError, CrowdyGraphQLError } from '../errors.js';
2
3
  import { digestCanonicalJson, sha256Digest, } from '../player-host/json-schema.js';
3
4
  import { parseRustcDiagnostics } from './diagnostics.js';
4
5
  import { githubNewRepositoryUrl, githubRepositorySlug } from './github/new-repo.js';
@@ -7,6 +8,9 @@ import { parseClientTickIntervalMs } from './client-tick-interval.js';
7
8
  import { createCrowdyStudioStarterProject, } from './starter-projects.js';
8
9
  /** As ck-exec's mod names. */
9
10
  const MOD_NAME = /^[a-z0-9_-]{1,48}$/;
11
+ /** As a ck-exec build's crate names. */
12
+ const CRATE_NAME = /^[a-z][a-z0-9_-]{0,63}$/;
13
+ const LOG_LEVELS = ['error', 'warn', 'info', 'debug'];
10
14
  class OperationCancelledError extends Error {
11
15
  }
12
16
  /**
@@ -18,6 +22,7 @@ export class CrowdyStudioController {
18
22
  constructor(options) {
19
23
  this.options = options;
20
24
  this.state = {
25
+ serverEngine: 'player-compute',
21
26
  projects: [],
22
27
  project: null,
23
28
  personalLibraryFiles: [],
@@ -55,8 +60,15 @@ export class CrowdyStudioController {
55
60
  this.surfaceTimers = new Map();
56
61
  this.pageVisible = true;
57
62
  this.destroyed = false;
63
+ this.modConnection = null;
58
64
  // ----- GitHub repository card ------------------------------------------
59
65
  this.githubStatusGeneration = 0;
66
+ const serverEngine = options.serverEngine ?? (options.mods ? 'ck-exec' : 'player-compute');
67
+ if (serverEngine === 'ck-exec' && !options.mods) {
68
+ throw new Error("serverEngine 'ck-exec' needs the mods option (client.exec)");
69
+ }
70
+ this.mods = serverEngine === 'ck-exec' ? options.mods : null;
71
+ this.state = { ...this.state, serverEngine };
60
72
  if (options.onStateChange)
61
73
  this.listeners.add(options.onStateChange);
62
74
  }
@@ -186,9 +198,14 @@ export class CrowdyStudioController {
186
198
  if (this.state.project && !(await this.saveNow())) {
187
199
  throw new Error('Resolve or retry the current project save before creating another');
188
200
  }
201
+ const modStarter = this.mods && projectTargets(options.kind).includes('SERVER')
202
+ ? await this.mods.modStarter(this.options.appId)
203
+ : undefined;
189
204
  const input = createCrowdyStudioStarterProject({
190
205
  ...options,
191
206
  ...this.scope(),
207
+ ...(modStarter ? { modStarter } : {}),
208
+ engine: this.state.serverEngine,
192
209
  });
193
210
  const project = await this.options.projectProvider.createProject(input);
194
211
  this.installProject(project);
@@ -254,6 +271,7 @@ export class CrowdyStudioController {
254
271
  this.stopSurfacePolling();
255
272
  this.broker?.stop();
256
273
  this.broker = null;
274
+ this.closeModConnection();
257
275
  this.clearSaveTimers();
258
276
  this.editGeneration = 0;
259
277
  this.persistedGeneration = 0;
@@ -890,7 +908,7 @@ export class CrowdyStudioController {
890
908
  }
891
909
  this.checkOperation(operation);
892
910
  // A mod has no client pairing: its players call it by name.
893
- if (!this.options.mods) {
911
+ if (!this.mods) {
894
912
  const requiredClientName = project.metadata.pairingPreference === 'REQUIRED'
895
913
  ? client.name
896
914
  : null;
@@ -966,8 +984,10 @@ export class CrowdyStudioController {
966
984
  message: `Submitting ${name}`,
967
985
  },
968
986
  });
969
- if (target === 'SERVER' && this.options.mods) {
970
- return this.buildMod(name, files, operation);
987
+ if (this.mods) {
988
+ return target === 'SERVER'
989
+ ? this.buildMod(name, files, operation)
990
+ : this.buildClientHalf(name, files, operation);
971
991
  }
972
992
  // The server resolves the source from the project itself: the saved
973
993
  // files at the project revision, or — for a GITHUB project — the rust at
@@ -1019,37 +1039,75 @@ export class CrowdyStudioController {
1019
1039
  return null;
1020
1040
  }
1021
1041
  /**
1022
- * The SERVER target as a ck-exec mod: the project's server files are the crate, built on
1023
- * the platform, then deployed to this grid (switched off until {@link enableServer}).
1042
+ * The SERVER target as a ck-exec mod: the project's server crate files, built on the
1043
+ * platform, then deployed to this grid (switched off until {@link enableServer}). Other
1044
+ * project files, such as grid program assets, are not part of the crate.
1024
1045
  */
1025
1046
  async buildMod(name, files, operation) {
1026
- const mods = this.options.mods;
1027
- if (!MOD_NAME.test(name)) {
1028
- throw new Error(`The server module name '${name}' must be 1-48 lowercase letters, digits, - or _ to run as a mod`);
1029
- }
1047
+ const mods = this.mods;
1048
+ const problem = modNameProblem(this.requireProject());
1049
+ if (problem)
1050
+ throw new Error(problem);
1030
1051
  const { appId, gridId } = this.scope();
1031
1052
  const queued = await mods.modBuild(appId, {
1032
- name,
1033
- files: files.map((file) => ({ path: file.path, content: file.content })),
1053
+ name: crateName(name),
1054
+ files: crateFiles(files),
1055
+ });
1056
+ this.checkOperation(operation);
1057
+ const built = await this.awaitModBuild('SERVER', name, queued.buildId, operation);
1058
+ if (!built)
1059
+ return null;
1060
+ const mod = await mods.modDeploy(appId, gridId, name, built.buildId);
1061
+ this.checkOperation(operation);
1062
+ return { target: 'SERVER', name, versionId: String(mod.version) };
1063
+ }
1064
+ /**
1065
+ * The CLIENT target as a ck-exec mod's CLIENT half: the project's `crowdy-client-sdk` crate,
1066
+ * built on the platform (`modClientBuild`, a build of `kind` `client`). It is attached to the
1067
+ * project's mod when the project runs ({@link attachClientHalf}).
1068
+ */
1069
+ async buildClientHalf(name, files, operation) {
1070
+ const problem = modNameProblem(this.requireProject());
1071
+ if (problem)
1072
+ throw new Error(problem);
1073
+ const cargo = files.find((file) => file.path === 'Cargo.toml')?.content ?? '';
1074
+ if (/^\s*crowdy-compute-sdk\s*=/m.test(cargo)) {
1075
+ this.recordBuild('CLIENT', LEGACY_CLIENT_CRATE);
1076
+ this.update({
1077
+ runtime: {
1078
+ phase: 'COMPILE_FAILED',
1079
+ target: 'CLIENT',
1080
+ message: `${name} is a legacy player compute crate; a CLIENT half builds on crowdy-client-sdk`,
1081
+ },
1082
+ });
1083
+ return null;
1084
+ }
1085
+ const queued = await this.mods.modClientBuild(this.options.appId, {
1086
+ name: crateName(name),
1087
+ files: crateFiles(files),
1034
1088
  });
1035
1089
  this.checkOperation(operation);
1090
+ const built = await this.awaitModBuild('CLIENT', name, queued.buildId, operation);
1091
+ return built ? { target: 'CLIENT', name, versionId: built.buildId } : null;
1092
+ }
1093
+ /** Polls a mod build, a mod's or a CLIENT half's, recording its log; null when it did not succeed. */
1094
+ async awaitModBuild(target, name, buildId, operation) {
1095
+ const mods = this.mods;
1036
1096
  const limit = this.options.compilePollLimit ?? 60;
1037
1097
  const pollMs = this.options.compilePollMs ?? 1500;
1038
1098
  for (let attempt = 0; attempt < limit; attempt++) {
1039
- const b = await mods.modBuildStatus(appId, queued.buildId);
1099
+ const b = await mods.modBuildStatus(this.options.appId, buildId);
1040
1100
  this.checkOperation(operation);
1041
1101
  if (b.status === 'succeeded') {
1042
- this.recordBuild('SERVER', b.log ?? '');
1043
- const mod = await mods.modDeploy(appId, gridId, name, b.buildId);
1044
- this.checkOperation(operation);
1045
- return { target: 'SERVER', name, versionId: String(mod.version) };
1102
+ this.recordBuild(target, b.log ?? '');
1103
+ return b;
1046
1104
  }
1047
1105
  if (b.status === 'failed') {
1048
- this.recordBuild('SERVER', b.log ?? 'Compilation failed without output');
1106
+ this.recordBuild(target, b.log ?? 'Compilation failed without output');
1049
1107
  this.update({
1050
1108
  runtime: {
1051
1109
  phase: 'COMPILE_FAILED',
1052
- target: 'SERVER',
1110
+ target,
1053
1111
  message: `${name} failed to compile`,
1054
1112
  },
1055
1113
  });
@@ -1059,12 +1117,106 @@ export class CrowdyStudioController {
1059
1117
  this.checkOperation(operation);
1060
1118
  }
1061
1119
  const timeout = `Compilation timed out after ${limit} polls`;
1062
- this.recordBuild('SERVER', timeout);
1120
+ this.recordBuild(target, timeout);
1063
1121
  this.update({
1064
- runtime: { phase: 'COMPILE_FAILED', target: 'SERVER', message: timeout },
1122
+ runtime: { phase: 'COMPILE_FAILED', target, message: timeout },
1065
1123
  });
1066
1124
  return null;
1067
1125
  }
1126
+ /**
1127
+ * Attaches a built CLIENT half to the project's mod, consents to it as its author (the API
1128
+ * serves a CLIENT half only to a player who consented or trusts its author, the author too) and
1129
+ * fetches the served module for this browser's preview. A CLIENT-only project first makes sure
1130
+ * the mod it rides is the player's and switched on.
1131
+ */
1132
+ async attachClientHalf(compiled, operation) {
1133
+ const mods = this.mods;
1134
+ const { appId, gridId } = this.scope();
1135
+ const project = this.requireProject();
1136
+ const modName = projectModName(project);
1137
+ if (!projectTargets(project.kind).includes('SERVER')) {
1138
+ await this.ensureClientOnlyMod(modName, operation);
1139
+ }
1140
+ this.update({
1141
+ runtime: { phase: 'ENABLING', target: 'CLIENT', message: `Attaching ${compiled.name} to ${modName}` },
1142
+ });
1143
+ const attached = await mods.modClientDeploy(appId, gridId, modName, compiled.versionId);
1144
+ this.checkOperation(operation);
1145
+ await mods.consentClientMod(appId, attached.modId, attached.capabilityHash);
1146
+ this.checkOperation(operation);
1147
+ this.recordNote('CLIENT', `Attached to mod '${modName}' as CLIENT version ${attached.clientVersion} (capabilities ` +
1148
+ `${attached.capabilityHash}; ticks every ${attached.tickIntervalMs} ms). Visitors of grid ` +
1149
+ `${gridId} run it once they consent to it or trust you; you consented to it as its author.`);
1150
+ let artifact;
1151
+ try {
1152
+ artifact = await mods.modClientArtifactBytes(appId, attached.modId);
1153
+ }
1154
+ catch (error) {
1155
+ if (error instanceof CrowdyGraphQLError && error.code === 'NOT_FOUND') {
1156
+ throw new CrowdyError({
1157
+ message: `${compiled.name} is attached to mod '${modName}', but its preview did not load: the ` +
1158
+ 'API serves a CLIENT half only while its mod is switched on and not held, to a player ' +
1159
+ `with run_client_code who stands in grid ${gridId}`,
1160
+ cause: error,
1161
+ });
1162
+ }
1163
+ if (error instanceof CrowdyGraphQLError && error.code === 'RATE_LIMITED') {
1164
+ throw new CrowdyError({
1165
+ message: `${compiled.name} is attached to mod '${modName}', but its preview was fetched too ` +
1166
+ 'often (12 a minute); deploy again in a minute',
1167
+ cause: error,
1168
+ });
1169
+ }
1170
+ throw error;
1171
+ }
1172
+ this.checkOperation(operation);
1173
+ if (artifact.digest !== attached.digest || artifact.clientVersion !== attached.clientVersion) {
1174
+ throw new Error('The served CLIENT half is not the one just attached; deploy again');
1175
+ }
1176
+ return artifact;
1177
+ }
1178
+ /**
1179
+ * A CLIENT half rides a mod: without a mod of this name running as the player on the grid,
1180
+ * the mod starter's server half is deployed under it first, and the log says so. The mod is
1181
+ * switched on, since only a running mod's CLIENT half is served.
1182
+ */
1183
+ async ensureClientOnlyMod(name, operation) {
1184
+ const mods = this.mods;
1185
+ const { appId, gridId } = this.scope();
1186
+ const mine = (await mods.myMods(appId)).find((mod) => String(mod.gridId) === gridId && mod.name === name);
1187
+ this.checkOperation(operation);
1188
+ if (!mine) {
1189
+ if (!this.canTarget('SERVER', 'write') || !this.canTarget('SERVER', 'run')) {
1190
+ throw new Error(`A CLIENT half rides a mod, and grid ${gridId} has no mod '${name}' of yours; deploying one needs SERVER write and run permissions here`);
1191
+ }
1192
+ this.update({
1193
+ runtime: { phase: 'COMPILING', target: 'SERVER', message: `Deploying the mod starter as ${name}` },
1194
+ });
1195
+ const starter = await mods.modStarter(appId);
1196
+ this.checkOperation(operation);
1197
+ const queued = await mods.modBuild(appId, { name: crateName(name), files: starter.files });
1198
+ this.checkOperation(operation);
1199
+ const built = await this.awaitModBuild('SERVER', name, queued.buildId, operation);
1200
+ if (!built) {
1201
+ throw new Error(`The mod starter did not build, so the CLIENT half has no mod '${name}' to ride`);
1202
+ }
1203
+ await mods.modDeploy(appId, gridId, name, built.buildId);
1204
+ this.checkOperation(operation);
1205
+ this.recordNote('SERVER', `Grid ${gridId} had no mod '${name}' of yours, and a CLIENT half rides a mod: deployed the ` +
1206
+ `ck-exec mod starter (${starter.crate}) as '${name}', its server half.`);
1207
+ }
1208
+ if (!mine?.enabled) {
1209
+ await mods.modSetEnabled(appId, gridId, name, true);
1210
+ this.checkOperation(operation);
1211
+ if (mine)
1212
+ this.recordNote('SERVER', `Switched mod '${name}' on, so its CLIENT half is served.`);
1213
+ }
1214
+ }
1215
+ recordNote(target, note) {
1216
+ this.update({
1217
+ buildOutput: [this.state.buildOutput, `## ${target}\n${note}`].filter(Boolean).join('\n\n'),
1218
+ });
1219
+ }
1068
1220
  recordBuild(target, log) {
1069
1221
  const section = `## ${target}\n${log || 'Compiled successfully.'}`;
1070
1222
  const authoritativeDiagnostics = [
@@ -1087,9 +1239,9 @@ export class CrowdyStudioController {
1087
1239
  this.checkOperation(operation);
1088
1240
  }
1089
1241
  async setServerEnabled(name, enabled) {
1090
- if (this.options.mods) {
1242
+ if (this.mods) {
1091
1243
  const { appId, gridId } = this.scope();
1092
- await this.options.mods.modSetEnabled(appId, gridId, name, enabled);
1244
+ await this.mods.modSetEnabled(appId, gridId, name, enabled);
1093
1245
  return;
1094
1246
  }
1095
1247
  await this.options.playerCompute.setEnabled({
@@ -1103,28 +1255,47 @@ export class CrowdyStudioController {
1103
1255
  throw new Error(`${compiled.name} compiled successfully, but run_client_code is unavailable on this grid`);
1104
1256
  }
1105
1257
  const runtime = this.clientRuntimeOptions();
1106
- const artifact = await this.options.playerCompute.artifactBytes({
1107
- ...this.scope(),
1108
- name: compiled.name,
1109
- versionId: compiled.versionId,
1110
- });
1111
- this.checkOperation(operation);
1112
- if (artifact.versionId !== compiled.versionId ||
1113
- !artifact.artifactHash ||
1114
- artifact.bytes.byteLength === 0) {
1115
- throw new Error('Client artifact did not match the compiled project version');
1116
- }
1117
- const brokerOptions = {
1118
- ...runtime,
1119
- artifactHash: artifact.artifactHash,
1120
- fuelPerDispatch: artifact.fuelPerDispatch,
1121
- onPresentation: this.options.onPresentation,
1122
- tickIntervalMs: this.options.clientTickIntervalMs ??
1123
- parseClientTickIntervalMs(this.requireProject().files.find((file) => file.target === 'CLIENT' && file.path === 'Cargo.toml')?.content),
1124
- };
1258
+ let brokerOptions;
1259
+ let bytes;
1260
+ if (this.mods) {
1261
+ const half = await this.attachClientHalf(compiled, operation);
1262
+ bytes = half.bytes;
1263
+ brokerOptions = {
1264
+ ...runtime,
1265
+ engine: 'ck-exec',
1266
+ moduleName: half.name,
1267
+ artifactHash: half.digest,
1268
+ fuelPerDispatch: half.fuelPerDispatch,
1269
+ consentedHostCalls: half.capabilitySummary.hostFunctions,
1270
+ onPresentation: this.options.onPresentation,
1271
+ tickIntervalMs: this.options.clientTickIntervalMs ?? half.tickIntervalMs,
1272
+ };
1273
+ }
1274
+ else {
1275
+ const artifact = await this.options.playerCompute.artifactBytes({
1276
+ ...this.scope(),
1277
+ name: compiled.name,
1278
+ versionId: compiled.versionId,
1279
+ });
1280
+ this.checkOperation(operation);
1281
+ if (artifact.versionId !== compiled.versionId ||
1282
+ !artifact.artifactHash ||
1283
+ artifact.bytes.byteLength === 0) {
1284
+ throw new Error('Client artifact did not match the compiled project version');
1285
+ }
1286
+ bytes = artifact.bytes;
1287
+ brokerOptions = {
1288
+ ...runtime,
1289
+ artifactHash: artifact.artifactHash,
1290
+ fuelPerDispatch: artifact.fuelPerDispatch,
1291
+ onPresentation: this.options.onPresentation,
1292
+ tickIntervalMs: this.options.clientTickIntervalMs ??
1293
+ parseClientTickIntervalMs(this.requireProject().files.find((file) => file.target === 'CLIENT' && file.path === 'Cargo.toml')?.content),
1294
+ };
1295
+ }
1125
1296
  const broker = this.options.brokerFactory?.(brokerOptions) ??
1126
1297
  new PlayerCodeBroker(brokerOptions);
1127
- await broker.start(artifact.bytes);
1298
+ await broker.start(bytes);
1128
1299
  this.checkOperation(operation);
1129
1300
  const previous = this.broker;
1130
1301
  this.broker = broker;
@@ -1151,10 +1322,12 @@ export class CrowdyStudioController {
1151
1322
  this.broker = null;
1152
1323
  }
1153
1324
  }
1154
- if (projectTargets(project.kind).includes('SERVER')) {
1325
+ // On ck-exec a CLIENT-only project's CLIENT half rides a mod too: switched off, it is no
1326
+ // longer served to visitors.
1327
+ if (projectTargets(project.kind).includes('SERVER') || this.mods) {
1155
1328
  serverStopped = false;
1156
1329
  try {
1157
- await this.setServerEnabled(moduleNameFor(project, 'SERVER'), false);
1330
+ await this.setServerEnabled(this.mods ? projectModName(project) : moduleNameFor(project, 'SERVER'), false);
1158
1331
  serverStopped = true;
1159
1332
  }
1160
1333
  catch (error) {
@@ -1182,6 +1355,12 @@ export class CrowdyStudioController {
1182
1355
  if (!projectTargets(project.kind).includes('SERVER')) {
1183
1356
  throw new Error('Invoke requires a SERVER target');
1184
1357
  }
1358
+ if (this.mods) {
1359
+ const result = await this.callMod(moduleNameFor(project, 'SERVER'), exportName.trim() || 'state', paramsJson);
1360
+ this.checkAgentOperation(agentOperation);
1361
+ this.update({ invokeResult: result });
1362
+ return result;
1363
+ }
1185
1364
  const result = await this.options.playerCompute.invoke({
1186
1365
  ...this.scope(),
1187
1366
  moduleName: moduleNameFor(project, 'SERVER'),
@@ -1214,10 +1393,65 @@ export class CrowdyStudioController {
1214
1393
  else
1215
1394
  this.stopSurfacePolling();
1216
1395
  }
1396
+ /**
1397
+ * The mod's endpoint `method` with `paramsJson` as its arguments, through one exec
1398
+ * connection to the mod (`mod:<name>`, keyed by the grid) that later calls reuse.
1399
+ */
1400
+ async callMod(name, method, paramsJson) {
1401
+ const text = paramsJson?.trim();
1402
+ let args = null;
1403
+ if (text) {
1404
+ try {
1405
+ args = JSON.parse(text);
1406
+ }
1407
+ catch {
1408
+ throw new Error('The call arguments must be JSON');
1409
+ }
1410
+ }
1411
+ if (this.modConnection?.name !== name) {
1412
+ this.closeModConnection();
1413
+ const connection = this.mods.connect(this.options.appId, {
1414
+ nodeType: modNodeType(name),
1415
+ key: this.options.gridId,
1416
+ });
1417
+ connection.catch(() => {
1418
+ if (this.modConnection?.connection === connection)
1419
+ this.modConnection = null;
1420
+ });
1421
+ this.modConnection = { name, connection };
1422
+ }
1423
+ const connection = await this.modConnection.connection;
1424
+ const started = Date.now();
1425
+ const value = await connection.call(modNodeType(name), this.options.gridId, method, args);
1426
+ return {
1427
+ resultJson: JSON.stringify(value ?? null, (_key, v) => typeof v === 'bigint' ? v.toString() : v),
1428
+ durationUs: (Date.now() - started) * 1000,
1429
+ };
1430
+ }
1431
+ closeModConnection() {
1432
+ const open = this.modConnection;
1433
+ this.modConnection = null;
1434
+ void open?.connection.then((connection) => connection.close(), () => { });
1435
+ }
1217
1436
  async refreshSurface(surface) {
1218
1437
  if (!this.state.project)
1219
1438
  return;
1220
1439
  const serverName = this.state.project.metadata.serverModuleName;
1440
+ if (this.mods && surface !== 'usage') {
1441
+ // ck-exec records no runs; a mod's log is its own `ctx.log` lines.
1442
+ if (surface === 'runs') {
1443
+ this.update({ runs: [] });
1444
+ return;
1445
+ }
1446
+ const modName = projectModNameOrNull(this.state.project);
1447
+ const lines = modName
1448
+ ? await this.mods.modLogs(this.options.appId, this.options.gridId, modName, {
1449
+ limit: 50,
1450
+ })
1451
+ : [];
1452
+ this.update({ logs: lines.map((line) => modLogRow(modName, line)) });
1453
+ return;
1454
+ }
1221
1455
  if (surface === 'runs') {
1222
1456
  this.update({
1223
1457
  runs: await this.options.playerCompute.runs({
@@ -1239,11 +1473,14 @@ export class CrowdyStudioController {
1239
1473
  });
1240
1474
  return;
1241
1475
  }
1476
+ // Player compute's units and compile quota, which nothing spends on ck-exec.
1242
1477
  const [usage, wallet] = await Promise.all([
1243
- this.options.playerCompute.usage({ appId: this.options.appId }),
1478
+ !this.mods
1479
+ ? this.options.playerCompute.usage({ appId: this.options.appId })
1480
+ : Promise.resolve(null),
1244
1481
  this.options.playerWallet?.balance() ?? Promise.resolve(null),
1245
1482
  ]);
1246
- this.update({ usage: normalizeUsage(usage), wallet });
1483
+ this.update({ usage: usage ? normalizeUsage(usage) : null, wallet });
1247
1484
  }
1248
1485
  destroy() {
1249
1486
  if (this.destroyed)
@@ -1254,6 +1491,7 @@ export class CrowdyStudioController {
1254
1491
  this.stopSurfacePolling();
1255
1492
  this.broker?.stop();
1256
1493
  this.broker = null;
1494
+ this.closeModConnection();
1257
1495
  this.listeners.clear();
1258
1496
  this.humanEditListeners.clear();
1259
1497
  }
@@ -1499,6 +1737,67 @@ function sameFileRef(a, b) {
1499
1737
  function compareProjectFile(a, b) {
1500
1738
  return crowdyStudioFileKey(a.target, a.path).localeCompare(crowdyStudioFileKey(b.target, b.path));
1501
1739
  }
1740
+ /** The node type players call a mod by, as `execModType`. */
1741
+ function modNodeType(name) {
1742
+ return `mod:${name}`;
1743
+ }
1744
+ /** What a mod build takes: `Cargo.toml`, `README.md` and Rust under `src/`. */
1745
+ function isModCrateFile(path) {
1746
+ return (path === 'Cargo.toml' ||
1747
+ path === 'README.md' ||
1748
+ (path.startsWith('src/') && path.endsWith('.rs')));
1749
+ }
1750
+ function crateFiles(files) {
1751
+ return files
1752
+ .filter((file) => isModCrateFile(file.path))
1753
+ .map((file) => ({ path: file.path, content: file.content }));
1754
+ }
1755
+ /** A build's crate name for a module: its own when it is one, else `mod-<name>`. */
1756
+ function crateName(name) {
1757
+ return CRATE_NAME.test(name) ? name : `mod-${name}`;
1758
+ }
1759
+ /**
1760
+ * The mod a ck-exec project runs as: its SERVER module's name, or for a CLIENT-only project the
1761
+ * CLIENT module's, which names the mod its CLIENT half rides.
1762
+ */
1763
+ function projectModName(project) {
1764
+ return moduleNameFor(project, projectTargets(project.kind).includes('SERVER') ? 'SERVER' : 'CLIENT');
1765
+ }
1766
+ function projectModNameOrNull(project) {
1767
+ try {
1768
+ return projectModName(project);
1769
+ }
1770
+ catch {
1771
+ return null;
1772
+ }
1773
+ }
1774
+ /** Why a ck-exec project's mod name cannot be a mod's, or null. */
1775
+ function modNameProblem(project) {
1776
+ const name = projectModName(project);
1777
+ if (MOD_NAME.test(name))
1778
+ return null;
1779
+ return projectTargets(project.kind).includes('SERVER')
1780
+ ? `The server module name '${name}' must be 1-48 lowercase letters, digits, - or _ to run as a mod`
1781
+ : `The CLIENT module name '${name}' names the mod its CLIENT half rides, so it must be 1-48 lowercase letters, digits, - or _`;
1782
+ }
1783
+ const LEGACY_CLIENT_CRATE = 'This CLIENT crate depends on crowdy-compute-sdk, legacy player compute. On ck-exec a CLIENT ' +
1784
+ 'target is a mod\u2019s CLIENT half, one crowdy-client-sdk crate: in Cargo.toml replace the ' +
1785
+ 'crowdy-compute-sdk line with crowdy-client-sdk = "0.1.0", and in src/ use crowdy_client_sdk ' +
1786
+ 'in place of crowdy_compute_sdk. Its host calls are the same, less the Game Model and sessions.';
1787
+ function modLogRow(moduleName, line) {
1788
+ const level = LOG_LEVELS[line.level] ?? 'debug';
1789
+ return {
1790
+ runId: line.id,
1791
+ moduleName,
1792
+ triggerSource: level,
1793
+ startedAt: line.at,
1794
+ durationUs: 0,
1795
+ fuelUsed: '0',
1796
+ success: level !== 'error',
1797
+ errorMessage: line.text,
1798
+ level,
1799
+ };
1800
+ }
1502
1801
  function moduleNameFor(project, target) {
1503
1802
  const name = target === 'SERVER'
1504
1803
  ? project.metadata.serverModuleName
@@ -1 +1 @@
1
- {"version":3,"file":"dom-shell.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/dom-shell.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dom-shell.d.ts","sourceRoot":"","sources":["../../src/crowdy-studio/dom-shell.ts"],"names":[],"mappings":"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"}
@@ -274,12 +274,13 @@ export class CrowdyStudioDomShell {
274
274
  clientModuleName: this.clientModuleName.value,
275
275
  }));
276
276
  this.pairing.addEventListener('change', () => this.controller.setPairingPreference(this.pairing.value));
277
- this.invokeExport = input('Export name');
278
- this.invokeExport.value = 'invoke';
277
+ const onMod = controller.getState().serverEngine === 'ck-exec';
278
+ this.invokeExport = input(onMod ? 'Endpoint' : 'Export name');
279
+ this.invokeExport.value = onMod ? 'state' : 'invoke';
279
280
  this.invokeParams = document.createElement('textarea');
280
281
  this.invokeParams.placeholder = '{"example": true}';
281
- this.invokeParams.setAttribute('aria-label', 'Invoke JSON parameters');
282
- const invokeButton = button('Invoke server export');
282
+ this.invokeParams.setAttribute('aria-label', onMod ? 'Call arguments (JSON)' : 'Invoke JSON parameters');
283
+ const invokeButton = button(onMod ? 'Call mod endpoint' : 'Invoke server export');
283
284
  this.invokeResult = document.createElement('pre');
284
285
  const invokeControls = element('div', 'ck-crowdy-studio-invoke');
285
286
  invokeControls.append(this.invokeExport, this.invokeParams, invokeButton);
@@ -310,7 +311,9 @@ export class CrowdyStudioDomShell {
310
311
  this.renderProblems(state);
311
312
  this.renderBuild(state);
312
313
  this.renderRuntimeRows(this.logsPanel, state.logs);
313
- this.renderRuntimeRows(this.runsPanel, state.runs);
314
+ this.renderRuntimeRows(this.runsPanel, state.runs, state.serverEngine === 'ck-exec'
315
+ ? 'A mod keeps no run records; its log lines are under Logs.'
316
+ : undefined);
314
317
  this.invokeResult.textContent = state.invokeResult
315
318
  ? formatInvokeResult(state.invokeResult)
316
319
  : '';
@@ -900,7 +903,9 @@ export class CrowdyStudioDomShell {
900
903
  setInputUnlessFocused(this.clientModuleName, project.metadata.clientModuleName ?? '');
901
904
  this.serverModuleName.disabled = !projectTargets(project.kind).includes('SERVER');
902
905
  this.clientModuleName.disabled = !projectTargets(project.kind).includes('CLIENT');
903
- this.pairing.disabled = project.kind !== 'FULL_STACK';
906
+ // A mod has no pairing: its CLIENT half rides it.
907
+ this.pairing.disabled =
908
+ project.kind !== 'FULL_STACK' || state.serverEngine === 'ck-exec';
904
909
  this.pairing.value = project.metadata.pairingPreference;
905
910
  }
906
911
  renderProblems(state) {
@@ -959,15 +964,17 @@ export class CrowdyStudioDomShell {
959
964
  output.textContent = state.buildOutput || 'No build has run.';
960
965
  this.buildPanel.append(output);
961
966
  }
962
- renderRuntimeRows(panel, rows) {
967
+ renderRuntimeRows(panel, rows, emptyText = 'No runtime records.') {
963
968
  panel.replaceChildren();
964
969
  if (rows.length === 0) {
965
- panel.append(empty('No runtime records.'));
970
+ panel.append(empty(emptyText));
966
971
  return;
967
972
  }
968
973
  for (const row of rows) {
969
974
  const line = document.createElement('pre');
970
- line.textContent = `${row.success ? '✓' : '✗'} ${row.startedAt} ${row.moduleName} ${row.triggerSource} · ${row.durationUs}µs · ${row.fuelUsed} fuel${row.errorMessage ? `\n${row.errorMessage}` : ''}`;
975
+ line.textContent = row.level
976
+ ? `${row.success ? '·' : '✗'} ${row.startedAt} ${row.moduleName} ${row.level}\n${row.errorMessage ?? ''}`
977
+ : `${row.success ? '✓' : '✗'} ${row.startedAt} ${row.moduleName} ${row.triggerSource} · ${row.durationUs}µs · ${row.fuelUsed} fuel${row.errorMessage ? `\n${row.errorMessage}` : ''}`;
971
978
  panel.append(line);
972
979
  }
973
980
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * A persistent, game-owned HUD layer for player-compute CLIENT mods. A
2
+ * A persistent, game-owned HUD layer for CLIENT mods (ck-exec CLIENT halves
3
+ * and legacy player-compute CLIENT modules). A
3
4
  * running client mod never touches the game DOM; it emits `hud_set` payloads
4
5
  * that the broker forwards here. This layer lives outside the studio panel so
5
6
  * a mod's HUD survives panel re-renders and keeps rendering after the coding
@@ -1 +1 @@
1
- {"version":3,"file":"hud-layer.d.ts","sourceRoot":"","sources":["../../../src/crowdy-studio/embed/hud-layer.ts"],"names":[],"mappings":"AAEA;;;;;;;;;GASG;AACH,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAE3D,OAAO,CAAC,UAAU;IAUlB,+DAA+D;IAC/D,GAAG,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI;IAKtC,oEAAoE;IACpE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI5B,KAAK,IAAI,IAAI;IAKb;;;;OAIG;IACH,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI;IAShE,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,SAAS;IAajB,mEAAmE;IACnE,OAAO,CAAC,QAAQ;CAQjB"}
1
+ {"version":3,"file":"hud-layer.d.ts","sourceRoot":"","sources":["../../../src/crowdy-studio/embed/hud-layer.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,IAAI,CAA+B;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAkC;IAE3D,OAAO,CAAC,UAAU;IAUlB,+DAA+D;IAC/D,GAAG,CAAC,KAAK,EAAE,oBAAoB,GAAG,IAAI;IAKtC,oEAAoE;IACpE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAI5B,KAAK,IAAI,IAAI;IAKb;;;;OAIG;IACH,YAAY,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI;IAShE,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,MAAM;IAYd,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,SAAS;IAajB,mEAAmE;IACnE,OAAO,CAAC,QAAQ;CAQjB"}