@uzuhq/code-sdk 0.7.6 → 0.7.7
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/dev-globals.d.ts +12 -27
- package/dist/dev-globals.js +0 -15
- package/dist/dev-hooks-ClWM8HzI.d.ts +682 -0
- package/dist/index.d.ts +152 -66
- package/dist/index.js +1837 -416
- package/package.json +8 -5
- package/dist/action-types.test-d.d.ts +0 -11
- package/dist/action-types.test-d.js +0 -101
- package/dist/dev-hooks.d.ts +0 -241
- package/dist/dev-hooks.js +0 -132
- package/dist/dev-hooks.test.d.ts +0 -1
- package/dist/dev-hooks.test.js +0 -294
- package/dist/dev-prediction-traps.d.ts +0 -32
- package/dist/dev-prediction-traps.js +0 -0
- package/dist/dev-prediction-traps.test.d.ts +0 -1
- package/dist/dev-prediction-traps.test.js +0 -178
- package/dist/dev-state-patch.d.ts +0 -81
- package/dist/dev-state-patch.js +0 -295
- package/dist/dev-state-patch.test.d.ts +0 -1
- package/dist/dev-state-patch.test.js +0 -333
- package/dist/json-patch.d.ts +0 -7
- package/dist/json-patch.js +0 -78
- package/dist/random.d.ts +0 -11
- package/dist/random.js +0 -34
- package/dist/reconnectable-ws.d.ts +0 -60
- package/dist/reconnectable-ws.js +0 -229
- package/dist/room.d.ts +0 -23
- package/dist/room.js +0 -36
- package/dist/roster-params.test.d.ts +0 -1
- package/dist/roster-params.test.js +0 -86
- package/dist/run/local-server-action.d.ts +0 -16
- package/dist/run/local-server-action.js +0 -217
- package/dist/run/local-server-action.test.d.ts +0 -1
- package/dist/run/local-server-action.test.js +0 -242
- package/dist/run/optimistic-action-client.d.ts +0 -68
- package/dist/run/optimistic-action-client.js +0 -209
- package/dist/run/optimistic-action-client.test.d.ts +0 -1
- package/dist/run/optimistic-action-client.test.js +0 -430
- package/dist/run/server-action.d.ts +0 -16
- package/dist/run/server-action.js +0 -181
- package/dist/run/server-action.test.d.ts +0 -1
- package/dist/run/server-action.test.js +0 -105
- package/dist/server-clock.d.ts +0 -29
- package/dist/server-clock.js +0 -40
- package/dist/server-only.d.ts +0 -33
- package/dist/server-only.js +0 -21
- package/dist/sync/local.d.ts +0 -8
- package/dist/sync/local.js +0 -50
- package/dist/sync/online.d.ts +0 -5
- package/dist/sync/online.js +0 -165
- package/dist/types.d.ts +0 -345
- package/dist/types.js +0 -8
package/dist/json-patch.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
function escapePointer(key) {
|
|
2
|
-
return key.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
3
|
-
}
|
|
4
|
-
function unescapePointer(token) {
|
|
5
|
-
return token.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
6
|
-
}
|
|
7
|
-
export function compare(oldObj, newObj, basePath = '') {
|
|
8
|
-
if (oldObj === newObj)
|
|
9
|
-
return [];
|
|
10
|
-
// Handle nulls / primitives
|
|
11
|
-
if (oldObj === null ||
|
|
12
|
-
newObj === null ||
|
|
13
|
-
typeof oldObj !== 'object' ||
|
|
14
|
-
typeof newObj !== 'object') {
|
|
15
|
-
return [{ op: 'replace', path: basePath || '/', value: newObj }];
|
|
16
|
-
}
|
|
17
|
-
// Arrays: compare as whole (no element-level diff)
|
|
18
|
-
if (Array.isArray(oldObj) || Array.isArray(newObj)) {
|
|
19
|
-
if (JSON.stringify(oldObj) === JSON.stringify(newObj))
|
|
20
|
-
return [];
|
|
21
|
-
return [{ op: 'replace', path: basePath || '/', value: newObj }];
|
|
22
|
-
}
|
|
23
|
-
// Objects: recurse
|
|
24
|
-
const ops = [];
|
|
25
|
-
const oldKeys = Object.keys(oldObj);
|
|
26
|
-
const newKeys = Object.keys(newObj);
|
|
27
|
-
// Removed keys
|
|
28
|
-
for (const key of oldKeys) {
|
|
29
|
-
if (!(key in newObj)) {
|
|
30
|
-
ops.push({ op: 'remove', path: `${basePath}/${escapePointer(key)}` });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
// Added or changed keys
|
|
34
|
-
for (const key of newKeys) {
|
|
35
|
-
const childPath = `${basePath}/${escapePointer(key)}`;
|
|
36
|
-
if (!(key in oldObj)) {
|
|
37
|
-
ops.push({ op: 'add', path: childPath, value: newObj[key] });
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
const childOps = compare(oldObj[key], newObj[key], childPath);
|
|
41
|
-
ops.push(...childOps);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return ops;
|
|
45
|
-
}
|
|
46
|
-
export function applyPatch(doc, ops) {
|
|
47
|
-
for (const op of ops) {
|
|
48
|
-
const tokens = op.path.split('/').slice(1).map(unescapePointer);
|
|
49
|
-
if (tokens.length === 0)
|
|
50
|
-
return false;
|
|
51
|
-
if (op.op === 'replace' || op.op === 'add') {
|
|
52
|
-
let target = doc;
|
|
53
|
-
for (let i = 0; i < tokens.length - 1; i++) {
|
|
54
|
-
target = target?.[tokens[i]];
|
|
55
|
-
if (target === undefined || target === null)
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
const lastKey = tokens[tokens.length - 1];
|
|
59
|
-
target[lastKey] = op.value;
|
|
60
|
-
}
|
|
61
|
-
else if (op.op === 'remove') {
|
|
62
|
-
let target = doc;
|
|
63
|
-
for (let i = 0; i < tokens.length - 1; i++) {
|
|
64
|
-
target = target?.[tokens[i]];
|
|
65
|
-
if (target === undefined || target === null)
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
const lastKey = tokens[tokens.length - 1];
|
|
69
|
-
if (Array.isArray(target)) {
|
|
70
|
-
target.splice(Number(lastKey), 1);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
delete target[lastKey];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
78
|
-
}
|
package/dist/random.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { SeededRandom } from './types.js';
|
|
2
|
-
export declare class SeededRandomImpl implements SeededRandom {
|
|
3
|
-
private _state;
|
|
4
|
-
constructor(seed: number);
|
|
5
|
-
get state(): number;
|
|
6
|
-
static fromState(state: number): SeededRandomImpl;
|
|
7
|
-
float(): number;
|
|
8
|
-
int(max: number): number;
|
|
9
|
-
pick<T>(array: T[]): T;
|
|
10
|
-
shuffle<T>(array: T[]): T[];
|
|
11
|
-
}
|
package/dist/random.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export class SeededRandomImpl {
|
|
2
|
-
constructor(seed) {
|
|
3
|
-
this._state = seed | 0;
|
|
4
|
-
}
|
|
5
|
-
get state() {
|
|
6
|
-
return this._state;
|
|
7
|
-
}
|
|
8
|
-
static fromState(state) {
|
|
9
|
-
const r = new SeededRandomImpl(0);
|
|
10
|
-
r._state = state;
|
|
11
|
-
return r;
|
|
12
|
-
}
|
|
13
|
-
float() {
|
|
14
|
-
this._state |= 0;
|
|
15
|
-
this._state = (this._state + 0x6d2b79f5) | 0;
|
|
16
|
-
let t = Math.imul(this._state ^ (this._state >>> 15), 1 | this._state);
|
|
17
|
-
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
18
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
19
|
-
}
|
|
20
|
-
int(max) {
|
|
21
|
-
return Math.floor(this.float() * max);
|
|
22
|
-
}
|
|
23
|
-
pick(array) {
|
|
24
|
-
return array[this.int(array.length)];
|
|
25
|
-
}
|
|
26
|
-
shuffle(array) {
|
|
27
|
-
const a = [...array];
|
|
28
|
-
for (let i = a.length - 1; i > 0; i--) {
|
|
29
|
-
const j = this.int(i + 1);
|
|
30
|
-
[a[i], a[j]] = [a[j], a[i]];
|
|
31
|
-
}
|
|
32
|
-
return a;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 自動再接続 WebSocket ラッパー
|
|
3
|
-
*
|
|
4
|
-
* - Exponential backoff + jitter 付き自動再接続
|
|
5
|
-
* - Heartbeat (__ping / __pong) による接続生存確認
|
|
6
|
-
* - 再接続中のメッセージバッファリング
|
|
7
|
-
*/
|
|
8
|
-
export type ConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'disconnected';
|
|
9
|
-
export interface ReconnectableWSOptions {
|
|
10
|
-
/** 最大再接続試行回数 (default: 15) */
|
|
11
|
-
maxReconnectAttempts?: number;
|
|
12
|
-
/** Backoff の基準遅延 ms (default: 1000) */
|
|
13
|
-
baseDelay?: number;
|
|
14
|
-
/** Backoff の最大遅延 ms (default: 30000) */
|
|
15
|
-
maxDelay?: number;
|
|
16
|
-
/** Jitter 係数 0〜1 (default: 0.5) */
|
|
17
|
-
jitterFactor?: number;
|
|
18
|
-
/** Heartbeat 送信間隔 ms (default: 25000) */
|
|
19
|
-
heartbeatInterval?: number;
|
|
20
|
-
/** Pong 待ちタイムアウト ms (default: 10000) */
|
|
21
|
-
heartbeatTimeout?: number;
|
|
22
|
-
/** 再接続中のバッファ上限 (default: 50) */
|
|
23
|
-
maxBufferSize?: number;
|
|
24
|
-
/** バッファ対象判定。false を返すとドロップ */
|
|
25
|
-
shouldBuffer?: (data: string) => boolean;
|
|
26
|
-
/** 接続状態変更コールバック */
|
|
27
|
-
onConnectionStateChange?: (state: ConnectionState) => void;
|
|
28
|
-
}
|
|
29
|
-
export declare class ReconnectableWebSocket {
|
|
30
|
-
private ws;
|
|
31
|
-
private readonly url;
|
|
32
|
-
private readonly opts;
|
|
33
|
-
private _state;
|
|
34
|
-
private reconnectAttempt;
|
|
35
|
-
private messageBuffer;
|
|
36
|
-
private messageHandlers;
|
|
37
|
-
private heartbeatTimer;
|
|
38
|
-
private heartbeatTimeoutTimer;
|
|
39
|
-
private reconnectTimer;
|
|
40
|
-
private disposed;
|
|
41
|
-
constructor(url: string, options?: ReconnectableWSOptions);
|
|
42
|
-
get connectionState(): ConnectionState;
|
|
43
|
-
/** WebSocket.readyState 互換 (既存コードとの互換用) */
|
|
44
|
-
get readyState(): number;
|
|
45
|
-
send(data: string): void;
|
|
46
|
-
addEventListener(type: 'message', handler: (ev: MessageEvent) => void): void;
|
|
47
|
-
removeEventListener(type: 'message', handler: (ev: MessageEvent) => void): void;
|
|
48
|
-
/** バッファをクリア(権威的 state 受信時に呼ぶ) */
|
|
49
|
-
clearBuffer(): void;
|
|
50
|
-
/** 全リソース解放 */
|
|
51
|
-
dispose(): void;
|
|
52
|
-
private connect;
|
|
53
|
-
private setState;
|
|
54
|
-
private scheduleReconnect;
|
|
55
|
-
private getReconnectDelay;
|
|
56
|
-
private flushBuffer;
|
|
57
|
-
private startHeartbeat;
|
|
58
|
-
private stopHeartbeat;
|
|
59
|
-
private handlePong;
|
|
60
|
-
}
|
package/dist/reconnectable-ws.js
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 自動再接続 WebSocket ラッパー
|
|
3
|
-
*
|
|
4
|
-
* - Exponential backoff + jitter 付き自動再接続
|
|
5
|
-
* - Heartbeat (__ping / __pong) による接続生存確認
|
|
6
|
-
* - 再接続中のメッセージバッファリング
|
|
7
|
-
*/
|
|
8
|
-
const DEFAULTS = {
|
|
9
|
-
maxReconnectAttempts: 15,
|
|
10
|
-
baseDelay: 1000,
|
|
11
|
-
maxDelay: 30000,
|
|
12
|
-
jitterFactor: 0.5,
|
|
13
|
-
heartbeatInterval: 25000,
|
|
14
|
-
heartbeatTimeout: 10000,
|
|
15
|
-
maxBufferSize: 50,
|
|
16
|
-
};
|
|
17
|
-
export class ReconnectableWebSocket {
|
|
18
|
-
constructor(url, options = {}) {
|
|
19
|
-
this.ws = null;
|
|
20
|
-
this._state = 'connecting';
|
|
21
|
-
this.reconnectAttempt = 0;
|
|
22
|
-
this.messageBuffer = [];
|
|
23
|
-
this.messageHandlers = [];
|
|
24
|
-
this.heartbeatTimer = null;
|
|
25
|
-
this.heartbeatTimeoutTimer = null;
|
|
26
|
-
this.reconnectTimer = null;
|
|
27
|
-
this.disposed = false;
|
|
28
|
-
this.url = url;
|
|
29
|
-
this.opts = {
|
|
30
|
-
maxReconnectAttempts: options.maxReconnectAttempts ?? DEFAULTS.maxReconnectAttempts,
|
|
31
|
-
baseDelay: options.baseDelay ?? DEFAULTS.baseDelay,
|
|
32
|
-
maxDelay: options.maxDelay ?? DEFAULTS.maxDelay,
|
|
33
|
-
jitterFactor: options.jitterFactor ?? DEFAULTS.jitterFactor,
|
|
34
|
-
heartbeatInterval: options.heartbeatInterval ?? DEFAULTS.heartbeatInterval,
|
|
35
|
-
heartbeatTimeout: options.heartbeatTimeout ?? DEFAULTS.heartbeatTimeout,
|
|
36
|
-
maxBufferSize: options.maxBufferSize ?? DEFAULTS.maxBufferSize,
|
|
37
|
-
shouldBuffer: options.shouldBuffer,
|
|
38
|
-
onConnectionStateChange: options.onConnectionStateChange,
|
|
39
|
-
};
|
|
40
|
-
this.connect();
|
|
41
|
-
}
|
|
42
|
-
// ─── Public API ──────────────────────────────────────────
|
|
43
|
-
get connectionState() {
|
|
44
|
-
return this._state;
|
|
45
|
-
}
|
|
46
|
-
/** WebSocket.readyState 互換 (既存コードとの互換用) */
|
|
47
|
-
get readyState() {
|
|
48
|
-
return this.ws?.readyState ?? WebSocket.CLOSED;
|
|
49
|
-
}
|
|
50
|
-
send(data) {
|
|
51
|
-
if (this._state === 'connected' && this.ws?.readyState === WebSocket.OPEN) {
|
|
52
|
-
this.ws.send(data);
|
|
53
|
-
}
|
|
54
|
-
else if (this._state === 'reconnecting') {
|
|
55
|
-
if (this.opts.shouldBuffer && !this.opts.shouldBuffer(data))
|
|
56
|
-
return;
|
|
57
|
-
if (this.messageBuffer.length < this.opts.maxBufferSize) {
|
|
58
|
-
this.messageBuffer.push(data);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// disconnected / connecting: ドロップ
|
|
62
|
-
}
|
|
63
|
-
addEventListener(type, handler) {
|
|
64
|
-
if (type === 'message') {
|
|
65
|
-
this.messageHandlers.push(handler);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
removeEventListener(type, handler) {
|
|
69
|
-
if (type === 'message') {
|
|
70
|
-
const index = this.messageHandlers.indexOf(handler);
|
|
71
|
-
if (index !== -1) {
|
|
72
|
-
this.messageHandlers.splice(index, 1);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/** バッファをクリア(権威的 state 受信時に呼ぶ) */
|
|
77
|
-
clearBuffer() {
|
|
78
|
-
this.messageBuffer = [];
|
|
79
|
-
}
|
|
80
|
-
/** 全リソース解放 */
|
|
81
|
-
dispose() {
|
|
82
|
-
this.disposed = true;
|
|
83
|
-
this.stopHeartbeat();
|
|
84
|
-
if (this.reconnectTimer) {
|
|
85
|
-
clearTimeout(this.reconnectTimer);
|
|
86
|
-
this.reconnectTimer = null;
|
|
87
|
-
}
|
|
88
|
-
if (this.ws) {
|
|
89
|
-
this.ws.onopen = null;
|
|
90
|
-
this.ws.onclose = null;
|
|
91
|
-
this.ws.onerror = null;
|
|
92
|
-
this.ws.onmessage = null;
|
|
93
|
-
try {
|
|
94
|
-
if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
|
|
95
|
-
this.ws.close();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
catch (e) {
|
|
99
|
-
console.warn('[ReconnectableWS] Error closing WebSocket:', e);
|
|
100
|
-
}
|
|
101
|
-
this.ws = null;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
// ─── Private ─────────────────────────────────────────────
|
|
105
|
-
connect() {
|
|
106
|
-
if (this.disposed)
|
|
107
|
-
return;
|
|
108
|
-
console.log(`[ReconnectableWS] 🔗 Connecting to ${this.url} (attempt=${this.reconnectAttempt})`);
|
|
109
|
-
const ws = new WebSocket(this.url);
|
|
110
|
-
ws.onopen = () => {
|
|
111
|
-
if (this.disposed)
|
|
112
|
-
return;
|
|
113
|
-
console.log(`[ReconnectableWS] ✅ Connected`);
|
|
114
|
-
this.reconnectAttempt = 0;
|
|
115
|
-
this.setState('connected');
|
|
116
|
-
this.startHeartbeat();
|
|
117
|
-
this.flushBuffer();
|
|
118
|
-
};
|
|
119
|
-
ws.onclose = (ev) => {
|
|
120
|
-
if (this.disposed)
|
|
121
|
-
return;
|
|
122
|
-
// 正常終了 (1000) の場合はリコネクトしない
|
|
123
|
-
if (ev.code === 1000) {
|
|
124
|
-
console.log(`[ReconnectableWS] 🔒 Closed cleanly (code=${ev.code})`);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
console.log(`[ReconnectableWS] ❌ Closed (code=${ev.code} reason=${ev.reason})`);
|
|
128
|
-
this.stopHeartbeat();
|
|
129
|
-
this.scheduleReconnect();
|
|
130
|
-
};
|
|
131
|
-
ws.onerror = () => {
|
|
132
|
-
if (this.disposed)
|
|
133
|
-
return;
|
|
134
|
-
console.log(`[ReconnectableWS] ⚠️ Error`);
|
|
135
|
-
// onclose が後続で発火するため、ここではリコネクトをトリガーしない
|
|
136
|
-
};
|
|
137
|
-
ws.onmessage = (ev) => {
|
|
138
|
-
if (this.disposed)
|
|
139
|
-
return;
|
|
140
|
-
const data = ev.data;
|
|
141
|
-
// Heartbeat pong のフィルタリング
|
|
142
|
-
if (data === '__pong') {
|
|
143
|
-
this.handlePong();
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
// 通常メッセージをハンドラに配信
|
|
147
|
-
for (const handler of this.messageHandlers) {
|
|
148
|
-
handler(ev);
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
this.ws = ws;
|
|
152
|
-
}
|
|
153
|
-
setState(state) {
|
|
154
|
-
if (this._state === state)
|
|
155
|
-
return;
|
|
156
|
-
this._state = state;
|
|
157
|
-
console.log(`[ReconnectableWS] 📡 State: ${state}`);
|
|
158
|
-
this.opts.onConnectionStateChange?.(state);
|
|
159
|
-
}
|
|
160
|
-
scheduleReconnect() {
|
|
161
|
-
if (this.disposed)
|
|
162
|
-
return;
|
|
163
|
-
if (this.reconnectAttempt >= this.opts.maxReconnectAttempts) {
|
|
164
|
-
console.log(`[ReconnectableWS] 🚫 Max reconnect attempts reached (${this.opts.maxReconnectAttempts})`);
|
|
165
|
-
this.setState('disconnected');
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
this.setState('reconnecting');
|
|
169
|
-
const delay = this.getReconnectDelay();
|
|
170
|
-
console.log(`[ReconnectableWS] ⏳ Reconnecting in ${Math.round(delay)}ms (attempt=${this.reconnectAttempt + 1})`);
|
|
171
|
-
this.reconnectTimer = setTimeout(() => {
|
|
172
|
-
this.reconnectAttempt++;
|
|
173
|
-
this.connect();
|
|
174
|
-
}, delay);
|
|
175
|
-
}
|
|
176
|
-
getReconnectDelay() {
|
|
177
|
-
const { baseDelay, maxDelay, jitterFactor } = this.opts;
|
|
178
|
-
const exponentialDelay = Math.min(baseDelay * Math.pow(2, this.reconnectAttempt), maxDelay);
|
|
179
|
-
const jitter = exponentialDelay * jitterFactor * (Math.random() * 2 - 1);
|
|
180
|
-
return Math.max(0, exponentialDelay + jitter);
|
|
181
|
-
}
|
|
182
|
-
flushBuffer() {
|
|
183
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
184
|
-
return;
|
|
185
|
-
const buffer = this.messageBuffer;
|
|
186
|
-
this.messageBuffer = [];
|
|
187
|
-
for (const data of buffer) {
|
|
188
|
-
try {
|
|
189
|
-
this.ws.send(data);
|
|
190
|
-
}
|
|
191
|
-
catch (e) {
|
|
192
|
-
console.warn(`[ReconnectableWS] ⚠️ Failed to flush buffered message:`, e);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (buffer.length > 0) {
|
|
196
|
-
console.log(`[ReconnectableWS] 📤 Flushed ${buffer.length} buffered messages`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// ─── Heartbeat ───────────────────────────────────────────
|
|
200
|
-
startHeartbeat() {
|
|
201
|
-
this.stopHeartbeat();
|
|
202
|
-
this.heartbeatTimer = setInterval(() => {
|
|
203
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
204
|
-
this.ws.send('__ping');
|
|
205
|
-
this.heartbeatTimeoutTimer = setTimeout(() => {
|
|
206
|
-
console.log(`[ReconnectableWS] 💔 Heartbeat timeout`);
|
|
207
|
-
// 強制切断 → onclose がリコネクトをトリガー
|
|
208
|
-
this.ws?.close(4001, 'heartbeat_timeout');
|
|
209
|
-
}, this.opts.heartbeatTimeout);
|
|
210
|
-
}
|
|
211
|
-
}, this.opts.heartbeatInterval);
|
|
212
|
-
}
|
|
213
|
-
stopHeartbeat() {
|
|
214
|
-
if (this.heartbeatTimer) {
|
|
215
|
-
clearInterval(this.heartbeatTimer);
|
|
216
|
-
this.heartbeatTimer = null;
|
|
217
|
-
}
|
|
218
|
-
if (this.heartbeatTimeoutTimer) {
|
|
219
|
-
clearTimeout(this.heartbeatTimeoutTimer);
|
|
220
|
-
this.heartbeatTimeoutTimer = null;
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
handlePong() {
|
|
224
|
-
if (this.heartbeatTimeoutTimer) {
|
|
225
|
-
clearTimeout(this.heartbeatTimeoutTimer);
|
|
226
|
-
this.heartbeatTimeoutTimer = null;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
package/dist/room.d.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
type RoomMessageHandler = (data: Record<string, unknown>) => void;
|
|
2
|
-
/** WebSocket 互換の send/addEventListener インターフェース */
|
|
3
|
-
interface WebSocketLike {
|
|
4
|
-
send(data: string): void;
|
|
5
|
-
addEventListener(type: 'message', handler: (ev: MessageEvent) => void): void;
|
|
6
|
-
}
|
|
7
|
-
/** Room の公開インターフェース。WebSocket / BroadcastChannel どちらでも実装可能。 */
|
|
8
|
-
export interface RoomLike {
|
|
9
|
-
readonly myId: string;
|
|
10
|
-
broadcast(msg: Record<string, unknown>): void;
|
|
11
|
-
send(id: string, msg: Record<string, unknown>): void;
|
|
12
|
-
on(type: string, handler: RoomMessageHandler): void;
|
|
13
|
-
}
|
|
14
|
-
export declare class Room implements RoomLike {
|
|
15
|
-
readonly myId: string;
|
|
16
|
-
private ws;
|
|
17
|
-
private handlers;
|
|
18
|
-
constructor(ws: WebSocketLike, myId: string);
|
|
19
|
-
broadcast(msg: Record<string, unknown>): void;
|
|
20
|
-
send(id: string, msg: Record<string, unknown>): void;
|
|
21
|
-
on(type: string, handler: RoomMessageHandler): void;
|
|
22
|
-
}
|
|
23
|
-
export {};
|
package/dist/room.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
export class Room {
|
|
2
|
-
constructor(ws, myId) {
|
|
3
|
-
this.handlers = new Map();
|
|
4
|
-
this.ws = ws;
|
|
5
|
-
this.myId = myId;
|
|
6
|
-
this.ws.addEventListener('message', (ev) => {
|
|
7
|
-
let parsed;
|
|
8
|
-
try {
|
|
9
|
-
parsed = JSON.parse(ev.data);
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const type = parsed.type;
|
|
15
|
-
if (!type)
|
|
16
|
-
return;
|
|
17
|
-
const from = parsed.__from;
|
|
18
|
-
console.log(`[SDK Room] ⬅ recv type=${type} from=${from}`, JSON.stringify(parsed));
|
|
19
|
-
const handlers = this.handlers.get(type) || [];
|
|
20
|
-
handlers.forEach((h) => h({ ...parsed, __from: from }));
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
broadcast(msg) {
|
|
24
|
-
console.log(`[SDK Room] ➡ broadcast type=${msg.type}`, JSON.stringify(msg));
|
|
25
|
-
this.ws.send(JSON.stringify(msg));
|
|
26
|
-
}
|
|
27
|
-
send(id, msg) {
|
|
28
|
-
console.log(`[SDK Room] ➡ send to=${id} type=${msg.type}`, JSON.stringify(msg));
|
|
29
|
-
this.ws.send(JSON.stringify({ ...msg, __to: id }));
|
|
30
|
-
}
|
|
31
|
-
on(type, handler) {
|
|
32
|
-
if (!this.handlers.has(type))
|
|
33
|
-
this.handlers.set(type, []);
|
|
34
|
-
this.handlers.get(type).push(handler);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* URL の roster パラメータ (`?players=` / 旧名 `?seats=`) の解決を固定する unit test。
|
|
3
|
-
*
|
|
4
|
-
* ホスト (mobile / uzutokyo / emulator / dev harness) と SDK の間はデプロイが揃わない。
|
|
5
|
-
* 新旧どちらの名前で渡されても run() が起動し、GameRoom へは新名で送り直すところまでを
|
|
6
|
-
* 1 本で見る。旧名しか読めない SDK / 旧名しか送らないホストが同時に居るのが移行期の前提。
|
|
7
|
-
*/
|
|
8
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
9
|
-
const logic = {
|
|
10
|
-
setup: () => ({ moves: 0 }),
|
|
11
|
-
actions: {},
|
|
12
|
-
update: () => { },
|
|
13
|
-
};
|
|
14
|
-
const roster = [
|
|
15
|
-
{ id: 'p1', nickname: 'P1', iconUrl: '' },
|
|
16
|
-
{ id: 'p2', nickname: 'P2', iconUrl: '' },
|
|
17
|
-
];
|
|
18
|
-
const wire = (seats) => JSON.stringify(seats.map((s) => ({ id: s.id, name: s.nickname, iconUrl: s.iconUrl })));
|
|
19
|
-
/** `new WebSocket(url)` を捕まえるスタブ。接続先 URL だけ見る。 */
|
|
20
|
-
const stubWebSocket = () => {
|
|
21
|
-
const urls = [];
|
|
22
|
-
class FakeWebSocket {
|
|
23
|
-
constructor(url) {
|
|
24
|
-
this.url = url;
|
|
25
|
-
this.onopen = null;
|
|
26
|
-
this.onclose = null;
|
|
27
|
-
this.onerror = null;
|
|
28
|
-
this.onmessage = null;
|
|
29
|
-
this.readyState = FakeWebSocket.OPEN;
|
|
30
|
-
urls.push(url);
|
|
31
|
-
}
|
|
32
|
-
send() { }
|
|
33
|
-
close() { }
|
|
34
|
-
}
|
|
35
|
-
FakeWebSocket.OPEN = 1;
|
|
36
|
-
vi.stubGlobal('WebSocket', FakeWebSocket);
|
|
37
|
-
return urls;
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* 指定のクエリで SDK を読み込み直して `run()` まで通す。
|
|
41
|
-
*
|
|
42
|
-
* `isHosted` は module 評価時に一度だけ決まるので、`FlutterHost` を先に生やしてから
|
|
43
|
-
* import しないと run() が即 return して何も起きない。
|
|
44
|
-
*/
|
|
45
|
-
const runWithQuery = async (query) => {
|
|
46
|
-
vi.resetModules();
|
|
47
|
-
vi.stubGlobal('FlutterHost', { postMessage: () => { } });
|
|
48
|
-
const urls = stubWebSocket();
|
|
49
|
-
window.history.replaceState({}, '', `/?${query}`);
|
|
50
|
-
const sdk = await import('./index.js');
|
|
51
|
-
sdk.run({ logic, playerCount: 2, onState: () => { }, inputs: () => { } });
|
|
52
|
-
return urls;
|
|
53
|
-
};
|
|
54
|
-
const baseQuery = 'roomId=room1&server=ws://localhost:1234&revisionId=rev1&seatId=p1';
|
|
55
|
-
afterEach(() => {
|
|
56
|
-
vi.unstubAllGlobals();
|
|
57
|
-
});
|
|
58
|
-
describe('roster パラメータの解決', () => {
|
|
59
|
-
/** 新ホストは `players=` で送る。 */
|
|
60
|
-
it('players から roster を読む', async () => {
|
|
61
|
-
const urls = await runWithQuery(`${baseQuery}&players=${encodeURIComponent(wire(roster))}`);
|
|
62
|
-
const sent = new URL(urls[0]).searchParams.get('players');
|
|
63
|
-
expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
|
|
64
|
-
});
|
|
65
|
-
/**
|
|
66
|
-
* 移行期の互換措置。デプロイ前のホストは旧名でしか送ってこない。読めないと run() の
|
|
67
|
-
* 入口で throw してシナリオが起動しない。
|
|
68
|
-
*/
|
|
69
|
-
it('players が無ければ旧名 seats に落ちる', async () => {
|
|
70
|
-
const urls = await runWithQuery(`${baseQuery}&seats=${encodeURIComponent(wire(roster))}`);
|
|
71
|
-
const sent = new URL(urls[0]).searchParams.get('players');
|
|
72
|
-
expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
|
|
73
|
-
});
|
|
74
|
-
/** 移行期は両方載せたホストが居る。新名を優先し、旧名は fallback にしか使わない。 */
|
|
75
|
-
it('両方あれば players を採る', async () => {
|
|
76
|
-
const old = [{ id: 'old1', nickname: 'Old1', iconUrl: '' }];
|
|
77
|
-
const urls = await runWithQuery(`${baseQuery}&players=${encodeURIComponent(wire(roster))}` +
|
|
78
|
-
`&seats=${encodeURIComponent(wire(old))}`);
|
|
79
|
-
const sent = new URL(urls[0]).searchParams.get('players');
|
|
80
|
-
expect(JSON.parse(sent ?? '[]').map((s) => s.id)).toEqual(['p1', 'p2']);
|
|
81
|
-
});
|
|
82
|
-
/** roster がどちらの名前でも無ければ、黙って空 roster で始めず入口で落とす。 */
|
|
83
|
-
it('どちらも無ければ throw する', async () => {
|
|
84
|
-
await expect(runWithQuery(baseQuery)).rejects.toThrow(/players is required/);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @docs
|
|
3
|
-
* - ServerAction仕様: docs/docs/uzu_code/connection-method/arch3-authority.md
|
|
4
|
-
* - SDK仕様: docs/docs/uzu_code/play-screen-sdk.md
|
|
5
|
-
*
|
|
6
|
-
* ブラウザ内 LocalGameRoom — GameRoom DO の動作をサーバーレスで再現する。
|
|
7
|
-
* server_url がない場合に vite dev だけで ServerAction シナリオをローカル実行する。
|
|
8
|
-
*
|
|
9
|
-
* online (`runOnlineServerAction`) と挙動を揃えるため、`logic.actions` は同期で実行し
|
|
10
|
-
* `sendAction()` 直後に `onState()` を同期発火する (楽観的更新だけで完結する仕様)。
|
|
11
|
-
* `logic.serverActions` は `Promise<void>` を返しうるので `await` で実行し、完了後に
|
|
12
|
-
* `onState()` を呼ぶ。同名なら actions → serverActions の順で走る (サーバーと同じ)。
|
|
13
|
-
*/
|
|
14
|
-
import type { GameConfig } from '../types.js';
|
|
15
|
-
import type { RunHandle } from '../dev-hooks.js';
|
|
16
|
-
export declare function runLocalServerAction<S>(config: GameConfig<S>): RunHandle<S>;
|