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.js CHANGED
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  EndpointAPI: () => EndpointAPI,
30
30
  GameAPI: () => GameAPI,
31
31
  GameConfigAPI: () => GameConfigAPI,
32
+ GameError: () => GameError,
32
33
  GameRoom: () => GameRoom,
33
34
  GameRoomTransport: () => GameRoomTransport,
34
35
  NativeAPI: () => NativeAPI,
@@ -56,6 +57,20 @@ var AuthError = class extends Error {
56
57
  this.name = "AuthError";
57
58
  }
58
59
  };
60
+ var GameError = class extends Error {
61
+ constructor(init) {
62
+ super(init.message || init.code || "GameError");
63
+ this.name = "GameError";
64
+ this.code = init.code || "UNKNOWN";
65
+ this.phase = init.phase;
66
+ this.feature = init.feature;
67
+ this.roomId = init.roomId;
68
+ this.scriptId = init.scriptId;
69
+ this.originClientId = init.originClientId;
70
+ this.requested = init.requested;
71
+ this.available = init.available;
72
+ }
73
+ };
59
74
 
60
75
  // src/core/abort.ts
61
76
  var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
@@ -6277,6 +6292,21 @@ var GameConfigAPI = class {
6277
6292
  };
6278
6293
 
6279
6294
  // src/api/game.ts
6295
+ function parseGameError(msg) {
6296
+ const payload = msg.data ?? msg;
6297
+ const get = (k) => payload[k];
6298
+ return new GameError({
6299
+ code: get("code") || msg.code,
6300
+ message: get("message") || "Unknown error",
6301
+ phase: get("phase") || msg.phase,
6302
+ feature: get("feature") || msg.feature,
6303
+ roomId: get("room_id") || msg.room_id,
6304
+ scriptId: get("script_id") || msg.script_id,
6305
+ originClientId: get("origin_client_id") || msg.origin_client_id,
6306
+ requested: get("requested") || msg.requested,
6307
+ available: get("available") || msg.available
6308
+ });
6309
+ }
6280
6310
  var getDefaultGameServerUrl = () => {
6281
6311
  if (typeof window !== "undefined") {
6282
6312
  const hostname = window.location.hostname;
@@ -6306,6 +6336,8 @@ var GameRoom = class {
6306
6336
  this.actionSequence = 0;
6307
6337
  this._roomId = null;
6308
6338
  this._state = null;
6339
+ this._scriptName = null;
6340
+ this._scriptVersion = null;
6309
6341
  this._isConnected = false;
6310
6342
  this.msgIdCounter = 0;
6311
6343
  this.config = {
@@ -6395,21 +6427,48 @@ var GameRoom = class {
6395
6427
  }
6396
6428
  this._isConnected = false;
6397
6429
  this._roomId = null;
6430
+ this._scriptName = null;
6431
+ this._scriptVersion = null;
6398
6432
  }
6399
6433
  /**
6400
- * 룸 생성
6434
+ * 룸 생성 (호환 시그니처) — 초기 상태만 반환.
6435
+ *
6436
+ * Attached script 의 검증(scriptName/scriptVersion echo 검사) 이 필요하면
6437
+ * 인스턴스 getter (`room.scriptName`, `room.scriptVersion`) 를 함께 사용하거나,
6438
+ * 메타까지 한 번에 받고 싶다면 {@link createRoomDetailed} 를 사용한다.
6439
+ *
6440
+ * 미존재/비활성 scriptName 은 서버가 `SCRIPT_NOT_FOUND` 로 즉시 reject 한다.
6441
+ * reject 값은 `GameError` 인스턴스이며 `.code`/`.requested`/`.available` 로 분기 가능.
6401
6442
  */
6402
6443
  createRoom(config = {}) {
6444
+ return this.createRoomDetailed(config).then((r) => r.state);
6445
+ }
6446
+ /**
6447
+ * 룸 생성 + 메타 — server 가 attach 한 lua script 이름/버전을 함께 반환.
6448
+ *
6449
+ * @example
6450
+ * const { state, scriptName, scriptVersion } = await room.createRoomDetailed({ scriptName: 'njb-main' })
6451
+ * if (scriptName !== 'njb-main') throw new Error('script mismatch')
6452
+ * console.log('attached', scriptName, 'v', scriptVersion)
6453
+ */
6454
+ createRoomDetailed(config = {}) {
6403
6455
  return new Promise((resolve, reject) => {
6404
6456
  const handler = (msg) => {
6405
6457
  if (msg.type === "room_created") {
6406
6458
  const data = msg.data;
6407
6459
  this._roomId = data.room_id;
6408
6460
  this._state = data.initial_state;
6409
- resolve(data.initial_state);
6461
+ this._scriptName = data.script_name ?? null;
6462
+ this._scriptVersion = data.script_version ?? null;
6463
+ resolve({
6464
+ roomId: data.room_id,
6465
+ state: data.initial_state,
6466
+ scriptName: data.script_name,
6467
+ scriptVersion: data.script_version
6468
+ });
6410
6469
  return true;
6411
6470
  } else if (msg.type === "error") {
6412
- reject(new Error(msg.data.message));
6471
+ reject(parseGameError(msg));
6413
6472
  return true;
6414
6473
  }
6415
6474
  return false;
@@ -6417,6 +6476,14 @@ var GameRoom = class {
6417
6476
  this.sendWithHandler("create_room", toCreateRoomWire(config), handler, 15e3, reject);
6418
6477
  });
6419
6478
  }
6479
+ /** Attached lua script 이름 — createRoom 이후 server 가 echo 한 값. */
6480
+ get scriptName() {
6481
+ return this._scriptName;
6482
+ }
6483
+ /** Attached lua script 의 active version. */
6484
+ get scriptVersion() {
6485
+ return this._scriptVersion;
6486
+ }
6420
6487
  /**
6421
6488
  * 룸 참가
6422
6489
  */
@@ -6427,10 +6494,12 @@ var GameRoom = class {
6427
6494
  const data = msg.data;
6428
6495
  this._roomId = data.room_id;
6429
6496
  this._state = data.initial_state;
6497
+ this._scriptName = null;
6498
+ this._scriptVersion = null;
6430
6499
  resolve(data.initial_state);
6431
6500
  return true;
6432
6501
  } else if (msg.type === "error") {
6433
- reject(new Error(msg.data.message));
6502
+ reject(parseGameError(msg));
6434
6503
  return true;
6435
6504
  }
6436
6505
  return false;
@@ -6444,17 +6513,19 @@ var GameRoom = class {
6444
6513
  leaveRoom() {
6445
6514
  return new Promise((resolve, reject) => {
6446
6515
  if (!this._roomId) {
6447
- reject(new Error("Not in a room"));
6516
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
6448
6517
  return;
6449
6518
  }
6450
6519
  const handler = (msg) => {
6451
6520
  if (msg.type === "room_left") {
6452
6521
  this._roomId = null;
6453
6522
  this._state = null;
6523
+ this._scriptName = null;
6524
+ this._scriptVersion = null;
6454
6525
  resolve();
6455
6526
  return true;
6456
6527
  } else if (msg.type === "error") {
6457
- reject(new Error(msg.data.message));
6528
+ reject(parseGameError(msg));
6458
6529
  return true;
6459
6530
  }
6460
6531
  return false;
@@ -6467,7 +6538,7 @@ var GameRoom = class {
6467
6538
  */
6468
6539
  sendAction(action) {
6469
6540
  if (!this._roomId) {
6470
- throw new Error("Not in a room");
6541
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
6471
6542
  }
6472
6543
  this.send("action", {
6473
6544
  type: action.type,
@@ -6481,7 +6552,7 @@ var GameRoom = class {
6481
6552
  */
6482
6553
  sendChat(message) {
6483
6554
  if (!this._roomId) {
6484
- throw new Error("Not in a room");
6555
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
6485
6556
  }
6486
6557
  this.send("chat", { message });
6487
6558
  }
@@ -6491,7 +6562,7 @@ var GameRoom = class {
6491
6562
  requestState() {
6492
6563
  return new Promise((resolve, reject) => {
6493
6564
  if (!this._roomId) {
6494
- reject(new Error("Not in a room"));
6565
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
6495
6566
  return;
6496
6567
  }
6497
6568
  const handler = (msg) => {
@@ -6501,7 +6572,7 @@ var GameRoom = class {
6501
6572
  resolve(state);
6502
6573
  return true;
6503
6574
  } else if (msg.type === "error") {
6504
- reject(new Error(msg.data.message));
6575
+ reject(parseGameError(msg));
6505
6576
  return true;
6506
6577
  }
6507
6578
  return false;
@@ -6520,7 +6591,7 @@ var GameRoom = class {
6520
6591
  resolve(data.rooms);
6521
6592
  return true;
6522
6593
  } else if (msg.type === "error") {
6523
- reject(new Error(msg.data.message));
6594
+ reject(parseGameError(msg));
6524
6595
  return true;
6525
6596
  }
6526
6597
  return false;
@@ -6542,7 +6613,7 @@ var GameRoom = class {
6542
6613
  resolve(rtt);
6543
6614
  return true;
6544
6615
  } else if (msg.type === "error") {
6545
- reject(new Error(msg.data.message));
6616
+ reject(parseGameError(msg));
6546
6617
  return true;
6547
6618
  }
6548
6619
  return false;
@@ -6600,12 +6671,12 @@ var GameRoom = class {
6600
6671
  this.ws?.addEventListener("message", messageHandler);
6601
6672
  timeoutId = setTimeout(() => {
6602
6673
  cleanup();
6603
- const err = new Error(`Request '${type}' timed out after ${timeoutMs}ms`);
6604
- onError?.(err);
6605
- this.handlers.onError?.({
6674
+ const err = new GameError({
6606
6675
  code: "TIMEOUT",
6607
- message: err.message
6676
+ message: `Request '${type}' timed out after ${timeoutMs}ms`
6608
6677
  });
6678
+ onError?.(err);
6679
+ this.handlers.onError?.(err);
6609
6680
  }, timeoutMs);
6610
6681
  try {
6611
6682
  this.send(type, data, msgId);
@@ -6639,10 +6710,7 @@ var GameRoom = class {
6639
6710
  });
6640
6711
  break;
6641
6712
  case "error":
6642
- this.handlers.onError?.({
6643
- code: msg.code || "UNKNOWN",
6644
- message: msg.message || "Unknown error"
6645
- });
6713
+ this.handlers.onError?.(parseGameError(msg));
6646
6714
  break;
6647
6715
  default:
6648
6716
  break;
@@ -6720,10 +6788,10 @@ var GameRoom = class {
6720
6788
  scheduleReconnect(roomId) {
6721
6789
  if (this.reconnectAttempts >= (this.config.maxReconnectAttempts ?? 5)) {
6722
6790
  console.error("Max reconnect attempts reached");
6723
- this.handlers.onError?.({
6791
+ this.handlers.onError?.(new GameError({
6724
6792
  code: "MAX_RECONNECT_ATTEMPTS",
6725
6793
  message: "Maximum reconnection attempts reached"
6726
- });
6794
+ }));
6727
6795
  return;
6728
6796
  }
6729
6797
  const delay = Math.min(
@@ -6744,10 +6812,11 @@ var GameRoom = class {
6744
6812
  console.log(`Successfully rejoined room ${previousRoomId}`);
6745
6813
  } catch (rejoinError) {
6746
6814
  console.error(`Failed to rejoin room ${previousRoomId}:`, rejoinError);
6747
- this.handlers.onError?.({
6815
+ this.handlers.onError?.(new GameError({
6748
6816
  code: "REJOIN_FAILED",
6749
- message: `Failed to rejoin room: ${rejoinError}`
6750
- });
6817
+ message: `Failed to rejoin room: ${rejoinError}`,
6818
+ roomId: previousRoomId
6819
+ }));
6751
6820
  }
6752
6821
  }
6753
6822
  } catch {
@@ -6956,6 +7025,38 @@ var GameAPI = class {
6956
7025
  }
6957
7026
  return headers;
6958
7027
  }
7028
+ // game-server (`game.connectbase.world`) 전용 fetch.
7029
+ //
7030
+ // 회귀 가드 (v3.14.1, NJB report 2026-05-14): matchqueue / leaderboard / scripts 16개
7031
+ // 메서드가 v3.0.0 BREAKING 이후 `this.http.*` 를 통해 기본 baseUrl (=core-server,
7032
+ // `api.connectbase.world`) 로 라우팅되면서 404 회귀. 호스트 분리된 game-server 경로는
7033
+ // 반드시 `this.gameServerUrl` 을 사용해야 한다. 회귀 재발 방지 테스트:
7034
+ // test/game-host-routing.test.ts.
7035
+ async gameFetch(method, path, body, errCode = "GAME_REQUEST_FAILED") {
7036
+ const init = { method, headers: { ...this.getHeaders() } };
7037
+ if (body !== void 0) {
7038
+ init.headers["Content-Type"] = "application/json";
7039
+ init.body = JSON.stringify(body);
7040
+ }
7041
+ const response = await fetch(`${this.gameServerUrl}${path}`, init);
7042
+ if (!response.ok) {
7043
+ let serverCode = errCode;
7044
+ let message = `${method} ${path} failed: ${response.statusText}`;
7045
+ try {
7046
+ const data = await response.json();
7047
+ if (data && typeof data.error === "string") {
7048
+ message = data.error;
7049
+ serverCode = data.error;
7050
+ }
7051
+ } catch {
7052
+ }
7053
+ throw new ApiError(response.status, message, serverCode);
7054
+ }
7055
+ if (response.status === 204) return void 0;
7056
+ const contentType = response.headers.get("content-type") || "";
7057
+ if (!contentType.includes("application/json")) return void 0;
7058
+ return response.json();
7059
+ }
6959
7060
  // ─────────────────────────────────────────────────────────────────────
6960
7061
  // Phase 3 — Matchqueue primitive (mechanism only)
6961
7062
  //
@@ -6976,9 +7077,11 @@ var GameAPI = class {
6976
7077
  * @constraints appId/queueKey/ticketId 는 [a-zA-Z0-9_-] 만 허용. ttlSec=0 이면 만료 없음.
6977
7078
  */
6978
7079
  async enqueueMatch(appId, queueKey, ticketId, attributes, ttlSec) {
6979
- return this.http.post(
7080
+ return this.gameFetch(
7081
+ "POST",
6980
7082
  `/v1/game/${appId}/matchqueue/${queueKey}/tickets`,
6981
- { ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 }
7083
+ { ticket_id: ticketId, attributes, ttl_sec: ttlSec ?? 0 },
7084
+ "GAME_ENQUEUE_MATCH_FAILED"
6982
7085
  );
6983
7086
  }
6984
7087
  /**
@@ -6987,16 +7090,22 @@ var GameAPI = class {
6987
7090
  * @example const tickets = await cb.game.listMatchqueue(appId, "ranked")
6988
7091
  */
6989
7092
  async listMatchqueue(appId, queueKey) {
6990
- return this.http.get(
6991
- `/v1/game/${appId}/matchqueue/${queueKey}`
7093
+ return this.gameFetch(
7094
+ "GET",
7095
+ `/v1/game/${appId}/matchqueue/${queueKey}`,
7096
+ void 0,
7097
+ "GAME_LIST_MATCHQUEUE_FAILED"
6992
7098
  );
6993
7099
  }
6994
7100
  /**
6995
7101
  * 매칭 큐에서 ticket 제거 (예: 사용자가 매칭 취소).
6996
7102
  */
6997
7103
  async cancelMatch(appId, queueKey, ticketId) {
6998
- await this.http.delete(
6999
- `/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`
7104
+ await this.gameFetch(
7105
+ "DELETE",
7106
+ `/v1/game/${appId}/matchqueue/${queueKey}/tickets/${ticketId}`,
7107
+ void 0,
7108
+ "GAME_CANCEL_MATCH_FAILED"
7000
7109
  );
7001
7110
  }
7002
7111
  // ─────────────────────────────────────────────────────────────────────
@@ -7012,47 +7121,66 @@ var GameAPI = class {
7012
7121
  * await cb.game.submitScore(appId, "global_elo", userId, 32, "incr")
7013
7122
  */
7014
7123
  async submitScore(appId, leaderboardKey, member, score, mode = "set") {
7015
- return this.http.post(
7124
+ return this.gameFetch(
7125
+ "POST",
7016
7126
  `/v1/game/${appId}/leaderboards/${leaderboardKey}/scores`,
7017
- { member, score, mode }
7127
+ { member, score, mode },
7128
+ "GAME_SUBMIT_SCORE_FAILED"
7018
7129
  );
7019
7130
  }
7020
7131
  /**
7021
7132
  * 상위 n 명 조회 (기본 10).
7022
7133
  */
7023
7134
  async getTopScores(appId, leaderboardKey, n = 10) {
7024
- return this.http.get(
7025
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`
7135
+ return this.gameFetch(
7136
+ "GET",
7137
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/top?n=${n}`,
7138
+ void 0,
7139
+ "GAME_GET_TOP_SCORES_FAILED"
7026
7140
  );
7027
7141
  }
7028
7142
  /**
7029
7143
  * 단일 member 의 rank + score.
7030
7144
  */
7031
7145
  async getMemberRank(appId, leaderboardKey, member) {
7032
- return this.http.get(
7033
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
7146
+ return this.gameFetch(
7147
+ "GET",
7148
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
7149
+ void 0,
7150
+ "GAME_GET_MEMBER_RANK_FAILED"
7034
7151
  );
7035
7152
  }
7036
7153
  /**
7037
7154
  * member 주변 (위 above 명 + 본인 + 아래 below 명) 조회.
7038
7155
  */
7039
7156
  async getRankAround(appId, leaderboardKey, member, above = 5, below = 5) {
7040
- return this.http.get(
7041
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`
7157
+ return this.gameFetch(
7158
+ "GET",
7159
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/around/${member}?above=${above}&below=${below}`,
7160
+ void 0,
7161
+ "GAME_GET_RANK_AROUND_FAILED"
7042
7162
  );
7043
7163
  }
7044
7164
  /**
7045
7165
  * leaderboard 전체 reset (시즌 종료 시).
7046
7166
  */
7047
7167
  async resetLeaderboard(appId, leaderboardKey) {
7048
- await this.http.delete(`/v1/game/${appId}/leaderboards/${leaderboardKey}`);
7168
+ await this.gameFetch(
7169
+ "DELETE",
7170
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}`,
7171
+ void 0,
7172
+ "GAME_RESET_LEADERBOARD_FAILED"
7173
+ );
7049
7174
  }
7050
7175
  /**
7051
7176
  * 특정 member 만 제거 (계정 삭제 등).
7052
7177
  */
7053
7178
  async removeFromLeaderboard(appId, leaderboardKey, member) {
7054
- await this.http.delete(
7055
- `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`
7179
+ await this.gameFetch(
7180
+ "DELETE",
7181
+ `/v1/game/${appId}/leaderboards/${leaderboardKey}/members/${member}`,
7182
+ void 0,
7183
+ "GAME_REMOVE_FROM_LEADERBOARD_FAILED"
7056
7184
  );
7057
7185
  }
7058
7186
  // ─────────────────────────────────────────────────────────────────────
@@ -7067,54 +7195,100 @@ var GameAPI = class {
7067
7195
  * await cb.game.uploadScript(appId, "ranked_br", fs.readFileSync("./ranked.lua", "utf-8"))
7068
7196
  */
7069
7197
  async uploadScript(appId, name, code) {
7070
- return this.http.post(
7198
+ return this.gameFetch(
7199
+ "POST",
7071
7200
  `/v1/game/${appId}/scripts`,
7072
- { name, code }
7201
+ { name, code },
7202
+ "GAME_UPLOAD_SCRIPT_FAILED"
7073
7203
  );
7074
7204
  }
7075
7205
  /**
7076
7206
  * app 의 모든 스크립트 메타데이터 목록.
7077
7207
  */
7078
7208
  async listScripts(appId) {
7079
- return this.http.get(`/v1/game/${appId}/scripts`);
7209
+ return this.gameFetch(
7210
+ "GET",
7211
+ `/v1/game/${appId}/scripts`,
7212
+ void 0,
7213
+ "GAME_LIST_SCRIPTS_FAILED"
7214
+ );
7080
7215
  }
7081
7216
  /**
7082
7217
  * 단일 스크립트 메타 + active version code.
7083
7218
  */
7084
7219
  async getScript(appId, name) {
7085
- return this.http.get(`/v1/game/${appId}/scripts/${name}`);
7220
+ return this.gameFetch(
7221
+ "GET",
7222
+ `/v1/game/${appId}/scripts/${name}`,
7223
+ void 0,
7224
+ "GAME_GET_SCRIPT_FAILED"
7225
+ );
7086
7226
  }
7087
7227
  /**
7088
7228
  * 단일 스크립트의 모든 버전 이력.
7089
7229
  */
7090
7230
  async listScriptVersions(appId, name) {
7091
- return this.http.get(
7092
- `/v1/game/${appId}/scripts/${name}/versions`
7231
+ return this.gameFetch(
7232
+ "GET",
7233
+ `/v1/game/${appId}/scripts/${name}/versions`,
7234
+ void 0,
7235
+ "GAME_LIST_SCRIPT_VERSIONS_FAILED"
7093
7236
  );
7094
7237
  }
7095
7238
  /**
7096
7239
  * 특정 버전 활성화 (hot reload 자동). version=0 또는 미지정 → latest 활성화.
7097
7240
  */
7098
7241
  async activateScript(appId, name, version) {
7099
- return this.http.post(
7242
+ return this.gameFetch(
7243
+ "POST",
7100
7244
  `/v1/game/${appId}/scripts/${name}/activate`,
7101
- { version: version ?? 0 }
7245
+ { version: version ?? 0 },
7246
+ "GAME_ACTIVATE_SCRIPT_FAILED"
7102
7247
  );
7103
7248
  }
7104
7249
  /**
7105
7250
  * 직전 active 버전으로 롤백 (hot reload 자동).
7106
7251
  */
7107
7252
  async rollbackScript(appId, name) {
7108
- return this.http.post(
7253
+ return this.gameFetch(
7254
+ "POST",
7109
7255
  `/v1/game/${appId}/scripts/${name}/rollback`,
7110
- {}
7256
+ {},
7257
+ "GAME_ROLLBACK_SCRIPT_FAILED"
7111
7258
  );
7112
7259
  }
7113
7260
  /**
7114
- * 스크립트 비활성화 (status=disabled). 코드/버전은 보존.
7261
+ * 스크립트 비활성화 (status=disabled, ActiveVersion=0). 코드/모든 버전은 보존되며
7262
+ * 이후 {@link activateScript} 로 재활성화할 수 있다.
7263
+ *
7264
+ * v3.14.0 BREAKING — 이전엔 `disableScript` 가 `DELETE /scripts/:name` 을 호출해
7265
+ * Disable 매핑이었으나, server 측 `DELETE` 가 hard-delete 로 분리됨에 따라
7266
+ * `POST /scripts/:name/deactivate` 로 endpoint 이동. 메서드명도 의도 명확화 위해 rename.
7115
7267
  */
7116
- async disableScript(appId, name) {
7117
- await this.http.delete(`/v1/game/${appId}/scripts/${name}`);
7268
+ async deactivateScript(appId, name) {
7269
+ await this.gameFetch(
7270
+ "POST",
7271
+ `/v1/game/${appId}/scripts/${name}/deactivate`,
7272
+ {},
7273
+ "GAME_DEACTIVATE_SCRIPT_FAILED"
7274
+ );
7275
+ }
7276
+ /**
7277
+ * 스크립트 영구 삭제 (hard-delete) — 메타 + 모든 버전 영구 제거. 복구 불가.
7278
+ *
7279
+ * 잘못 업로드된 스크립트나 더 이상 사용하지 않는 슬롯을 정리할 때 사용. 단순 비활성화는
7280
+ * {@link deactivateScript} 를 사용한다 (코드 보존, 재활성화 가능).
7281
+ *
7282
+ * v3.14.0 신규 — server 측 `DELETE /scripts/:name` 의 의미가 Disable → hard-delete 로
7283
+ * 변경된 것에 대응.
7284
+ */
7285
+ async deleteScript(appId, name) {
7286
+ await this.gameFetch(
7287
+ "DELETE",
7288
+ `/v1/game/${appId}/scripts/${name}`,
7289
+ void 0,
7290
+ "GAME_DELETE_SCRIPT_FAILED"
7291
+ );
7118
7292
  }
7119
7293
  };
7120
7294
 
@@ -9420,7 +9594,7 @@ var WebTransportTransport = class {
9420
9594
  }
9421
9595
  send(data, reliable = true) {
9422
9596
  if (!this.transport) {
9423
- throw new Error("Not connected");
9597
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9424
9598
  }
9425
9599
  const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
9426
9600
  if (reliable) {
@@ -9511,7 +9685,7 @@ var WebSocketTransport = class {
9511
9685
  }
9512
9686
  send(data, _reliable) {
9513
9687
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
9514
- throw new Error("Not connected");
9688
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9515
9689
  }
9516
9690
  if (typeof data === "string") {
9517
9691
  this.ws.send(data);
@@ -9536,6 +9710,8 @@ var GameRoomTransport = class {
9536
9710
  this.actionSequence = 0;
9537
9711
  this._roomId = null;
9538
9712
  this._state = null;
9713
+ this._scriptName = null;
9714
+ this._scriptVersion = null;
9539
9715
  this._isConnected = false;
9540
9716
  this._connectionStatus = "disconnected";
9541
9717
  this._lastError = null;
@@ -9588,6 +9764,14 @@ var GameRoomTransport = class {
9588
9764
  get isConnected() {
9589
9765
  return this._isConnected;
9590
9766
  }
9767
+ /** Attached lua script 이름 — createRoom 이후 server 가 echo 한 값. */
9768
+ get scriptName() {
9769
+ return this._scriptName;
9770
+ }
9771
+ /** Attached lua script 의 active version. */
9772
+ get scriptVersion() {
9773
+ return this._scriptVersion;
9774
+ }
9591
9775
  /**
9592
9776
  * Connection state information
9593
9777
  */
@@ -9640,7 +9824,7 @@ var GameRoomTransport = class {
9640
9824
  const onError = (error) => {
9641
9825
  this._connectionStatus = "error";
9642
9826
  this._lastError = error;
9643
- this.handlers.onError?.({ code: "CONNECTION_ERROR", message: error.message });
9827
+ this.handlers.onError?.(new GameError({ code: "CONNECTION_ERROR", message: error.message }));
9644
9828
  };
9645
9829
  if (useWebTransport) {
9646
9830
  try {
@@ -9702,18 +9886,35 @@ var GameRoomTransport = class {
9702
9886
  this._state = null;
9703
9887
  }
9704
9888
  /**
9705
- * Create a new room
9889
+ * Create a new room (호환 시그니처) — 초기 상태만 반환.
9890
+ *
9891
+ * Attached script 메타가 필요하면 {@link createRoomDetailed} 또는 인스턴스 getter
9892
+ * (`transport.scriptName`, `transport.scriptVersion`) 를 사용.
9706
9893
  */
9707
9894
  async createRoom(config = {}) {
9895
+ const r = await this.createRoomDetailed(config);
9896
+ return r.state;
9897
+ }
9898
+ /**
9899
+ * Create a new room — server 가 attach 한 lua script 메타 함께 반환.
9900
+ */
9901
+ async createRoomDetailed(config = {}) {
9708
9902
  return new Promise((resolve, reject) => {
9709
9903
  const handler = (msg) => {
9710
9904
  if (msg.type === "room_created") {
9711
9905
  const data = msg.data;
9712
9906
  this._roomId = data.room_id;
9713
9907
  this._state = data.initial_state;
9714
- resolve(data.initial_state);
9908
+ this._scriptName = data.script_name ?? null;
9909
+ this._scriptVersion = data.script_version ?? null;
9910
+ resolve({
9911
+ roomId: data.room_id,
9912
+ state: data.initial_state,
9913
+ scriptName: data.script_name,
9914
+ scriptVersion: data.script_version
9915
+ });
9715
9916
  } else if (msg.type === "error") {
9716
- reject(new Error(msg.data.message));
9917
+ reject(parseGameError(msg));
9717
9918
  }
9718
9919
  };
9719
9920
  this.sendWithHandler("create_room", toCreateRoomWire(config), handler);
@@ -9729,9 +9930,11 @@ var GameRoomTransport = class {
9729
9930
  const data = msg.data;
9730
9931
  this._roomId = data.room_id;
9731
9932
  this._state = data.initial_state;
9933
+ this._scriptName = null;
9934
+ this._scriptVersion = null;
9732
9935
  resolve(data.initial_state);
9733
9936
  } else if (msg.type === "error") {
9734
- reject(new Error(msg.data.message));
9937
+ reject(parseGameError(msg));
9735
9938
  }
9736
9939
  };
9737
9940
  this.sendWithHandler("join_room", { room_id: roomId, metadata }, handler);
@@ -9743,16 +9946,18 @@ var GameRoomTransport = class {
9743
9946
  async leaveRoom() {
9744
9947
  return new Promise((resolve, reject) => {
9745
9948
  if (!this._roomId) {
9746
- reject(new Error("Not in a room"));
9949
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
9747
9950
  return;
9748
9951
  }
9749
9952
  const handler = (msg) => {
9750
9953
  if (msg.type === "room_left") {
9751
9954
  this._roomId = null;
9752
9955
  this._state = null;
9956
+ this._scriptName = null;
9957
+ this._scriptVersion = null;
9753
9958
  resolve();
9754
9959
  } else if (msg.type === "error") {
9755
- reject(new Error(msg.data.message));
9960
+ reject(parseGameError(msg));
9756
9961
  }
9757
9962
  };
9758
9963
  this.sendWithHandler("leave_room", {}, handler);
@@ -9764,7 +9969,7 @@ var GameRoomTransport = class {
9764
9969
  */
9765
9970
  sendAction(action, reliable = false) {
9766
9971
  if (!this._roomId) {
9767
- throw new Error("Not in a room");
9972
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
9768
9973
  }
9769
9974
  const message = JSON.stringify({
9770
9975
  type: "action",
@@ -9783,7 +9988,7 @@ var GameRoomTransport = class {
9783
9988
  */
9784
9989
  sendChat(message) {
9785
9990
  if (!this._roomId) {
9786
- throw new Error("Not in a room");
9991
+ throw new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" });
9787
9992
  }
9788
9993
  this.send("chat", { message });
9789
9994
  }
@@ -9793,7 +9998,7 @@ var GameRoomTransport = class {
9793
9998
  async requestState() {
9794
9999
  return new Promise((resolve, reject) => {
9795
10000
  if (!this._roomId) {
9796
- reject(new Error("Not in a room"));
10001
+ reject(new GameError({ code: "NOT_IN_ROOM", message: "Not in a room" }));
9797
10002
  return;
9798
10003
  }
9799
10004
  const handler = (msg) => {
@@ -9802,7 +10007,7 @@ var GameRoomTransport = class {
9802
10007
  this._state = state;
9803
10008
  resolve(state);
9804
10009
  } else if (msg.type === "error") {
9805
- reject(new Error(msg.data.message));
10010
+ reject(parseGameError(msg));
9806
10011
  }
9807
10012
  };
9808
10013
  this.sendWithHandler("get_state", {}, handler);
@@ -9822,7 +10027,7 @@ var GameRoomTransport = class {
9822
10027
  this.handlers.onPong?.(pong);
9823
10028
  resolve(rtt);
9824
10029
  } else if (msg.type === "error") {
9825
- reject(new Error(msg.data.message));
10030
+ reject(parseGameError(msg));
9826
10031
  }
9827
10032
  };
9828
10033
  this.sendWithHandler("ping", { timestamp }, handler);
@@ -9831,7 +10036,7 @@ var GameRoomTransport = class {
9831
10036
  // Private methods
9832
10037
  send(type, data) {
9833
10038
  if (!this.transport?.isConnected()) {
9834
- throw new Error("Not connected");
10039
+ throw new GameError({ code: "NOT_CONNECTED", message: "Not connected" });
9835
10040
  }
9836
10041
  const message = JSON.stringify({ type, data });
9837
10042
  this.transport.send(message, true);
@@ -9874,14 +10079,7 @@ var GameRoomTransport = class {
9874
10079
  });
9875
10080
  break;
9876
10081
  case "error":
9877
- this.handlers.onError?.({
9878
- code: msg.code || "UNKNOWN",
9879
- message: msg.message || "Unknown error",
9880
- phase: msg.phase,
9881
- feature: msg.feature,
9882
- roomId: msg.room_id,
9883
- scriptId: msg.script_id
9884
- });
10082
+ this.handlers.onError?.(parseGameError(msg));
9885
10083
  break;
9886
10084
  case "room_stale":
9887
10085
  if (this.handlers.onRoomStale) {
@@ -10092,6 +10290,7 @@ var index_default = ConnectBase;
10092
10290
  EndpointAPI,
10093
10291
  GameAPI,
10094
10292
  GameConfigAPI,
10293
+ GameError,
10095
10294
  GameRoom,
10096
10295
  GameRoomTransport,
10097
10296
  NativeAPI,