@uzuhq/code-sdk 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/dev-hooks.d.ts +9 -1
- package/dist/dev-hooks.js +3 -1
- package/dist/dev-prediction-traps.d.ts +32 -0
- package/dist/dev-prediction-traps.js +0 -0
- package/dist/dev-prediction-traps.test.d.ts +1 -0
- package/dist/dev-prediction-traps.test.js +178 -0
- package/dist/dev-state-patch.d.ts +1 -1
- package/dist/dev-state-patch.js +1 -1
- package/dist/index.d.ts +10 -8
- package/dist/index.js +1 -0
- package/dist/run/local-server-action.d.ts +2 -2
- package/dist/run/local-server-action.js +1 -1
- package/dist/run/optimistic-action-client.d.ts +2 -2
- package/dist/run/optimistic-action-client.js +10 -6
- package/dist/run/server-action.d.ts +2 -2
- package/dist/server-only.d.ts +2 -2
- package/dist/sync/local.d.ts +2 -2
- package/dist/types.d.ts +18 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -566,7 +566,7 @@ scenario の `test/*.ts` / `tools/*.ts` で SDK を import せず `window.__uzu_
|
|
|
566
566
|
|
|
567
567
|
これで `(window as any).__uzu_dev` で殴らずに `window.__uzu_dev?.send({ as: 'dev_0', type: '...', payload: ... })` が補完 + 型チェックされる。SDK を直接 import するモジュールでは `dev-hooks.ts` 側の `declare global` で既に型が見えているため、本 entry を `types` に足す必要はない。
|
|
568
568
|
|
|
569
|
-
詳細は `docs/
|
|
569
|
+
詳細は `docs/uzu_code/sdk-guide/dev-hooks.md` を参照。
|
|
570
570
|
|
|
571
571
|
## License
|
|
572
572
|
|
package/dist/dev-hooks.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - 外部 automation API: docs/docs/
|
|
3
|
+
* - 外部 automation API: docs/docs/uzu_code/sdk-guide/dev-hooks.md
|
|
4
4
|
*
|
|
5
5
|
* 外部 automation (Playwright / AI agent / E2E test) が SDK が動いている iframe の
|
|
6
6
|
* state を決定論的に読み書きするための window.__uzu_dev API。
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* 特定 action 名のラッパー等) は scenario 側で window.__<scene>_dev を生やす。
|
|
21
21
|
*/
|
|
22
22
|
import type { JsonMergePatch, JsonPatchOp } from './dev-state-patch.js';
|
|
23
|
+
import type { PredictionWarning } from './dev-prediction-traps.js';
|
|
23
24
|
import type { ServerEvent } from './types.js';
|
|
24
25
|
export type { JsonMergePatch, JsonPatchOp } from './dev-state-patch.js';
|
|
25
26
|
export { applyJsonMergePatch, applyJsonPatch } from './dev-state-patch.js';
|
|
@@ -36,6 +37,13 @@ export interface UzuDevHooks<S = unknown> {
|
|
|
36
37
|
* 貼り付けやすい uint32 を返す。
|
|
37
38
|
*/
|
|
38
39
|
getSeed?(): number;
|
|
40
|
+
/**
|
|
41
|
+
* 素の action handler が先読み実行中に実時刻 / 乱数を読んだ記録。
|
|
42
|
+
*
|
|
43
|
+
* 先読みはサーバーと同じ結果を再現できることが前提なので、ここに何か入っていたら
|
|
44
|
+
* そのシナリオはオンラインでだけ予測がズレる。E2E で `[]` を assert すると回帰を防げる。
|
|
45
|
+
*/
|
|
46
|
+
getPredictionWarnings(): readonly PredictionWarning[];
|
|
39
47
|
/**
|
|
40
48
|
* Server-side で action を直接 dispatch する。
|
|
41
49
|
*
|
package/dist/dev-hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - 外部 automation API: docs/docs/
|
|
3
|
+
* - 外部 automation API: docs/docs/uzu_code/sdk-guide/dev-hooks.md
|
|
4
4
|
*
|
|
5
5
|
* 外部 automation (Playwright / AI agent / E2E test) が SDK が動いている iframe の
|
|
6
6
|
* state を決定論的に読み書きするための window.__uzu_dev API。
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* scenario 固有の helper (特定 field path の読み書き / phase 遷移時の field reset /
|
|
20
20
|
* 特定 action 名のラッパー等) は scenario 側で window.__<scene>_dev を生やす。
|
|
21
21
|
*/
|
|
22
|
+
import { getPredictionWarnings } from './dev-prediction-traps.js';
|
|
22
23
|
export { applyJsonMergePatch, applyJsonPatch } from './dev-state-patch.js';
|
|
23
24
|
export function createDevHooks(ctx) {
|
|
24
25
|
const getRawState = () => (ctx.getRawState ? ctx.getRawState() : null);
|
|
@@ -69,6 +70,7 @@ export function createDevHooks(ctx) {
|
|
|
69
70
|
playerId: () => ctx.playerId(),
|
|
70
71
|
subscribeSnapshot: (cb) => ctx.subscribeSnapshot(cb),
|
|
71
72
|
waitForSnapshot,
|
|
73
|
+
getPredictionWarnings: () => getPredictionWarnings(),
|
|
72
74
|
};
|
|
73
75
|
if (ctx.sendAction) {
|
|
74
76
|
const sendAction = ctx.sendAction;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @docs
|
|
3
|
+
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
+
* - 開発パターン: docs/docs/uzu_code/sdk-guide/patterns.md
|
|
5
|
+
*
|
|
6
|
+
* 素の action handler のクライアント先読み実行中に、サーバーと結果が一致しない API
|
|
7
|
+
* (実時刻 / 乱数) が呼ばれたら警告する。
|
|
8
|
+
*
|
|
9
|
+
* 先読みは「サーバーと同じコードを同じ入力で走らせれば同じ結果になる」ことが前提で、
|
|
10
|
+
* 実時刻や乱数を読むとその前提が崩れる。ズレた state は一瞬表示されたあと ack で
|
|
11
|
+
* 上書きされ、画面が飛ぶ。
|
|
12
|
+
*
|
|
13
|
+
* 静的解析ではなく実行時に差し替えるのは、handler がヘルパー関数を何段挟んでいても
|
|
14
|
+
* 捕まえたいから。同じ理由で、演出用途 (描画ループの `Math.sin(Date.now() / 400)` など)
|
|
15
|
+
* は先読み経路を通らないので原理的に誤検知しない。
|
|
16
|
+
*/
|
|
17
|
+
export interface PredictionWarning {
|
|
18
|
+
/** 呼び出した action 名 */
|
|
19
|
+
action: string;
|
|
20
|
+
/** 呼ばれた API 名 (`'Date.now()'` など) */
|
|
21
|
+
api: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 素の action handler の先読み実行を計装して走らせる。
|
|
25
|
+
*
|
|
26
|
+
* Flutter native ホスト (本番) では計装せず素通しする。判定基準は dev hooks と同じ。
|
|
27
|
+
*/
|
|
28
|
+
export declare const runPredicted: (action: string, run: () => void) => void;
|
|
29
|
+
/** 検出済みの警告一覧。`__uzu_dev.getPredictionWarnings()` から E2E で assert する用。 */
|
|
30
|
+
export declare const getPredictionWarnings: () => readonly PredictionWarning[];
|
|
31
|
+
/** 検出結果をリセットする (テスト用)。 */
|
|
32
|
+
export declare const clearPredictionWarnings: () => void;
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dev-prediction-traps.ts の unit test。
|
|
3
|
+
*
|
|
4
|
+
* 素の action handler をクライアント先読みで実行する間だけグローバルを差し替え、
|
|
5
|
+
* サーバーと結果が一致しない API (実時刻 / 乱数) の呼び出しを検出する仕組みを検証する。
|
|
6
|
+
*
|
|
7
|
+
* カバー対象:
|
|
8
|
+
* - 検出対象 API それぞれが記録されること
|
|
9
|
+
* - ヘルパー関数を経由した呼び出しも捕まえること (静的解析では追えない経路)
|
|
10
|
+
* - 同じ action / API の重複を 1 件に畳むこと
|
|
11
|
+
* - 実行後にグローバルが必ず元へ戻ること (例外時も含む)
|
|
12
|
+
* - 先読みの外では計装されていないこと
|
|
13
|
+
* - 本番 (Flutter native ホスト) では計装しないこと
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { clearPredictionWarnings, getPredictionWarnings, runPredicted, } from './dev-prediction-traps.js';
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
clearPredictionWarnings();
|
|
19
|
+
// 警告本文は console に出るので、テスト出力を汚さないよう黙らせる。
|
|
20
|
+
vi.spyOn(console, 'groupCollapsed').mockImplementation(() => { });
|
|
21
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
22
|
+
vi.spyOn(console, 'groupEnd').mockImplementation(() => { });
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
delete window.FlutterHost;
|
|
27
|
+
});
|
|
28
|
+
describe('runPredicted', () => {
|
|
29
|
+
/** 検出対象 API を先読み中に呼ぶと、action 名とセットで記録される。 */
|
|
30
|
+
describe('非決定的な API の検出', () => {
|
|
31
|
+
it('Date.now() を記録する', () => {
|
|
32
|
+
runPredicted('gm.timer.set', () => {
|
|
33
|
+
Date.now();
|
|
34
|
+
});
|
|
35
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'gm.timer.set', api: 'Date.now()' }]);
|
|
36
|
+
});
|
|
37
|
+
it('引数なしの new Date() を記録する', () => {
|
|
38
|
+
runPredicted('startPresentation', () => {
|
|
39
|
+
new Date();
|
|
40
|
+
});
|
|
41
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'startPresentation', api: 'new Date()' }]);
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* 引数付きの new Date() は与えられた値から決まるので、サーバーと結果がズレない。
|
|
45
|
+
* 誤検知を避けるため記録しない。
|
|
46
|
+
*/
|
|
47
|
+
it('引数付きの new Date(...) は記録しない', () => {
|
|
48
|
+
runPredicted('setDeadline', () => {
|
|
49
|
+
new Date(1700000000000);
|
|
50
|
+
});
|
|
51
|
+
expect(getPredictionWarnings()).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
it('Math.random() を記録する', () => {
|
|
54
|
+
runPredicted('restart', () => {
|
|
55
|
+
Math.random();
|
|
56
|
+
});
|
|
57
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'restart', api: 'Math.random()' }]);
|
|
58
|
+
});
|
|
59
|
+
it('performance.now() を記録する', () => {
|
|
60
|
+
runPredicted('tickLocal', () => {
|
|
61
|
+
performance.now();
|
|
62
|
+
});
|
|
63
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'tickLocal', api: 'performance.now()' }]);
|
|
64
|
+
});
|
|
65
|
+
/**
|
|
66
|
+
* 静的解析が最も苦手とする経路。handler 本体に API が現れず、ヘルパー関数の中で
|
|
67
|
+
* 呼ばれていても捕まえられることを保証する (kikaidochu の setLine が実際にこの形)。
|
|
68
|
+
*/
|
|
69
|
+
it('ヘルパー関数を経由した呼び出しも記録する', () => {
|
|
70
|
+
const setDeadlineViaHelper = () => Date.now() + 60000;
|
|
71
|
+
runPredicted('setLine', () => {
|
|
72
|
+
setDeadlineViaHelper();
|
|
73
|
+
});
|
|
74
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'setLine', api: 'Date.now()' }]);
|
|
75
|
+
});
|
|
76
|
+
/** 元の API の戻り値はそのまま通す (計装で挙動を変えない)。 */
|
|
77
|
+
it('元の API の戻り値を変えない', () => {
|
|
78
|
+
let observed = 0;
|
|
79
|
+
runPredicted('noop', () => {
|
|
80
|
+
observed = Date.now();
|
|
81
|
+
});
|
|
82
|
+
expect(observed).toBeGreaterThan(0);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe('重複の抑制', () => {
|
|
86
|
+
/** 同じ action で同じ API を何度呼んでも記録は 1 件。連打でログが溢れない。 */
|
|
87
|
+
it('同一 action / 同一 API は 1 件に畳む', () => {
|
|
88
|
+
runPredicted('gm.timer.set', () => {
|
|
89
|
+
Date.now();
|
|
90
|
+
Date.now();
|
|
91
|
+
});
|
|
92
|
+
runPredicted('gm.timer.set', () => {
|
|
93
|
+
Date.now();
|
|
94
|
+
});
|
|
95
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'gm.timer.set', api: 'Date.now()' }]);
|
|
96
|
+
});
|
|
97
|
+
/** action が違えば別件として記録する。どの action を直すべきか分かる必要がある。 */
|
|
98
|
+
it('action が違えば別件として記録する', () => {
|
|
99
|
+
runPredicted('a', () => {
|
|
100
|
+
Date.now();
|
|
101
|
+
});
|
|
102
|
+
runPredicted('b', () => {
|
|
103
|
+
Date.now();
|
|
104
|
+
});
|
|
105
|
+
expect(getPredictionWarnings()).toEqual([
|
|
106
|
+
{ action: 'a', api: 'Date.now()' },
|
|
107
|
+
{ action: 'b', api: 'Date.now()' },
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
/** 同じ action でも API が違えば別件。両方直す必要があるため。 */
|
|
111
|
+
it('API が違えば別件として記録する', () => {
|
|
112
|
+
runPredicted('restart', () => {
|
|
113
|
+
Date.now();
|
|
114
|
+
Math.random();
|
|
115
|
+
});
|
|
116
|
+
expect(getPredictionWarnings()).toEqual([
|
|
117
|
+
{ action: 'restart', api: 'Date.now()' },
|
|
118
|
+
{ action: 'restart', api: 'Math.random()' },
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('グローバルの復元', () => {
|
|
123
|
+
/** 先読みの外で呼ばれる API は計装されていない (描画ループが誤検知しない前提)。 */
|
|
124
|
+
it('実行後にグローバルが元へ戻る', () => {
|
|
125
|
+
const beforeNow = Date.now;
|
|
126
|
+
const beforeRandom = Math.random;
|
|
127
|
+
const beforeDate = Date;
|
|
128
|
+
runPredicted('a', () => {
|
|
129
|
+
Date.now();
|
|
130
|
+
});
|
|
131
|
+
expect(Date.now).toBe(beforeNow);
|
|
132
|
+
expect(Math.random).toBe(beforeRandom);
|
|
133
|
+
expect(Date).toBe(beforeDate);
|
|
134
|
+
});
|
|
135
|
+
/**
|
|
136
|
+
* handler が throw しても復元する。楽観実行は throw を握りつぶして
|
|
137
|
+
* サーバー送信だけ続ける経路があるので、ここで漏れるとグローバルが汚染されたまま残る。
|
|
138
|
+
*/
|
|
139
|
+
it('handler が例外を投げてもグローバルが元へ戻る', () => {
|
|
140
|
+
const beforeNow = Date.now;
|
|
141
|
+
const beforeDate = Date;
|
|
142
|
+
expect(() => {
|
|
143
|
+
runPredicted('a', () => {
|
|
144
|
+
Date.now();
|
|
145
|
+
throw new Error('handler failed');
|
|
146
|
+
});
|
|
147
|
+
}).toThrow('handler failed');
|
|
148
|
+
expect(Date.now).toBe(beforeNow);
|
|
149
|
+
expect(Date).toBe(beforeDate);
|
|
150
|
+
// 例外で中断しても、そこまでに呼ばれた API は記録されている。
|
|
151
|
+
expect(getPredictionWarnings()).toEqual([{ action: 'a', api: 'Date.now()' }]);
|
|
152
|
+
});
|
|
153
|
+
/** 先読みの外の呼び出しは記録されない。描画コードの Date.now() を拾わない保証。 */
|
|
154
|
+
it('先読みの外で呼んだ API は記録しない', () => {
|
|
155
|
+
Date.now();
|
|
156
|
+
Math.random();
|
|
157
|
+
expect(getPredictionWarnings()).toEqual([]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('本番での無効化', () => {
|
|
161
|
+
/**
|
|
162
|
+
* Flutter native ホストでは計装しない。判定基準は dev hooks の attach と同じく
|
|
163
|
+
* `window.FlutterHost` の有無。
|
|
164
|
+
*/
|
|
165
|
+
it('window.FlutterHost があるときは計装せず素通しする', () => {
|
|
166
|
+
window.FlutterHost = { postMessage: () => { } };
|
|
167
|
+
const beforeNow = Date.now;
|
|
168
|
+
let ran = false;
|
|
169
|
+
runPredicted('gm.timer.set', () => {
|
|
170
|
+
Date.now();
|
|
171
|
+
ran = true;
|
|
172
|
+
});
|
|
173
|
+
expect(ran).toBe(true);
|
|
174
|
+
expect(Date.now).toBe(beforeNow);
|
|
175
|
+
expect(getPredictionWarnings()).toEqual([]);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - 外部 automation API: docs/docs/
|
|
3
|
+
* - 外部 automation API: docs/docs/uzu_code/sdk-guide/dev-hooks.md
|
|
4
4
|
*
|
|
5
5
|
* `__uzu_dev.mergeRawState` / `__uzu_dev.patchRawState` の中身。
|
|
6
6
|
* scenario state を「object 階層の部分更新 (RFC 7396)」と「array 要素単体の書換
|
package/dist/dev-state-patch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - 外部 automation API: docs/docs/
|
|
3
|
+
* - 外部 automation API: docs/docs/uzu_code/sdk-guide/dev-hooks.md
|
|
4
4
|
*
|
|
5
5
|
* `__uzu_dev.mergeRawState` / `__uzu_dev.patchRawState` の中身。
|
|
6
6
|
* scenario state を「object 階層の部分更新 (RFC 7396)」と「array 要素単体の書換
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - SDK仕様: docs/docs/
|
|
4
|
-
* - ブリッジ仕様: docs/docs/
|
|
5
|
-
* - Relay仕様: docs/docs/
|
|
6
|
-
* - SyncRoom仕様: docs/docs/
|
|
7
|
-
* - ゲーム仕様: docs/docs/
|
|
8
|
-
* - システム全体像: docs/docs/
|
|
9
|
-
* - AI用APIリファレンス (README): javascript/
|
|
3
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
4
|
+
* - ブリッジ仕様: docs/docs/uzu_code/bridge.md
|
|
5
|
+
* - Relay仕様: docs/docs/uzu_code/relay.md
|
|
6
|
+
* - SyncRoom仕様: docs/docs/uzu_code/sync-room.md
|
|
7
|
+
* - ゲーム仕様: docs/docs/uzu_code/games.md
|
|
8
|
+
* - システム全体像: docs/docs/uzu_code/overview.md
|
|
9
|
+
* - AI用APIリファレンス (README): javascript/uzu_code/sdk/README.md
|
|
10
10
|
*
|
|
11
11
|
* SDK は runtime のみ。 dev harness の DOM 描画 (iframe grid / HUD / state
|
|
12
12
|
* inspector) と in-memory サーバー (Node ws による GameRoom / SyncRoom / RelayRoom) は
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* scenario ディレクトリで `uzu dev` を実行するだけで良い。 子 iframe は既存 online mode
|
|
16
16
|
* (`?server=ws://localhost:<port>` 経路) で dev-server に接続する。
|
|
17
17
|
*/
|
|
18
|
-
export type { PlayScreenMessage, BridgeChannel, BridgeMessage, Seat, SeatKind, Emit, ServerEvent, SeededRandom, GameLogic, GameConfig, SyncConfig, PatchFn, SetFn, Operation, ConnectionState, ConnectionCallbacks, PlayerVoiceState, PlayersChangedMessage, ActionHandler, ServerOnlyAction, ServerOnlyActionHandlerFn, } from './types.js';
|
|
18
|
+
export type { PlayScreenMessage, BridgeChannel, BridgeMessage, Seat, SeatKind, Emit, ServerEvent, SeededRandom, GameLogic, GameConfig, SyncConfig, PatchFn, SetFn, Operation, ConnectionState, ConnectionCallbacks, PlayerVoiceState, PlayersChangedMessage, ActionContext, ActionHandler, ServerOnlyAction, ServerOnlyActionContext, ServerOnlyActionHandlerFn, } from './types.js';
|
|
19
19
|
export { SERVER_TIME, DEFAULT_ICON_URLS } from './types.js';
|
|
20
20
|
export { serverOnly, isServerOnlyAction } from './server-only.js';
|
|
21
21
|
export { Room } from './room.js';
|
|
@@ -27,6 +27,8 @@ export { applyJsonMergePatch, applyJsonPatch } from './dev-state-patch.js';
|
|
|
27
27
|
export type { JsonMergePatch, JsonPatchOp } from './dev-state-patch.js';
|
|
28
28
|
export { attachDevHooks, createDevHooks } from './dev-hooks.js';
|
|
29
29
|
export type { UzuDevHooks, DevHooksCtx, RunHandle, SyncHandle } from './dev-hooks.js';
|
|
30
|
+
export { getPredictionWarnings } from './dev-prediction-traps.js';
|
|
31
|
+
export type { PredictionWarning } from './dev-prediction-traps.js';
|
|
30
32
|
import type { BridgeMessage, GameConfig, SyncConfig, PlayerVoiceState } from './types.js';
|
|
31
33
|
import type { RoomLike } from './room.js';
|
|
32
34
|
type GameMessageHandler = (payload: Record<string, unknown>) => void;
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { ReconnectableWebSocket } from './reconnectable-ws.js';
|
|
|
5
5
|
export { SeededRandomImpl } from './random.js';
|
|
6
6
|
export { applyJsonMergePatch, applyJsonPatch } from './dev-state-patch.js';
|
|
7
7
|
export { attachDevHooks, createDevHooks } from './dev-hooks.js';
|
|
8
|
+
export { getPredictionWarnings } from './dev-prediction-traps.js';
|
|
8
9
|
import { Room } from './room.js';
|
|
9
10
|
import { ReconnectableWebSocket } from './reconnectable-ws.js';
|
|
10
11
|
import { runOnlineServerAction } from './run/server-action.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - ServerAction仕様: docs/docs/
|
|
4
|
-
* - SDK仕様: docs/docs/
|
|
3
|
+
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
5
5
|
*
|
|
6
6
|
* ブラウザ内 LocalGameRoom — GameRoom DO の動作をサーバーレスで再現する。
|
|
7
7
|
* server_url がない場合に vite dev だけで ServerAction シナリオをローカル実行する。
|
|
@@ -49,7 +49,7 @@ export function runLocalServerAction(config) {
|
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
|
-
handler(state, payload ?? {}, myId, actionEmit, {
|
|
52
|
+
handler(state, payload ?? {}, myId, actionEmit, {});
|
|
53
53
|
}
|
|
54
54
|
catch (err) {
|
|
55
55
|
console.warn('[SDK LocalServerAction] Action error:', err);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - ServerAction仕様: docs/docs/
|
|
4
|
-
* - 開発パターン: docs/docs/
|
|
3
|
+
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
+
* - 開発パターン: docs/docs/uzu_code/sdk-guide/patterns.md
|
|
5
5
|
*
|
|
6
6
|
* ServerAction の楽観的更新クライアント — online / emulator 共通ロジック。
|
|
7
7
|
*
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyPatch } from '../json-patch.js';
|
|
2
2
|
import { isServerOnlyAction } from '../server-only.js';
|
|
3
|
+
import { runPredicted } from '../dev-prediction-traps.js';
|
|
3
4
|
export function createOptimisticActionClient(config) {
|
|
4
5
|
const { logic, playerId, onState, events, sendAction } = config;
|
|
5
6
|
/** サーバー確定 state (楽観的更新のベース) */
|
|
@@ -32,13 +33,14 @@ export function createOptimisticActionClient(config) {
|
|
|
32
33
|
while (i < pendingActions.length) {
|
|
33
34
|
const { action, payload } = pendingActions[i];
|
|
34
35
|
const handler = logic.actions[action];
|
|
35
|
-
|
|
36
|
+
// serverOnly handler は send 時点で pending に積まれないので、ここに来るのは
|
|
37
|
+
// 素の handler だけ。型を絞るためにも明示的に弾く。
|
|
38
|
+
if (!handler || isServerOnlyAction(handler)) {
|
|
36
39
|
pendingActions.splice(i, 1);
|
|
37
40
|
continue;
|
|
38
41
|
}
|
|
39
42
|
try {
|
|
40
|
-
|
|
41
|
-
handler(displayState, payload, playerId, noopEmit, { tick: 0 });
|
|
43
|
+
runPredicted(action, () => handler(displayState, payload, playerId, noopEmit, {}));
|
|
42
44
|
i++;
|
|
43
45
|
}
|
|
44
46
|
catch {
|
|
@@ -69,11 +71,13 @@ export function createOptimisticActionClient(config) {
|
|
|
69
71
|
// serverOnly handler は先行実行を skip。pending にも積まないので、ack 受信時の
|
|
70
72
|
// isMyAck 判定で false となり、サーバー発の events が普通に emit される。
|
|
71
73
|
const handler = logic.actions[type];
|
|
72
|
-
|
|
74
|
+
// callback 内では displayState の narrowing が効かないので const に退避する。
|
|
75
|
+
const target = displayState;
|
|
76
|
+
if (handler && !isServerOnlyAction(handler) && target !== null) {
|
|
73
77
|
try {
|
|
74
|
-
handler(
|
|
78
|
+
runPredicted(type, () => handler(target, payload ?? {}, playerId, emit, {}));
|
|
75
79
|
pendingActions.push({ seq, action: type, payload: payload ?? {} });
|
|
76
|
-
onState(
|
|
80
|
+
onState(target, playerId);
|
|
77
81
|
}
|
|
78
82
|
catch {
|
|
79
83
|
// ローカル実行失敗 → 楽観的更新せずサーバーに送るだけ
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - ServerAction仕様: docs/docs/
|
|
4
|
-
* - SDK仕様: docs/docs/
|
|
3
|
+
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
5
5
|
*
|
|
6
6
|
* ServerAction モードのオンライン接続 (WebSocket トランスポート)。
|
|
7
7
|
*
|
package/dist/server-only.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - ServerAction仕様: docs/docs/
|
|
4
|
-
* - 開発パターン: docs/docs/
|
|
3
|
+
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
+
* - 開発パターン: docs/docs/uzu_code/sdk-guide/patterns.md
|
|
5
5
|
*
|
|
6
6
|
* serverOnly() で wrap した handler はクライアント側の楽観的更新(先行実行)を
|
|
7
7
|
* スキップし、サーバー側でだけ実行される。fetch などの副作用付き処理を安全に
|
package/dist/sync/local.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - SDK仕様: docs/docs/
|
|
4
|
-
* - 開発パターン: docs/docs/
|
|
3
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
4
|
+
* - 開発パターン: docs/docs/uzu_code/sdk-guide/patterns.md
|
|
5
5
|
*/
|
|
6
6
|
import type { SyncConfig } from '../types.js';
|
|
7
7
|
import type { SyncHandle } from '../dev-hooks.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @docs
|
|
3
|
-
* - SDK仕様: docs/docs/
|
|
4
|
-
* - APIリファレンス: docs/docs/
|
|
5
|
-
* - 開発パターン: docs/docs/
|
|
6
|
-
* - 外部 automation API: docs/docs/
|
|
3
|
+
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
4
|
+
* - APIリファレンス: docs/docs/uzu_code/sdk-guide/api-reference.md
|
|
5
|
+
* - 開発パターン: docs/docs/uzu_code/sdk-guide/patterns.md
|
|
6
|
+
* - 外部 automation API: docs/docs/uzu_code/sdk-guide/dev-hooks.md
|
|
7
7
|
*/
|
|
8
8
|
/** @deprecated 旧フラット形式。新コードでは BridgeMessage を使用 */
|
|
9
9
|
export type PlayScreenMessage = {
|
|
@@ -67,12 +67,21 @@ export interface SeededRandom {
|
|
|
67
67
|
pick<T>(array: T[]): T;
|
|
68
68
|
shuffle<T>(array: T[]): T[];
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
/**
|
|
71
|
+
* 素の action handler の実行文脈。空なのは意図的。
|
|
72
|
+
*
|
|
73
|
+
* 素の handler はサーバーとクライアント先読みの両方で走る。クライアントが自力で
|
|
74
|
+
* 再現できない値 (tick / 実時刻 / 乱数) をここで配ると、サーバー・ソロ・dev では
|
|
75
|
+
* 本物が入るのにオンラインの先読みだけ値がズレる、という一番気付きにくい形で壊れる。
|
|
76
|
+
* そういう値が要る action は serverOnly() にして先読みの対象から外す。
|
|
77
|
+
*/
|
|
78
|
+
export type ActionContext = Record<never, never>;
|
|
79
|
+
/** serverOnly() handler の実行文脈。サーバーでしか走らないので tick を渡せる。 */
|
|
80
|
+
export interface ServerOnlyActionContext {
|
|
74
81
|
tick: number;
|
|
75
|
-
}
|
|
82
|
+
}
|
|
83
|
+
export type ActionHandler<S> = (state: S, payload: any, playerId: string, emit: Emit, ctx: ActionContext) => void;
|
|
84
|
+
export type ServerOnlyActionHandlerFn<S> = (state: S, payload: any, playerId: string, emit: Emit, ctx: ServerOnlyActionContext) => Promise<void> | void;
|
|
76
85
|
/** serverOnly() で wrap された handler。`__serverOnly` brand で識別する。 */
|
|
77
86
|
export type ServerOnlyAction<S> = ServerOnlyActionHandlerFn<S> & {
|
|
78
87
|
readonly __serverOnly: true;
|