connectbase-client 3.13.1 → 3.14.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.
package/dist/index.mjs CHANGED
@@ -14,6 +14,20 @@ var AuthError = class extends Error {
14
14
  this.name = "AuthError";
15
15
  }
16
16
  };
17
+ var GameError = class extends Error {
18
+ constructor(init) {
19
+ super(init.message || init.code || "GameError");
20
+ this.name = "GameError";
21
+ this.code = init.code || "UNKNOWN";
22
+ this.phase = init.phase;
23
+ this.feature = init.feature;
24
+ this.roomId = init.roomId;
25
+ this.scriptId = init.scriptId;
26
+ this.originClientId = init.originClientId;
27
+ this.requested = init.requested;
28
+ this.available = init.available;
29
+ }
30
+ };
17
31
 
18
32
  // src/core/abort.ts
19
33
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
@@ -6235,6 +6249,21 @@ var GameConfigAPI = class {
6235
6249
  };
6236
6250
 
6237
6251
  // src/api/game.ts
6252
+ function parseGameError(msg) {
6253
+ const payload = msg.data ?? msg;
6254
+ const get = (k) => payload[k];
6255
+ return new GameError({
6256
+ code: get("code") || msg.code,
6257
+ message: get("message") || "Unknown error",
6258
+ phase: get("phase") || msg.phase,
6259
+ feature: get("feature") || msg.feature,
6260
+ roomId: get("room_id") || msg.room_id,
6261
+ scriptId: get("script_id") || msg.script_id,
6262
+ originClientId: get("origin_client_id") || msg.origin_client_id,
6263
+ requested: get("requested") || msg.requested,
6264
+ available: get("available") || msg.available
6265
+ });
6266
+ }
6238
6267
  var getDefaultGameServerUrl = () => {
6239
6268
  if (typeof window !== "undefined") {
6240
6269
  const hostname = window.location.hostname;
@@ -6264,6 +6293,8 @@ var GameRoom = class {
6264
6293
  this.actionSequence = 0;
6265
6294
  this._roomId = null;
6266
6295
  this._state = null;
6296
+ this._scriptName = null;
6297
+ this._scriptVersion = null;
6267
6298
  this._isConnected = false;
6268
6299
  this.msgIdCounter = 0;
6269
6300
  this.config = {
@@ -6353,21 +6384,48 @@ var GameRoom = class {
6353
6384
  }
6354
6385
  this._isConnected = false;
6355
6386
  this._roomId = null;
6387
+ this._scriptName = null;
6388
+ this._scriptVersion = null;
6356
6389
  }
6357
6390
  /**
6358
- * 룸 생성
6391
+ * 룸 생성 (호환 시그니처) — 초기 상태만 반환.
6392
+ *
6393
+ * Attached script 의 검증(scriptName/scriptVersion echo 검사) 이 필요하면
6394
+ * 인스턴스 getter (`room.scriptName`, `room.scriptVersion`) 를 함께 사용하거나,
6395
+ * 메타까지 한 번에 받고 싶다면 {@link createRoomDetailed} 를 사용한다.
6396
+ *
6397
+ * 미존재/비활성 scriptName 은 서버가 `SCRIPT_NOT_FOUND` 로 즉시 reject 한다.
6398
+ * reject 값은 `GameError` 인스턴스이며 `.code`/`.requested`/`.available` 로 분기 가능.
6359
6399
  */
6360
6400
  createRoom(config = {}) {
6401
+ return this.createRoomDetailed(config).then((r) => r.state);
6402
+ }
6403
+ /**
6404
+ * 룸 생성 + 메타 — server 가 attach 한 lua script 이름/버전을 함께 반환.
6405
+ *
6406
+ * @example
6407
+ * const { state, scriptName, scriptVersion } = await room.createRoomDetailed({ scriptName: 'njb-main' })
6408
+ * if (scriptName !== 'njb-main') throw new Error('script mismatch')
6409
+ * console.log('attached', scriptName, 'v', scriptVersion)
6410
+ */
6411
+ createRoomDetailed(config = {}) {
6361
6412
  return new Promise((resolve, reject) => {
6362
6413
  const handler = (msg) => {
6363
6414
  if (msg.type === "room_created") {
6364
6415
  const data = msg.data;
6365
6416
  this._roomId = data.room_id;
6366
6417
  this._state = data.initial_state;
6367
- resolve(data.initial_state);
6418
+ this._scriptName = data.script_name ?? null;
6419
+ this._scriptVersion = data.script_version ?? null;
6420
+ resolve({
6421
+ roomId: data.room_id,
6422
+ state: data.initial_state,
6423
+ scriptName: data.script_name,
6424
+ scriptVersion: data.script_version
6425
+ });
6368
6426
  return true;
6369
6427
  } else if (msg.type === "error") {
6370
- reject(new Error(msg.data.message));
6428
+ reject(parseGameError(msg));
6371
6429
  return true;
6372
6430
  }
6373
6431
  return false;
@@ -6375,6 +6433,14 @@ var GameRoom = class {
6375
6433
  this.sendWithHandler("create_room", toCreateRoomWire(config), handler, 15e3, reject);
6376
6434
  });
6377
6435
  }
6436
+ /** Attached lua script 이름 — createRoom 이후 server 가 echo 한 값. */
6437
+ get scriptName() {
6438
+ return this._scriptName;
6439
+ }
6440
+ /** Attached lua script 의 active version. */
6441
+ get scriptVersion() {
6442
+ return this._scriptVersion;
6443
+ }
6378
6444
  /**
6379
6445
  * 룸 참가
6380
6446
  */
@@ -6385,10 +6451,12 @@ var GameRoom = class {
6385
6451
  const data = msg.data;
6386
6452
  this._roomId = data.room_id;
6387
6453
  this._state = data.initial_state;
6454
+ this._scriptName = null;
6455
+ this._scriptVersion = null;
6388
6456
  resolve(data.initial_state);
6389
6457
  return true;
6390
6458
  } else if (msg.type === "error") {
6391
- reject(new Error(msg.data.message));
6459
+ reject(parseGameError(msg));
6392
6460
  return true;
6393
6461
  }
6394
6462
  return false;
@@ -6402,17 +6470,19 @@ var GameRoom = class {
6402
6470
  leaveRoom() {
6403
6471
  return new Promise((resolve, reject) => {
6404
6472
  if (!this._roomId) {
6405
- reject(new Error("Not in a room"));
6473
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
6406
6474
  return;
6407
6475
  }
6408
6476
  const handler = (msg) => {
6409
6477
  if (msg.type === "room_left") {
6410
6478
  this._roomId = null;
6411
6479
  this._state = null;
6480
+ this._scriptName = null;
6481
+ this._scriptVersion = null;
6412
6482
  resolve();
6413
6483
  return true;
6414
6484
  } else if (msg.type === "error") {
6415
- reject(new Error(msg.data.message));
6485
+ reject(parseGameError(msg));
6416
6486
  return true;
6417
6487
  }
6418
6488
  return false;
@@ -6425,7 +6495,7 @@ var GameRoom = class {
6425
6495
  */
6426
6496
  sendAction(action) {
6427
6497
  if (!this._roomId) {
6428
- throw new Error("Not in a room");
6498
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
6429
6499
  }
6430
6500
  this.send("action", {
6431
6501
  type: action.type,
@@ -6439,7 +6509,7 @@ var GameRoom = class {
6439
6509
  */
6440
6510
  sendChat(message) {
6441
6511
  if (!this._roomId) {
6442
- throw new Error("Not in a room");
6512
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
6443
6513
  }
6444
6514
  this.send("chat", { message });
6445
6515
  }
@@ -6449,7 +6519,7 @@ var GameRoom = class {
6449
6519
  requestState() {
6450
6520
  return new Promise((resolve, reject) => {
6451
6521
  if (!this._roomId) {
6452
- reject(new Error("Not in a room"));
6522
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
6453
6523
  return;
6454
6524
  }
6455
6525
  const handler = (msg) => {
@@ -6459,7 +6529,7 @@ var GameRoom = class {
6459
6529
  resolve(state);
6460
6530
  return true;
6461
6531
  } else if (msg.type === "error") {
6462
- reject(new Error(msg.data.message));
6532
+ reject(parseGameError(msg));
6463
6533
  return true;
6464
6534
  }
6465
6535
  return false;
@@ -6478,7 +6548,7 @@ var GameRoom = class {
6478
6548
  resolve(data.rooms);
6479
6549
  return true;
6480
6550
  } else if (msg.type === "error") {
6481
- reject(new Error(msg.data.message));
6551
+ reject(parseGameError(msg));
6482
6552
  return true;
6483
6553
  }
6484
6554
  return false;
@@ -6500,7 +6570,7 @@ var GameRoom = class {
6500
6570
  resolve(rtt);
6501
6571
  return true;
6502
6572
  } else if (msg.type === "error") {
6503
- reject(new Error(msg.data.message));
6573
+ reject(parseGameError(msg));
6504
6574
  return true;
6505
6575
  }
6506
6576
  return false;
@@ -6558,12 +6628,12 @@ var GameRoom = class {
6558
6628
  this.ws?.addEventListener("message", messageHandler);
6559
6629
  timeoutId = setTimeout(() => {
6560
6630
  cleanup();
6561
- const err = new Error(`Request '${type}' timed out after ${timeoutMs}ms`);
6562
- onError?.(err);
6563
- this.handlers.onError?.({
6631
+ const err = new GameError({
6564
6632
  code: "TIMEOUT",
6565
- message: err.message
6633
+ message: `Request '${type}' timed out after ${timeoutMs}ms`
6566
6634
  });
6635
+ onError?.(err);
6636
+ this.handlers.onError?.(err);
6567
6637
  }, timeoutMs);
6568
6638
  try {
6569
6639
  this.send(type, data, msgId);
@@ -6597,10 +6667,7 @@ var GameRoom = class {
6597
6667
  });
6598
6668
  break;
6599
6669
  case "error":
6600
- this.handlers.onError?.({
6601
- code: msg.code || "UNKNOWN",
6602
- message: msg.message || "Unknown error"
6603
- });
6670
+ this.handlers.onError?.(parseGameError(msg));
6604
6671
  break;
6605
6672
  default:
6606
6673
  break;
@@ -6678,10 +6745,10 @@ var GameRoom = class {
6678
6745
  scheduleReconnect(roomId) {
6679
6746
  if (this.reconnectAttempts >= (this.config.maxReconnectAttempts ?? 5)) {
6680
6747
  console.error("Max reconnect attempts reached");
6681
- this.handlers.onError?.({
6748
+ this.handlers.onError?.(new GameError({
6682
6749
  code: "MAX_RECONNECT_ATTEMPTS",
6683
6750
  message: "Maximum reconnection attempts reached"
6684
- });
6751
+ }));
6685
6752
  return;
6686
6753
  }
6687
6754
  const delay = Math.min(
@@ -6702,10 +6769,11 @@ var GameRoom = class {
6702
6769
  console.log(`Successfully rejoined room ${previousRoomId}`);
6703
6770
  } catch (rejoinError) {
6704
6771
  console.error(`Failed to rejoin room ${previousRoomId}:`, rejoinError);
6705
- this.handlers.onError?.({
6772
+ this.handlers.onError?.(new GameError({
6706
6773
  code: "REJOIN_FAILED",
6707
- message: `Failed to rejoin room: ${rejoinError}`
6708
- });
6774
+ message: `Failed to rejoin room: ${rejoinError}`,
6775
+ roomId: previousRoomId
6776
+ }));
6709
6777
  }
6710
6778
  }
6711
6779
  } catch {
@@ -6914,6 +6982,38 @@ var GameAPI = class {
6914
6982
  }
6915
6983
  return headers;
6916
6984
  }
6985
+ // game-server (`game.connectbase.world`) 전용 fetch.
6986
+ //
6987
+ // 회귀 가드 (v3.14.1, NJB report 2026-05-14): matchqueue / leaderboard / scripts 16개
6988
+ // 메서드가 v3.0.0 BREAKING 이후 `this.http.*` 를 통해 기본 baseUrl (=core-server,
6989
+ // `api.connectbase.world`) 로 라우팅되면서 404 회귀. 호스트 분리된 game-server 경로는
6990
+ // 반드시 `this.gameServerUrl` 을 사용해야 한다. 회귀 재발 방지 테스트:
6991
+ // test/game-host-routing.test.ts.
6992
+ async gameFetch(method, path, body, errCode = "GAME_REQUEST_FAILED") {
6993
+ const init = { method, headers: { ...this.getHeaders() } };
6994
+ if (body !== void 0) {
6995
+ init.headers["Content-Type"] = "application/json";
6996
+ init.body = JSON.stringify(body);
6997
+ }
6998
+ const response = await fetch(`${this.gameServerUrl}${path}`, init);
6999
+ if (!response.ok) {
7000
+ let serverCode = errCode;
7001
+ let message = `${method} ${path} failed: ${response.statusText}`;
7002
+ try {
7003
+ const data = await response.json();
7004
+ if (data && typeof data.error === "string") {
7005
+ message = data.error;
7006
+ serverCode = data.error;
7007
+ }
7008
+ } catch {
7009
+ }
7010
+ throw new ApiError(response.status, message, serverCode);
7011
+ }
7012
+ if (response.status === 204) return void 0;
7013
+ const contentType = response.headers.get("content-type") || "";
7014
+ if (!contentType.includes("application/json")) return void 0;
7015
+ return response.json();
7016
+ }
6917
7017
  // ─────────────────────────────────────────────────────────────────────
6918
7018
  // Phase 3 — Matchqueue primitive (mechanism only)
6919
7019
  //
@@ -6934,9 +7034,11 @@ var GameAPI = class {
6934
7034
  * @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
6935
7035
  */
6936
7036
  async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
6937
- return this.http.post(
7037
+ return this.gameFetch(
7038
+ "POST",
6938
7039
  `/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
6939
- { ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
7040
+ { ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 },
7041
+ "GAME_ENQUEUE_MATCH_FAILED"
6940
7042
  );
6941
7043
  }
6942
7044
  /**
@@ -6945,16 +7047,22 @@ var GameAPI = class {
6945
7047
  * @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
6946
7048
  */
6947
7049
  async listMatchqueue(appId, queueKey) {
6948
- return this.http.get(
6949
- `/v1/game/${appId}/matchqueue/${queueKey}`
7050
+ return this.gameFetch(
7051
+ "GET",
7052
+ `/v1/game/${appId}/matchqueue/${queueKey}`,
7053
+ void 0,
7054
+ "GAME_LIST_MATCHQUEUE_FAILED"
6950
7055
  );
6951
7056
  }
6952
7057
  /**
6953
7058
  * 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
6954
7059
  */
6955
7060
  async cancelMatch(appId, queueKey, ticketId) {
6956
- await this.http.delete(
6957
- `/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`
7061
+ await this.gameFetch(
7062
+ "DELETE",
7063
+ `/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`,
7064
+ void 0,
7065
+ "GAME_CANCEL_MATCH_FAILED"
6958
7066
  );
6959
7067
  }
6960
7068
  // ─────────────────────────────────────────────────────────────────────
@@ -6970,47 +7078,66 @@ var GameAPI = class {
6970
7078
  * await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
6971
7079
  */
6972
7080
  async submitScore(appId, leaderboardKey, member, score, mode = "set") {
6973
- return this.http.post(
7081
+ return this.gameFetch(
7082
+ "POST",
6974
7083
  `/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
6975
- { member, score, mode }
7084
+ { member, score, mode },
7085
+ "GAME_SUBMIT_SCORE_FAILED"
6976
7086
  );
6977
7087
  }
6978
7088
  /**
6979
7089
  * 상위 n 명 조회 (기본 10).
6980
7090
  */
6981
7091
  async getTopScores(appId, leaderboardKey, n = 10) {
6982
- return this.http.get(
6983
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`
7092
+ return this.gameFetch(
7093
+ "GET",
7094
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`,
7095
+ void 0,
7096
+ "GAME_GET_TOP_SCORES_FAILED"
6984
7097
  );
6985
7098
  }
6986
7099
  /**
6987
7100
  * 단일 member 의 rank + score.
6988
7101
  */
6989
7102
  async getMemberRank(appId, leaderboardKey, member) {
6990
- return this.http.get(
6991
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
7103
+ return this.gameFetch(
7104
+ "GET",
7105
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
7106
+ void 0,
7107
+ "GAME_GET_MEMBER_RANK_FAILED"
6992
7108
  );
6993
7109
  }
6994
7110
  /**
6995
7111
  * member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
6996
7112
  */
6997
7113
  async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
6998
- return this.http.get(
6999
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`
7114
+ return this.gameFetch(
7115
+ "GET",
7116
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`,
7117
+ void 0,
7118
+ "GAME_GET_RANK_AROUND_FAILED"
7000
7119
  );
7001
7120
  }
7002
7121
  /**
7003
7122
  * leaderboard 전체 reset (시즌 종료 시).
7004
7123
  */
7005
7124
  async resetLeaderboard(appId, leaderboardKey) {
7006
- await this.http.delete(`/v1/game/${appId}/leaderboards/${leaderboardKey}`);
7125
+ await this.gameFetch(
7126
+ "DELETE",
7127
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}`,
7128
+ void 0,
7129
+ "GAME_RESET_LEADERBOARD_FAILED"
7130
+ );
7007
7131
  }
7008
7132
  /**
7009
7133
  * 특정 member 만 제거 (계정 삭제 등).
7010
7134
  */
7011
7135
  async removeFromLeaderboard(appId, leaderboardKey, member) {
7012
- await this.http.delete(
7013
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
7136
+ await this.gameFetch(
7137
+ "DELETE",
7138
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
7139
+ void 0,
7140
+ "GAME_REMOVE_FROM_LEADERBOARD_FAILED"
7014
7141
  );
7015
7142
  }
7016
7143
  // ─────────────────────────────────────────────────────────────────────
@@ -7025,54 +7152,100 @@ var GameAPI = class {
7025
7152
  * await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
7026
7153
  */
7027
7154
  async uploadScript(appId, name, code) {
7028
- return this.http.post(
7155
+ return this.gameFetch(
7156
+ "POST",
7029
7157
  `/v1/game/${appId}/scripts`,
7030
- { name, code }
7158
+ { name, code },
7159
+ "GAME_UPLOAD_SCRIPT_FAILED"
7031
7160
  );
7032
7161
  }
7033
7162
  /**
7034
7163
  * app 의 모든 스크립트 메타데이터 목록.
7035
7164
  */
7036
7165
  async listScripts(appId) {
7037
- return this.http.get(`/v1/game/${appId}/scripts`);
7166
+ return this.gameFetch(
7167
+ "GET",
7168
+ `/v1/game/${appId}/scripts`,
7169
+ void 0,
7170
+ "GAME_LIST_SCRIPTS_FAILED"
7171
+ );
7038
7172
  }
7039
7173
  /**
7040
7174
  * 단일 스크립트 메타 + active version code.
7041
7175
  */
7042
7176
  async getScript(appId, name) {
7043
- return this.http.get(`/v1/game/${appId}/scripts/${name}`);
7177
+ return this.gameFetch(
7178
+ "GET",
7179
+ `/v1/game/${appId}/scripts/${name}`,
7180
+ void 0,
7181
+ "GAME_GET_SCRIPT_FAILED"
7182
+ );
7044
7183
  }
7045
7184
  /**
7046
7185
  * 단일 스크립트의 모든 버전 이력.
7047
7186
  */
7048
7187
  async listScriptVersions(appId, name) {
7049
- return this.http.get(
7050
- `/v1/game/${appId}/scripts/${name}/versions`
7188
+ return this.gameFetch(
7189
+ "GET",
7190
+ `/v1/game/${appId}/scripts/${name}/versions`,
7191
+ void 0,
7192
+ "GAME_LIST_SCRIPT_VERSIONS_FAILED"
7051
7193
  );
7052
7194
  }
7053
7195
  /**
7054
7196
  * 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
7055
7197
  */
7056
7198
  async activateScript(appId, name, version) {
7057
- return this.http.post(
7199
+ return this.gameFetch(
7200
+ "POST",
7058
7201
  `/v1/game/${appId}/scripts/${name}/activate`,
7059
- { version: version ?? 0 }
7202
+ { version: version ?? 0 },
7203
+ "GAME_ACTIVATE_SCRIPT_FAILED"
7060
7204
  );
7061
7205
  }
7062
7206
  /**
7063
7207
  * 직전 active 버전으로 롤백 (hot reload 자동).
7064
7208
  */
7065
7209
  async rollbackScript(appId, name) {
7066
- return this.http.post(
7210
+ return this.gameFetch(
7211
+ "POST",
7067
7212
  `/v1/game/${appId}/scripts/${name}/rollback`,
7068
- {}
7213
+ {},
7214
+ "GAME_ROLLBACK_SCRIPT_FAILED"
7069
7215
  );
7070
7216
  }
7071
7217
  /**
7072
- * 스크립트 비활성화 (status=disabled). 코드/버전은 보존.
7218
+ * 스크립트 비활성화 (status=disabled, ActiveVersion=0). 코드/모든 버전은 보존되며
7219
+ * 이후 {@link activateScript} 로 재활성화할 수 있다.
7220
+ *
7221
+ * v3.14.0 BREAKING — 이전엔 `disableScript` 가 `DELETE /scripts/:name` 을 호출해
7222
+ * Disable 매핑이었으나, server 측 `DELETE` 가 hard-delete 로 분리됨에 따라
7223
+ * `POST /scripts/:name/deactivate` 로 endpoint 이동. 메서드명도 의도 명확화 위해 rename.
7073
7224
  */
7074
- async disableScript(appId, name) {
7075
- await this.http.delete(`/v1/game/${appId}/scripts/${name}`);
7225
+ async deactivateScript(appId, name) {
7226
+ await this.gameFetch(
7227
+ "POST",
7228
+ `/v1/game/${appId}/scripts/${name}/deactivate`,
7229
+ {},
7230
+ "GAME_DEACTIVATE_SCRIPT_FAILED"
7231
+ );
7232
+ }
7233
+ /**
7234
+ * 스크립트 영구 삭제 (hard-delete) — 메타 + 모든 버전 영구 제거. 복구 불가.
7235
+ *
7236
+ * 잘못 업로드된 스크립트나 더 이상 사용하지 않는 슬롯을 정리할 때 사용. 단순 비활성화는
7237
+ * {@link deactivateScript} 를 사용한다 (코드 보존, 재활성화 가능).
7238
+ *
7239
+ * v3.14.0 신규 — server 측 `DELETE /scripts/:name` 의 의미가 Disable → hard-delete 로
7240
+ * 변경된 것에 대응.
7241
+ */
7242
+ async deleteScript(appId, name) {
7243
+ await this.gameFetch(
7244
+ "DELETE",
7245
+ `/v1/game/${appId}/scripts/${name}`,
7246
+ void 0,
7247
+ "GAME_DELETE_SCRIPT_FAILED"
7248
+ );
7076
7249
  }
7077
7250
  };
7078
7251
 
@@ -9378,7 +9551,7 @@ var WebTransportTransport = class {
9378
9551
  }
9379
9552
  send(data, reliable = true) {
9380
9553
  if (!this.transport) {
9381
- throw new Error("Not connected");
9554
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9382
9555
  }
9383
9556
  const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
9384
9557
  if (reliable) {
@@ -9469,7 +9642,7 @@ var WebSocketTransport = class {
9469
9642
  }
9470
9643
  send(data, _reliable) {
9471
9644
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
9472
- throw new Error("Not connected");
9645
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9473
9646
  }
9474
9647
  if (typeof data === "string") {
9475
9648
  this.ws.send(data);
@@ -9494,6 +9667,8 @@ var GameRoomTransport = class {
9494
9667
  this.actionSequence = 0;
9495
9668
  this._roomId = null;
9496
9669
  this._state = null;
9670
+ this._scriptName = null;
9671
+ this._scriptVersion = null;
9497
9672
  this._isConnected = false;
9498
9673
  this._connectionStatus = "disconnected";
9499
9674
  this._lastError = null;
@@ -9546,6 +9721,14 @@ var GameRoomTransport = class {
9546
9721
  get isConnected() {
9547
9722
  return this._isConnected;
9548
9723
  }
9724
+ /** Attached lua script 이름 — createRoom 이후 server 가 echo 한 값. */
9725
+ get scriptName() {
9726
+ return this._scriptName;
9727
+ }
9728
+ /** Attached lua script 의 active version. */
9729
+ get scriptVersion() {
9730
+ return this._scriptVersion;
9731
+ }
9549
9732
  /**
9550
9733
  * Connection state information
9551
9734
  */
@@ -9598,7 +9781,7 @@ var GameRoomTransport = class {
9598
9781
  const onError = (error) => {
9599
9782
  this._connectionStatus = "error";
9600
9783
  this._lastError = error;
9601
- this.handlers.onError?.({ code: "CONNECTION_ERROR", message: error.message });
9784
+ this.handlers.onError?.(new GameError({ code: "CONNECTION_ERROR", message: error.message }));
9602
9785
  };
9603
9786
  if (useWebTransport) {
9604
9787
  try {
@@ -9660,18 +9843,35 @@ var GameRoomTransport = class {
9660
9843
  this._state = null;
9661
9844
  }
9662
9845
  /**
9663
- * Create a new room
9846
+ * Create a new room (호환 시그니처) — 초기 상태만 반환.
9847
+ *
9848
+ * Attached script 메타가 필요하면 {@link createRoomDetailed} 또는 인스턴스 getter
9849
+ * (`transport.scriptName`, `transport.scriptVersion`) 를 사용.
9664
9850
  */
9665
9851
  async createRoom(config = {}) {
9852
+ const r = await this.createRoomDetailed(config);
9853
+ return r.state;
9854
+ }
9855
+ /**
9856
+ * Create a new room — server 가 attach 한 lua script 메타 함께 반환.
9857
+ */
9858
+ async createRoomDetailed(config = {}) {
9666
9859
  return new Promise((resolve, reject) => {
9667
9860
  const handler = (msg) => {
9668
9861
  if (msg.type === "room_created") {
9669
9862
  const data = msg.data;
9670
9863
  this._roomId = data.room_id;
9671
9864
  this._state = data.initial_state;
9672
- resolve(data.initial_state);
9865
+ this._scriptName = data.script_name ?? null;
9866
+ this._scriptVersion = data.script_version ?? null;
9867
+ resolve({
9868
+ roomId: data.room_id,
9869
+ state: data.initial_state,
9870
+ scriptName: data.script_name,
9871
+ scriptVersion: data.script_version
9872
+ });
9673
9873
  } else if (msg.type === "error") {
9674
- reject(new Error(msg.data.message));
9874
+ reject(parseGameError(msg));
9675
9875
  }
9676
9876
  };
9677
9877
  this.sendWithHandler("create_room", toCreateRoomWire(config), handler);
@@ -9687,9 +9887,11 @@ var GameRoomTransport = class {
9687
9887
  const data = msg.data;
9688
9888
  this._roomId = data.room_id;
9689
9889
  this._state = data.initial_state;
9890
+ this._scriptName = null;
9891
+ this._scriptVersion = null;
9690
9892
  resolve(data.initial_state);
9691
9893
  } else if (msg.type === "error") {
9692
- reject(new Error(msg.data.message));
9894
+ reject(parseGameError(msg));
9693
9895
  }
9694
9896
  };
9695
9897
  this.sendWithHandler("join_room", { room_id: roomId, metadata }, handler);
@@ -9701,16 +9903,18 @@ var GameRoomTransport = class {
9701
9903
  async leaveRoom() {
9702
9904
  return new Promise((resolve, reject) => {
9703
9905
  if (!this._roomId) {
9704
- reject(new Error("Not in a room"));
9906
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
9705
9907
  return;
9706
9908
  }
9707
9909
  const handler = (msg) => {
9708
9910
  if (msg.type === "room_left") {
9709
9911
  this._roomId = null;
9710
9912
  this._state = null;
9913
+ this._scriptName = null;
9914
+ this._scriptVersion = null;
9711
9915
  resolve();
9712
9916
  } else if (msg.type === "error") {
9713
- reject(new Error(msg.data.message));
9917
+ reject(parseGameError(msg));
9714
9918
  }
9715
9919
  };
9716
9920
  this.sendWithHandler("leave_room", {}, handler);
@@ -9722,7 +9926,7 @@ var GameRoomTransport = class {
9722
9926
  */
9723
9927
  sendAction(action, reliable = false) {
9724
9928
  if (!this._roomId) {
9725
- throw new Error("Not in a room");
9929
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
9726
9930
  }
9727
9931
  const message = JSON.stringify({
9728
9932
  type: "action",
@@ -9741,7 +9945,7 @@ var GameRoomTransport = class {
9741
9945
  */
9742
9946
  sendChat(message) {
9743
9947
  if (!this._roomId) {
9744
- throw new Error("Not in a room");
9948
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
9745
9949
  }
9746
9950
  this.send("chat", { message });
9747
9951
  }
@@ -9751,7 +9955,7 @@ var GameRoomTransport = class {
9751
9955
  async requestState() {
9752
9956
  return new Promise((resolve, reject) => {
9753
9957
  if (!this._roomId) {
9754
- reject(new Error("Not in a room"));
9958
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
9755
9959
  return;
9756
9960
  }
9757
9961
  const handler = (msg) => {
@@ -9760,7 +9964,7 @@ var GameRoomTransport = class {
9760
9964
  this._state = state;
9761
9965
  resolve(state);
9762
9966
  } else if (msg.type === "error") {
9763
- reject(new Error(msg.data.message));
9967
+ reject(parseGameError(msg));
9764
9968
  }
9765
9969
  };
9766
9970
  this.sendWithHandler("get_state", {}, handler);
@@ -9780,7 +9984,7 @@ var GameRoomTransport = class {
9780
9984
  this.handlers.onPong?.(pong);
9781
9985
  resolve(rtt);
9782
9986
  } else if (msg.type === "error") {
9783
- reject(new Error(msg.data.message));
9987
+ reject(parseGameError(msg));
9784
9988
  }
9785
9989
  };
9786
9990
  this.sendWithHandler("ping", { timestamp }, handler);
@@ -9789,7 +9993,7 @@ var GameRoomTransport = class {
9789
9993
  // Private methods
9790
9994
  send(type, data) {
9791
9995
  if (!this.transport?.isConnected()) {
9792
- throw new Error("Not connected");
9996
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9793
9997
  }
9794
9998
  const message = JSON.stringify({ type, data });
9795
9999
  this.transport.send(message, true);
@@ -9832,14 +10036,7 @@ var GameRoomTransport = class {
9832
10036
  });
9833
10037
  break;
9834
10038
  case "error":
9835
- this.handlers.onError?.({
9836
- code: msg.code || "UNKNOWN",
9837
- message: msg.message || "Unknown error",
9838
- phase: msg.phase,
9839
- feature: msg.feature,
9840
- roomId: msg.room_id,
9841
- scriptId: msg.script_id
9842
- });
10039
+ this.handlers.onError?.(parseGameError(msg));
9843
10040
  break;
9844
10041
  case "room_stale":
9845
10042
  if (this.handlers.onRoomStale) {
@@ -10049,6 +10246,7 @@ export {
10049
10246
  EndpointAPI,
10050
10247
  GameAPI,
10051
10248
  GameConfigAPI,
10249
+ GameError,
10052
10250
  GameRoom,
10053
10251
  GameRoomTransport,
10054
10252
  NativeAPI,