@uzuhq/code-sdk 0.8.7 → 0.8.8

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 (2) hide show
  1. package/dist/index.js +68 -15
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1301,9 +1301,43 @@ function onInsetsChange(cb) {
1301
1301
  };
1302
1302
  }
1303
1303
  //#endregion
1304
+ //#region src/scenario-callback.ts
1305
+ /**
1306
+ * @docs
1307
+ * - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
1308
+ *
1309
+ * SDK からシナリオのコード (描画 / イベント購読 / bridge の購読) を呼ぶときの例外境界。
1310
+ * シナリオのコールバックを呼ぶ箇所は、すべてここを通す。
1311
+ *
1312
+ * これらは state 配信やメッセージ受信のたびに同じ経路を通る。例外をそのまま上へ通すと
1313
+ * 呼び出し元の state 適用 (confirmedState 更新 / ack 処理 / seq 前進) が途中で止まり、
1314
+ * 以降の配信でも同じ経路で投げ続ける。WebSocket もホストアプリも生きたまま画面だけが
1315
+ * 永久に更新されなくなる — 2026-09-09 の UZU TOKYO 公演で実際に起きた壊れ方がこれ。
1316
+ *
1317
+ * ここで断ち切れば state 適用は最後まで走り、次の配信で描画がやり直される
1318
+ * (onState は毎回フル state から再実行されるので冪等)。同じ理由で、購読が複数ある
1319
+ * ところでは 1 件の失敗を他の購読へ波及させない。
1320
+ */
1321
+ /**
1322
+ * シナリオ側のコールバックを実行し、例外を境界で止める。
1323
+ *
1324
+ * 例外は握り潰さず `console.error` に出す。ホスト側のログ収集がこれを拾う。
1325
+ */
1326
+ const callScenario = (label, fn) => {
1327
+ try {
1328
+ fn();
1329
+ } catch (err) {
1330
+ console.error(`[uzu] シナリオの ${label} で例外が発生しました (処理は継続します)`, err);
1331
+ }
1332
+ };
1333
+ //#endregion
1304
1334
  //#region src/run/optimistic-action-client.ts
1305
1335
  function createOptimisticActionClient(config) {
1306
- const { logic, playerId, onState, events, sendAction } = config;
1336
+ const { logic, playerId, events, sendAction } = config;
1337
+ /** 呼び出し箇所ごとに守らず、シナリオの描画へ出る唯一の口をここで塞ぐ。 */
1338
+ const onState = (state, id) => {
1339
+ callScenario("描画 (onState)", () => config.onState(state, id));
1340
+ };
1307
1341
  /** サーバー確定 state (楽観的更新のベース) */
1308
1342
  let confirmedState = null;
1309
1343
  /** 表示用 state (pending actions 適用済み) */
@@ -1336,7 +1370,11 @@ function createOptimisticActionClient(config) {
1336
1370
  /** 再適用時はイベントを発火しない (送信時に既に発火済み) */
1337
1371
  const noopEmit = () => {};
1338
1372
  const dispatchEvents = (evts) => {
1339
- for (const e of evts) events?.[e.name]?.handler(e.data);
1373
+ for (const e of evts) {
1374
+ const handler = events?.[e.name]?.handler;
1375
+ if (!handler) continue;
1376
+ callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1377
+ }
1340
1378
  };
1341
1379
  /** events の同一性キー。name と data が一致すれば「同じ出来事」とみなす。 */
1342
1380
  const eventKey = (e) => `${e.name}\u0000${JSON.stringify(e.data ?? {})}`;
@@ -1423,7 +1461,7 @@ function createOptimisticActionClient(config) {
1423
1461
  const subscription = events?.[eventName];
1424
1462
  if (!subscription?.predict) return;
1425
1463
  const payloadData = data ?? {};
1426
- subscription.handler(payloadData);
1464
+ callScenario(`event ハンドラ "${eventName}"`, () => subscription.handler(payloadData));
1427
1465
  firedPredictions.push({
1428
1466
  seq,
1429
1467
  name: eventName,
@@ -1448,7 +1486,9 @@ function createOptimisticActionClient(config) {
1448
1486
  time
1449
1487
  });
1450
1488
  onState(target, playerId);
1451
- } catch {}
1489
+ } catch (err) {
1490
+ console.warn(`[uzu] action "${type}" の先読み実行で例外`, err);
1491
+ }
1452
1492
  sendAction({
1453
1493
  action: type,
1454
1494
  payload: payload ?? {},
@@ -1552,13 +1592,7 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1552
1592
  config.inputs((type, payload) => {
1553
1593
  client.send(type, payload);
1554
1594
  });
1555
- ws.addEventListener("message", (ev) => {
1556
- let parsed;
1557
- try {
1558
- parsed = JSON.parse(ev.data);
1559
- } catch {
1560
- return;
1561
- }
1595
+ const handleMessage = (parsed) => {
1562
1596
  const msgType = parsed.type;
1563
1597
  console.log(`[SDK ServerAction] ⬅ recv type=${msgType}`);
1564
1598
  client.observeGameTime(parsed.gameTime);
@@ -1649,6 +1683,21 @@ function runOnlineServerAction(config, gameEndpoint, roomId, seatId, players, se
1649
1683
  client.reset(parsed.state);
1650
1684
  return;
1651
1685
  }
1686
+ };
1687
+ ws.addEventListener("message", (ev) => {
1688
+ let parsed;
1689
+ try {
1690
+ parsed = JSON.parse(ev.data);
1691
+ } catch {
1692
+ return;
1693
+ }
1694
+ if (parsed === null || typeof parsed !== "object") return;
1695
+ const msg = parsed;
1696
+ try {
1697
+ handleMessage(msg);
1698
+ } catch (err) {
1699
+ console.error(`[SDK ServerAction] ❌ message 処理で例外 type=${String(msg.type)}`, err);
1700
+ }
1652
1701
  });
1653
1702
  }
1654
1703
  //#endregion
@@ -1657,7 +1706,7 @@ function runLocalServerAction(config) {
1657
1706
  const { logic, inputs, events } = config;
1658
1707
  const onState = (next, id) => {
1659
1708
  observeGameTime(gameTime());
1660
- config.onState(next, id, "player");
1709
+ callScenario("描画 (onState)", () => config.onState(next, id, "player"));
1661
1710
  };
1662
1711
  const tickRate = logic.tickRate ?? 0;
1663
1712
  const clock = new GameClock();
@@ -1744,7 +1793,11 @@ function runLocalServerAction(config) {
1744
1793
  wakeupTimer = setTimeout(fireDue, Math.max(0, clock.toWall(next) - Date.now()));
1745
1794
  };
1746
1795
  const dispatchEvents = (evts) => {
1747
- for (const e of evts) events?.[e.name]?.handler(e.data);
1796
+ for (const e of evts) {
1797
+ const handler = events?.[e.name]?.handler;
1798
+ if (!handler) continue;
1799
+ callScenario(`event ハンドラ "${e.name}"`, () => handler(e.data));
1800
+ }
1748
1801
  };
1749
1802
  clock.restart();
1750
1803
  const setupTime = gameTime();
@@ -2118,7 +2171,7 @@ function handleMessage(msg) {
2118
2171
  return;
2119
2172
  case "playersChanged": {
2120
2173
  const players = payload?.players ?? {};
2121
- _playersChangedHandlers.forEach((fn) => fn(players));
2174
+ for (const fn of _playersChangedHandlers) callScenario("playersChanged ハンドラ", () => fn(players));
2122
2175
  return;
2123
2176
  }
2124
2177
  case "requestPause":
@@ -2138,7 +2191,7 @@ function handleMessage(msg) {
2138
2191
  if (channel === "game") {
2139
2192
  const h = gameHandlers.get(type) || [];
2140
2193
  console.log(`[SDK] 🔧 handleMessage game/${type} handlers=${h.length}`);
2141
- h.forEach((fn) => fn(payload ?? {}));
2194
+ for (const fn of h) callScenario(`game/${type} ハンドラ`, () => fn(payload ?? {}));
2142
2195
  return;
2143
2196
  }
2144
2197
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uzuhq/code-sdk",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "UZU PlayScreen SDK - Flutter ↔ JS ゲーム通信ライブラリ",
5
5
  "type": "module",
6
6
  "exports": {