p2p-lockstep-kit-session 0.1.7 → 0.1.9
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/session/index.js +492 -119
- package/dist/session/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/serialization-smoke.mjs +425 -59
package/dist/session/index.js
CHANGED
|
@@ -2,6 +2,53 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
+
// utils/logger.ts
|
|
6
|
+
var logWith = (level) => (message, meta) => {
|
|
7
|
+
if (meta !== void 0) {
|
|
8
|
+
console[level](message, meta);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
console[level](message);
|
|
12
|
+
};
|
|
13
|
+
var consoleLogger = {
|
|
14
|
+
debug: logWith("debug"),
|
|
15
|
+
info: logWith("info"),
|
|
16
|
+
warn: logWith("warn"),
|
|
17
|
+
error: logWith("error")
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// utils/serialization/json.ts
|
|
21
|
+
var decodeSafe = (raw) => {
|
|
22
|
+
if (typeof raw !== "string") {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: new TypeError("decodeSafe expects a serialized string")
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
return { ok: false, error };
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// utils/protocol/session.ts
|
|
36
|
+
var parseSessionMessage = (data) => {
|
|
37
|
+
if (typeof data !== "string") {
|
|
38
|
+
if (!data || typeof data !== "object") {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
const decoded = decodeSafe(
|
|
44
|
+
data
|
|
45
|
+
);
|
|
46
|
+
if (!decoded.ok || !decoded.value || typeof decoded.value !== "object") {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return decoded.value;
|
|
50
|
+
};
|
|
51
|
+
|
|
5
52
|
// session/commandBus.ts
|
|
6
53
|
var CommandBus = class {
|
|
7
54
|
constructor() {
|
|
@@ -13,17 +60,31 @@ var CommandBus = class {
|
|
|
13
60
|
}
|
|
14
61
|
register(type, handler) {
|
|
15
62
|
this.handlers[type] = handler;
|
|
63
|
+
consoleLogger.debug(`[session:bus] registered ${type}`);
|
|
16
64
|
}
|
|
17
65
|
dispatch(message) {
|
|
18
66
|
this.processingQueue = this.processingQueue.then(async () => {
|
|
67
|
+
consoleLogger.debug(`[session:bus] dispatch ${message.type}`, {
|
|
68
|
+
from: message.from,
|
|
69
|
+
payload: message.payload,
|
|
70
|
+
turn: message.turn,
|
|
71
|
+
sid: message.sid
|
|
72
|
+
});
|
|
19
73
|
const handler = this.handlers[message.type];
|
|
20
74
|
if (handler) {
|
|
21
75
|
try {
|
|
22
76
|
await handler(message);
|
|
77
|
+
consoleLogger.debug(`[session:bus] handled ${message.type}`, {
|
|
78
|
+
from: message.from
|
|
79
|
+
});
|
|
23
80
|
} catch (err) {
|
|
24
81
|
console.error(`[CommandBus] Error in ${message.type}:`, err);
|
|
25
82
|
}
|
|
83
|
+
return;
|
|
26
84
|
}
|
|
85
|
+
consoleLogger.debug(`[session:bus] no handler for ${message.type}`, {
|
|
86
|
+
from: message.from
|
|
87
|
+
});
|
|
27
88
|
});
|
|
28
89
|
}
|
|
29
90
|
};
|
|
@@ -57,11 +118,12 @@ var transitions = [
|
|
|
57
118
|
{ from: "remote_turn", event: "REMOTE_UNDO", to: "approving" },
|
|
58
119
|
{ from: "turn", event: "REMOTE_RESTART", to: "approving" },
|
|
59
120
|
{ from: "remote_turn", event: "REMOTE_RESTART", to: "approving" },
|
|
60
|
-
// Approval outcomes when we were waiting
|
|
121
|
+
// Approval outcomes when we were waiting for the peer.
|
|
61
122
|
{ from: "waiting_approval", event: "APPROVE", to: "turn" },
|
|
62
123
|
{ from: "waiting_approval", event: "REJECT", to: "turn" },
|
|
63
124
|
{ from: "waiting_approval", event: "REJECT", to: "remote_turn" },
|
|
64
|
-
// Approval outcomes when we were confirming
|
|
125
|
+
// Approval outcomes when we were confirming the peer's request.
|
|
126
|
+
// The target turn is explicit because resumeTurn decides who continues.
|
|
65
127
|
{ from: "approving", event: "APPROVE", to: "remote_turn" },
|
|
66
128
|
{ from: "approving", event: "REJECT", to: "remote_turn" },
|
|
67
129
|
{ from: "approving", event: "REJECT", to: "turn" },
|
|
@@ -257,11 +319,21 @@ var UINotificationAdapter = class {
|
|
|
257
319
|
if (this.lastNotificationTime + this.notificationThrottleMs > now) return;
|
|
258
320
|
this.lastNotificationTime = now;
|
|
259
321
|
const snapshot = buildGameStateSnapshot(this.stateRef, this.getConnected());
|
|
322
|
+
consoleLogger.debug("[session:observer] state snapshot", {
|
|
323
|
+
local: snapshot.localState,
|
|
324
|
+
remote: snapshot.remoteState,
|
|
325
|
+
turn: snapshot.turn,
|
|
326
|
+
history: snapshot.history.length,
|
|
327
|
+
pending: snapshot.pendingAction,
|
|
328
|
+
connected: snapshot.connected
|
|
329
|
+
});
|
|
260
330
|
this.uiObserver.notifyStateChange(snapshot);
|
|
261
331
|
}
|
|
262
332
|
onHistoryChanged() {
|
|
333
|
+
this.onStateChanged();
|
|
263
334
|
}
|
|
264
335
|
onGameReset() {
|
|
336
|
+
this.onStateChanged();
|
|
265
337
|
}
|
|
266
338
|
emitEvent(event) {
|
|
267
339
|
this.uiObserver.notifyGameEvent(event);
|
|
@@ -299,6 +371,7 @@ var State = class {
|
|
|
299
371
|
if (remoteId) {
|
|
300
372
|
this.remoteId = remoteId;
|
|
301
373
|
}
|
|
374
|
+
consoleLogger.debug("[session:state] created", { localId: id, remoteId });
|
|
302
375
|
}
|
|
303
376
|
/**
|
|
304
377
|
* Register an internal observer (like plugin pattern)
|
|
@@ -316,6 +389,7 @@ var State = class {
|
|
|
316
389
|
}
|
|
317
390
|
setremoteId(id) {
|
|
318
391
|
this.remoteId = id;
|
|
392
|
+
consoleLogger.debug("[session:state] remote id set", { remoteId: id });
|
|
319
393
|
}
|
|
320
394
|
getState(player) {
|
|
321
395
|
return this.getPlayerFsm(player).getState();
|
|
@@ -327,6 +401,7 @@ var State = class {
|
|
|
327
401
|
return this.history.slice();
|
|
328
402
|
}
|
|
329
403
|
replaceHistory(entries) {
|
|
404
|
+
consoleLogger.debug("[session:history] replace", { count: entries.length });
|
|
330
405
|
this.clearHistory();
|
|
331
406
|
entries.forEach((entry) => {
|
|
332
407
|
this.pushHistory({
|
|
@@ -337,15 +412,33 @@ var State = class {
|
|
|
337
412
|
});
|
|
338
413
|
}
|
|
339
414
|
clearHistory() {
|
|
415
|
+
const count = this.history.length;
|
|
340
416
|
this.history.splice(0, this.history.length);
|
|
417
|
+
consoleLogger.debug("[session:history] clear", { count });
|
|
341
418
|
this.notifyHistoryChanged();
|
|
342
419
|
}
|
|
343
420
|
pushHistory(entry) {
|
|
344
421
|
this.history.push(entry);
|
|
422
|
+
consoleLogger.debug("[session:history] push", {
|
|
423
|
+
turn: entry.turn,
|
|
424
|
+
player: entry.player,
|
|
425
|
+
move: entry.move,
|
|
426
|
+
count: this.history.length
|
|
427
|
+
});
|
|
345
428
|
this.notifyHistoryChanged();
|
|
346
429
|
}
|
|
347
430
|
popHistory() {
|
|
348
|
-
|
|
431
|
+
const entry = this.history.pop() ?? null;
|
|
432
|
+
if (entry) {
|
|
433
|
+
consoleLogger.debug("[session:history] pop", {
|
|
434
|
+
turn: entry.turn,
|
|
435
|
+
player: entry.player,
|
|
436
|
+
move: entry.move,
|
|
437
|
+
count: this.history.length
|
|
438
|
+
});
|
|
439
|
+
this.notifyHistoryChanged();
|
|
440
|
+
}
|
|
441
|
+
return entry;
|
|
349
442
|
}
|
|
350
443
|
canAction(player, action) {
|
|
351
444
|
return this.getPlayerFsm(player).hasNextState(action);
|
|
@@ -358,28 +451,56 @@ var State = class {
|
|
|
358
451
|
* so we automatically find and apply it.
|
|
359
452
|
*/
|
|
360
453
|
dispatch(player, action, to) {
|
|
454
|
+
const before = this.getState(player);
|
|
361
455
|
this.getPlayerFsm(player).dispatch(action, to);
|
|
456
|
+
const after = this.getState(player);
|
|
457
|
+
consoleLogger.debug(`[session:fsm] ${player} ${action}`, {
|
|
458
|
+
from: before,
|
|
459
|
+
to: after,
|
|
460
|
+
requested: to,
|
|
461
|
+
local: this.getState("local"),
|
|
462
|
+
remote: this.getState("remote"),
|
|
463
|
+
turn: this.getTurnCount(),
|
|
464
|
+
history: this.history.length,
|
|
465
|
+
pending: this.pendingAction
|
|
466
|
+
});
|
|
362
467
|
this.notifyStateChanged();
|
|
363
468
|
}
|
|
364
469
|
setPendingAction(action) {
|
|
470
|
+
consoleLogger.debug("[session:state] pending action set", {
|
|
471
|
+
from: this.pendingAction,
|
|
472
|
+
to: action
|
|
473
|
+
});
|
|
365
474
|
this.pendingAction = action;
|
|
366
475
|
}
|
|
367
476
|
getPendingAction() {
|
|
368
477
|
return this.pendingAction;
|
|
369
478
|
}
|
|
370
479
|
setPendingUndoCount(count) {
|
|
480
|
+
consoleLogger.debug("[session:state] pending undo count set", {
|
|
481
|
+
from: this.pendingUndoCount,
|
|
482
|
+
to: count
|
|
483
|
+
});
|
|
371
484
|
this.pendingUndoCount = count;
|
|
372
485
|
}
|
|
373
486
|
getPendingUndoCount() {
|
|
374
487
|
return this.pendingUndoCount;
|
|
375
488
|
}
|
|
376
489
|
setLastStart(player) {
|
|
490
|
+
consoleLogger.debug("[session:state] last start set", {
|
|
491
|
+
from: this.lastStart,
|
|
492
|
+
to: player
|
|
493
|
+
});
|
|
377
494
|
this.lastStart = player;
|
|
378
495
|
}
|
|
379
496
|
getLastStart() {
|
|
380
497
|
return this.lastStart;
|
|
381
498
|
}
|
|
382
499
|
setResumeTurn(player) {
|
|
500
|
+
consoleLogger.debug("[session:state] resume turn set", {
|
|
501
|
+
from: this.resumeTurn,
|
|
502
|
+
to: player
|
|
503
|
+
});
|
|
383
504
|
this.resumeTurn = player;
|
|
384
505
|
}
|
|
385
506
|
getResumeTurn() {
|
|
@@ -389,27 +510,60 @@ var State = class {
|
|
|
389
510
|
return player === "local" ? this.local : this.remote;
|
|
390
511
|
}
|
|
391
512
|
notifyStateChanged() {
|
|
513
|
+
consoleLogger.debug("[session:state] notify state changed", {
|
|
514
|
+
local: this.getState("local"),
|
|
515
|
+
remote: this.getState("remote"),
|
|
516
|
+
turn: this.getTurnCount(),
|
|
517
|
+
history: this.history.length,
|
|
518
|
+
pending: this.pendingAction
|
|
519
|
+
});
|
|
392
520
|
this.stateObserverManager.notifyStateChanged();
|
|
393
521
|
}
|
|
394
522
|
notifyHistoryChanged() {
|
|
523
|
+
consoleLogger.debug("[session:state] notify history changed", {
|
|
524
|
+
turn: this.getTurnCount(),
|
|
525
|
+
history: this.history.length,
|
|
526
|
+
pending: this.pendingAction
|
|
527
|
+
});
|
|
395
528
|
this.stateObserverManager.notifyHistoryChanged();
|
|
396
529
|
}
|
|
397
530
|
notifyGameReset() {
|
|
531
|
+
consoleLogger.debug("[session:state] notify game reset");
|
|
398
532
|
this.stateObserverManager.notifyGameReset();
|
|
399
533
|
}
|
|
400
534
|
dispatchPair(localAction, localTo, remoteAction, remoteTo) {
|
|
535
|
+
const before = {
|
|
536
|
+
local: this.local.getState(),
|
|
537
|
+
remote: this.remote.getState()
|
|
538
|
+
};
|
|
401
539
|
this.local.dispatch(localAction, localTo);
|
|
402
540
|
this.remote.dispatch(remoteAction, remoteTo);
|
|
541
|
+
consoleLogger.debug("[session:fsm] pair dispatch", {
|
|
542
|
+
before,
|
|
543
|
+
after: {
|
|
544
|
+
local: this.local.getState(),
|
|
545
|
+
remote: this.remote.getState()
|
|
546
|
+
},
|
|
547
|
+
localAction,
|
|
548
|
+
localTo,
|
|
549
|
+
remoteAction,
|
|
550
|
+
remoteTo,
|
|
551
|
+
turn: this.getTurnCount(),
|
|
552
|
+
history: this.history.length,
|
|
553
|
+
pending: this.pendingAction
|
|
554
|
+
});
|
|
403
555
|
this.notifyStateChanged();
|
|
404
556
|
}
|
|
405
557
|
saveGameSnapshot(snapshot) {
|
|
406
558
|
this.gameSnapshot = snapshot;
|
|
559
|
+
consoleLogger.debug("[session:state] game snapshot saved", { snapshot });
|
|
407
560
|
}
|
|
408
561
|
getGameSnapshot() {
|
|
409
562
|
return this.gameSnapshot;
|
|
410
563
|
}
|
|
411
564
|
clearGameSnapshot() {
|
|
412
565
|
this.gameSnapshot = null;
|
|
566
|
+
consoleLogger.debug("[session:state] game snapshot cleared");
|
|
413
567
|
}
|
|
414
568
|
/**
|
|
415
569
|
* Check if there's a pending action (undo/restart)
|
|
@@ -421,6 +575,11 @@ var State = class {
|
|
|
421
575
|
* Clear all pending states (called after approval/rejection)
|
|
422
576
|
*/
|
|
423
577
|
clearPendingStates() {
|
|
578
|
+
consoleLogger.debug("[session:state] pending states cleared", {
|
|
579
|
+
pending: this.pendingAction,
|
|
580
|
+
pendingUndoCount: this.pendingUndoCount,
|
|
581
|
+
resumeTurn: this.resumeTurn
|
|
582
|
+
});
|
|
424
583
|
this.pendingAction = null;
|
|
425
584
|
this.pendingUndoCount = null;
|
|
426
585
|
this.resumeTurn = null;
|
|
@@ -433,6 +592,10 @@ var State = class {
|
|
|
433
592
|
this.pendingAction = "undo";
|
|
434
593
|
this.pendingUndoCount = undoCount;
|
|
435
594
|
this.resumeTurn = resumeTurn;
|
|
595
|
+
consoleLogger.debug("[session:state] undo request initialized", {
|
|
596
|
+
undoCount,
|
|
597
|
+
resumeTurn
|
|
598
|
+
});
|
|
436
599
|
}
|
|
437
600
|
/**
|
|
438
601
|
* Initialize restart request with resume turn
|
|
@@ -440,6 +603,9 @@ var State = class {
|
|
|
440
603
|
initializeRestartRequest(resumeTurn) {
|
|
441
604
|
this.pendingAction = "restart";
|
|
442
605
|
this.resumeTurn = resumeTurn;
|
|
606
|
+
consoleLogger.debug("[session:state] restart request initialized", {
|
|
607
|
+
resumeTurn
|
|
608
|
+
});
|
|
443
609
|
}
|
|
444
610
|
/**
|
|
445
611
|
* Check if pending action is undo
|
|
@@ -457,6 +623,7 @@ var State = class {
|
|
|
457
623
|
* Apply undo by popping history N times
|
|
458
624
|
*/
|
|
459
625
|
applyUndo(count = 1) {
|
|
626
|
+
consoleLogger.debug("[session:history] apply undo", { count });
|
|
460
627
|
for (let i = 0; i < count; i++) {
|
|
461
628
|
this.popHistory();
|
|
462
629
|
}
|
|
@@ -465,6 +632,13 @@ var State = class {
|
|
|
465
632
|
* Reset game state to initial (for restart)
|
|
466
633
|
*/
|
|
467
634
|
resetGame() {
|
|
635
|
+
consoleLogger.debug("[session:state] reset game", {
|
|
636
|
+
local: this.getState("local"),
|
|
637
|
+
remote: this.getState("remote"),
|
|
638
|
+
history: this.history.length,
|
|
639
|
+
lastStart: this.lastStart,
|
|
640
|
+
pending: this.pendingAction
|
|
641
|
+
});
|
|
468
642
|
this.clearHistory();
|
|
469
643
|
this.local = new SessionFsm("idle");
|
|
470
644
|
this.remote = new SessionFsm("idle");
|
|
@@ -480,6 +654,7 @@ var State = class {
|
|
|
480
654
|
*/
|
|
481
655
|
recordStartPlayer(player) {
|
|
482
656
|
this.lastStart = player;
|
|
657
|
+
consoleLogger.debug("[session:state] start player recorded", { player });
|
|
483
658
|
}
|
|
484
659
|
/**
|
|
485
660
|
* Get move to undo from history
|
|
@@ -517,6 +692,11 @@ var State = class {
|
|
|
517
692
|
* Determines who plays first based on starter parameter
|
|
518
693
|
*/
|
|
519
694
|
dispatchStart(firstPlayer) {
|
|
695
|
+
const before = {
|
|
696
|
+
local: this.local.getState(),
|
|
697
|
+
remote: this.remote.getState(),
|
|
698
|
+
lastStart: this.lastStart
|
|
699
|
+
};
|
|
520
700
|
if (firstPlayer === "local") {
|
|
521
701
|
this.local.dispatch("START", "turn");
|
|
522
702
|
this.remote.dispatch("START", "remote_turn");
|
|
@@ -526,18 +706,38 @@ var State = class {
|
|
|
526
706
|
this.remote.dispatch("START", "turn");
|
|
527
707
|
this.lastStart = "remote";
|
|
528
708
|
}
|
|
709
|
+
consoleLogger.debug("[session:fsm] start dispatch", {
|
|
710
|
+
before,
|
|
711
|
+
firstPlayer,
|
|
712
|
+
after: {
|
|
713
|
+
local: this.local.getState(),
|
|
714
|
+
remote: this.remote.getState(),
|
|
715
|
+
lastStart: this.lastStart
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
this.notifyStateChanged();
|
|
529
719
|
}
|
|
530
720
|
/**
|
|
531
721
|
* Dispatch SYNC_COMPLETE with automatic target state resolution
|
|
532
722
|
* Based on who should have the turn after sync
|
|
533
723
|
*/
|
|
534
724
|
dispatchSyncComplete(nextPlayer) {
|
|
535
|
-
this.resumeTurn = nextPlayer;
|
|
536
725
|
if (nextPlayer === "local") {
|
|
537
|
-
this.dispatchPair(
|
|
726
|
+
this.dispatchPair(
|
|
727
|
+
"SYNC_COMPLETE",
|
|
728
|
+
"turn",
|
|
729
|
+
"SYNC_COMPLETE",
|
|
730
|
+
"remote_turn"
|
|
731
|
+
);
|
|
538
732
|
} else {
|
|
539
|
-
this.dispatchPair(
|
|
733
|
+
this.dispatchPair(
|
|
734
|
+
"SYNC_COMPLETE",
|
|
735
|
+
"remote_turn",
|
|
736
|
+
"SYNC_COMPLETE",
|
|
737
|
+
"turn"
|
|
738
|
+
);
|
|
540
739
|
}
|
|
740
|
+
this.resumeTurn = null;
|
|
541
741
|
}
|
|
542
742
|
// ===== Game Plugin Integration (Proxy Pattern) =====
|
|
543
743
|
/**
|
|
@@ -546,6 +746,10 @@ var State = class {
|
|
|
546
746
|
*/
|
|
547
747
|
setGamePlugin(plugin) {
|
|
548
748
|
this.gamePlugin = plugin;
|
|
749
|
+
consoleLogger.debug("[session:plugin] game plugin set", {
|
|
750
|
+
hasInitialize: Boolean(plugin.initialize),
|
|
751
|
+
hasCleanup: Boolean(plugin.cleanup)
|
|
752
|
+
});
|
|
549
753
|
if (plugin.initialize) {
|
|
550
754
|
plugin.initialize();
|
|
551
755
|
}
|
|
@@ -564,7 +768,16 @@ var State = class {
|
|
|
564
768
|
*/
|
|
565
769
|
validateMove(move2) {
|
|
566
770
|
const gameState = this.buildGameState();
|
|
567
|
-
|
|
771
|
+
const result = this.gamePlugin.validateMove(move2, gameState);
|
|
772
|
+
consoleLogger.debug("[session:plugin] validate move", {
|
|
773
|
+
move: move2,
|
|
774
|
+
result,
|
|
775
|
+
local: gameState.localState,
|
|
776
|
+
remote: gameState.remoteState,
|
|
777
|
+
turn: gameState.turn,
|
|
778
|
+
history: gameState.history.length
|
|
779
|
+
});
|
|
780
|
+
return result;
|
|
568
781
|
}
|
|
569
782
|
/**
|
|
570
783
|
* Check if game has ended (someone won)
|
|
@@ -573,7 +786,13 @@ var State = class {
|
|
|
573
786
|
*/
|
|
574
787
|
checkWin() {
|
|
575
788
|
const gameState = this.buildGameState();
|
|
576
|
-
|
|
789
|
+
const winner = this.gamePlugin.checkWin(gameState, this.getHistory());
|
|
790
|
+
consoleLogger.debug("[session:plugin] check win", {
|
|
791
|
+
winner,
|
|
792
|
+
turn: gameState.turn,
|
|
793
|
+
history: gameState.history.length
|
|
794
|
+
});
|
|
795
|
+
return winner;
|
|
577
796
|
}
|
|
578
797
|
/**
|
|
579
798
|
* Cleanup when game ends (for plugin to reset internal state)
|
|
@@ -582,6 +801,7 @@ var State = class {
|
|
|
582
801
|
if (this.gamePlugin.cleanup) {
|
|
583
802
|
this.gamePlugin.cleanup();
|
|
584
803
|
}
|
|
804
|
+
consoleLogger.debug("[session:plugin] cleanup game");
|
|
585
805
|
}
|
|
586
806
|
/**
|
|
587
807
|
* Build game state for plugin
|
|
@@ -598,53 +818,6 @@ var State = class {
|
|
|
598
818
|
}
|
|
599
819
|
};
|
|
600
820
|
|
|
601
|
-
// utils/logger.ts
|
|
602
|
-
var logWith = (level) => (message, meta) => {
|
|
603
|
-
if (meta !== void 0) {
|
|
604
|
-
console[level](message, meta);
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
console[level](message);
|
|
608
|
-
};
|
|
609
|
-
var consoleLogger = {
|
|
610
|
-
debug: logWith("debug"),
|
|
611
|
-
info: logWith("info"),
|
|
612
|
-
warn: logWith("warn"),
|
|
613
|
-
error: logWith("error")
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
// utils/serialization/json.ts
|
|
617
|
-
var decodeSafe = (raw) => {
|
|
618
|
-
if (typeof raw !== "string") {
|
|
619
|
-
return {
|
|
620
|
-
ok: false,
|
|
621
|
-
error: new TypeError("decodeSafe expects a serialized string")
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
try {
|
|
625
|
-
return { ok: true, value: JSON.parse(raw) };
|
|
626
|
-
} catch (error) {
|
|
627
|
-
return { ok: false, error };
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
|
|
631
|
-
// utils/protocol/session.ts
|
|
632
|
-
var parseSessionMessage = (data) => {
|
|
633
|
-
if (typeof data !== "string") {
|
|
634
|
-
if (!data || typeof data !== "object") {
|
|
635
|
-
return null;
|
|
636
|
-
}
|
|
637
|
-
return data;
|
|
638
|
-
}
|
|
639
|
-
const decoded = decodeSafe(
|
|
640
|
-
data
|
|
641
|
-
);
|
|
642
|
-
if (!decoded.ok || !decoded.value || typeof decoded.value !== "object") {
|
|
643
|
-
return null;
|
|
644
|
-
}
|
|
645
|
-
return decoded.value;
|
|
646
|
-
};
|
|
647
|
-
|
|
648
821
|
// session/net.ts
|
|
649
822
|
var NetClient = class {
|
|
650
823
|
constructor(client, bus, peerId) {
|
|
@@ -791,6 +964,13 @@ var ready = (command) => {
|
|
|
791
964
|
const state = getState();
|
|
792
965
|
const bus = getBus();
|
|
793
966
|
const localSid = getSid();
|
|
967
|
+
consoleLogger.debug("[session:ready] received", {
|
|
968
|
+
from: command.from,
|
|
969
|
+
sid: command.sid,
|
|
970
|
+
localSid,
|
|
971
|
+
local: state.getState("local"),
|
|
972
|
+
remote: state.getState("remote")
|
|
973
|
+
});
|
|
794
974
|
if (command.from === "local") {
|
|
795
975
|
if (!state.canAction("local", "READY")) {
|
|
796
976
|
console.warn("[Ready] Cannot dispatch READY from current state", {
|
|
@@ -805,6 +985,10 @@ var ready = (command) => {
|
|
|
805
985
|
sid: localSid
|
|
806
986
|
};
|
|
807
987
|
send(message);
|
|
988
|
+
consoleLogger.debug("[session:ready] local toggled", {
|
|
989
|
+
local: state.getState("local"),
|
|
990
|
+
remote: state.getState("remote")
|
|
991
|
+
});
|
|
808
992
|
return;
|
|
809
993
|
}
|
|
810
994
|
const remoteSid = command.sid;
|
|
@@ -824,6 +1008,10 @@ var ready = (command) => {
|
|
|
824
1008
|
}
|
|
825
1009
|
state.dispatch("remote", "READY");
|
|
826
1010
|
state.dispatch("local", "REMOTE_READY");
|
|
1011
|
+
consoleLogger.debug("[session:ready] remote toggled", {
|
|
1012
|
+
local: state.getState("local"),
|
|
1013
|
+
remote: state.getState("remote")
|
|
1014
|
+
});
|
|
827
1015
|
};
|
|
828
1016
|
|
|
829
1017
|
// session/handlers/start.ts
|
|
@@ -835,6 +1023,13 @@ var getNextStarter = (lastStarter) => {
|
|
|
835
1023
|
};
|
|
836
1024
|
var start = (command) => {
|
|
837
1025
|
const state = getState();
|
|
1026
|
+
consoleLogger.debug("[session:start] received", {
|
|
1027
|
+
from: command.from,
|
|
1028
|
+
payload: command.payload,
|
|
1029
|
+
local: state.getState("local"),
|
|
1030
|
+
remote: state.getState("remote"),
|
|
1031
|
+
lastStart: state.getLastStart()
|
|
1032
|
+
});
|
|
838
1033
|
if (command.from === "local") {
|
|
839
1034
|
if (!state.canAction("local", "START") || !state.canAction("remote", "REMOTE_START")) {
|
|
840
1035
|
console.warn("[Start] Cannot START from current state", {
|
|
@@ -846,6 +1041,10 @@ var start = (command) => {
|
|
|
846
1041
|
const nextStarter = getNextStarter(state.getLastStart());
|
|
847
1042
|
const localTarget2 = nextStarter === "local" ? "turn" : "remote_turn";
|
|
848
1043
|
const remoteTarget2 = nextStarter === "local" ? "remote_turn" : "turn";
|
|
1044
|
+
if (state.getHistory().length > 0) {
|
|
1045
|
+
state.clearHistory();
|
|
1046
|
+
consoleLogger.debug("[session:start] cleared previous match history");
|
|
1047
|
+
}
|
|
849
1048
|
state.setLastStart(nextStarter);
|
|
850
1049
|
state.dispatch("local", "START", localTarget2);
|
|
851
1050
|
state.dispatch("remote", "REMOTE_START", remoteTarget2);
|
|
@@ -853,6 +1052,7 @@ var start = (command) => {
|
|
|
853
1052
|
type: "START",
|
|
854
1053
|
payload: { starter: nextStarter === "local" ? "sender" : "receiver" }
|
|
855
1054
|
});
|
|
1055
|
+
consoleLogger.debug("[session:start] local started", { nextStarter });
|
|
856
1056
|
return;
|
|
857
1057
|
}
|
|
858
1058
|
const starterInfo = command.payload?.starter;
|
|
@@ -870,16 +1070,28 @@ var start = (command) => {
|
|
|
870
1070
|
const starter = starterInfo === "sender" ? "remote" : "local";
|
|
871
1071
|
const localTarget = starter === "local" ? "turn" : "remote_turn";
|
|
872
1072
|
const remoteTarget = starter === "local" ? "remote_turn" : "turn";
|
|
1073
|
+
if (state.getHistory().length > 0) {
|
|
1074
|
+
state.clearHistory();
|
|
1075
|
+
consoleLogger.debug("[session:start] cleared previous match history");
|
|
1076
|
+
}
|
|
873
1077
|
state.setLastStart(starter);
|
|
874
1078
|
state.dispatch("local", "REMOTE_START", localTarget);
|
|
875
1079
|
state.dispatch("remote", "START", remoteTarget);
|
|
1080
|
+
consoleLogger.debug("[session:start] remote started", { starter });
|
|
876
1081
|
};
|
|
877
1082
|
|
|
878
1083
|
// session/handlers/move.ts
|
|
879
1084
|
var move = (command) => {
|
|
880
1085
|
const state = getState();
|
|
881
|
-
const bus = getBus();
|
|
882
1086
|
const movePayload = command.payload;
|
|
1087
|
+
consoleLogger.debug("[session:move] received", {
|
|
1088
|
+
from: command.from,
|
|
1089
|
+
payload: movePayload,
|
|
1090
|
+
local: state.getState("local"),
|
|
1091
|
+
remote: state.getState("remote"),
|
|
1092
|
+
turn: state.getTurnCount(),
|
|
1093
|
+
history: state.getHistory().length
|
|
1094
|
+
});
|
|
883
1095
|
if (command.from === "local") {
|
|
884
1096
|
if (!state.canAction("local", "MOVE")) {
|
|
885
1097
|
console.warn("[Move] Cannot MOVE from current state", {
|
|
@@ -903,25 +1115,31 @@ var move = (command) => {
|
|
|
903
1115
|
player: "local",
|
|
904
1116
|
move: movePayload
|
|
905
1117
|
});
|
|
1118
|
+
const message = {
|
|
1119
|
+
type: "MOVE",
|
|
1120
|
+
turn: turn2,
|
|
1121
|
+
payload: movePayload
|
|
1122
|
+
};
|
|
1123
|
+
send(message);
|
|
1124
|
+
consoleLogger.debug("[session:move] local move sent", {
|
|
1125
|
+
turn: turn2,
|
|
1126
|
+
payload: movePayload
|
|
1127
|
+
});
|
|
906
1128
|
const winner2 = state.checkWin();
|
|
907
1129
|
if (winner2) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
type: "GAME_OVER",
|
|
912
|
-
payload: { winner: winner2, turn: turn2 }
|
|
1130
|
+
consoleLogger.debug("[session:move] game over detected", {
|
|
1131
|
+
winner: winner2,
|
|
1132
|
+
turn: turn2
|
|
913
1133
|
});
|
|
914
1134
|
state.dispatch("local", "GAME_OVER");
|
|
915
1135
|
state.dispatch("remote", "GAME_OVER");
|
|
916
1136
|
state.cleanupGame();
|
|
1137
|
+
consoleLogger.debug("[session:move] local game over applied", {
|
|
1138
|
+
winner: winner2,
|
|
1139
|
+
turn: turn2
|
|
1140
|
+
});
|
|
917
1141
|
return;
|
|
918
1142
|
}
|
|
919
|
-
const message = {
|
|
920
|
-
type: "MOVE",
|
|
921
|
-
turn: turn2,
|
|
922
|
-
payload: movePayload
|
|
923
|
-
};
|
|
924
|
-
send(message);
|
|
925
1143
|
return;
|
|
926
1144
|
}
|
|
927
1145
|
if (!state.canAction("remote", "MOVE")) {
|
|
@@ -948,31 +1166,63 @@ var move = (command) => {
|
|
|
948
1166
|
});
|
|
949
1167
|
const winner = state.checkWin();
|
|
950
1168
|
if (winner) {
|
|
951
|
-
|
|
952
|
-
bus.emit("GAME_OVER", { winner, turn }, "local");
|
|
1169
|
+
consoleLogger.debug("[session:move] game over detected", { winner, turn });
|
|
953
1170
|
state.dispatch("local", "GAME_OVER");
|
|
954
1171
|
state.dispatch("remote", "GAME_OVER");
|
|
955
1172
|
state.cleanupGame();
|
|
1173
|
+
consoleLogger.debug("[session:move] remote game over applied", {
|
|
1174
|
+
winner,
|
|
1175
|
+
turn
|
|
1176
|
+
});
|
|
1177
|
+
return;
|
|
956
1178
|
}
|
|
1179
|
+
consoleLogger.debug("[session:move] remote move applied", {
|
|
1180
|
+
turn,
|
|
1181
|
+
payload: movePayload
|
|
1182
|
+
});
|
|
957
1183
|
};
|
|
958
1184
|
|
|
959
1185
|
// session/handlers/request.ts
|
|
1186
|
+
var isRequestAction = (value) => value === "undo" || value === "restart";
|
|
960
1187
|
var request = (command) => {
|
|
961
1188
|
if (command.type !== "APPROVE" && command.type !== "REJECT") {
|
|
962
1189
|
return;
|
|
963
1190
|
}
|
|
964
1191
|
const state = getState();
|
|
965
|
-
const
|
|
1192
|
+
const payload = command.payload;
|
|
1193
|
+
const pendingAction = state.getPendingAction();
|
|
1194
|
+
const action = pendingAction ?? (command.from === "local" && command.type === "REJECT" && isRequestAction(payload?.action) ? payload.action : null);
|
|
1195
|
+
consoleLogger.debug("[session:request] received", {
|
|
1196
|
+
type: command.type,
|
|
1197
|
+
from: command.from,
|
|
1198
|
+
action,
|
|
1199
|
+
local: state.getState("local"),
|
|
1200
|
+
remote: state.getState("remote"),
|
|
1201
|
+
history: state.getHistory().length
|
|
1202
|
+
});
|
|
966
1203
|
if (!action) {
|
|
967
1204
|
console.warn("[Request] No pending action", { commandType: command.type });
|
|
968
1205
|
return;
|
|
969
1206
|
}
|
|
970
|
-
const payload = command.payload;
|
|
971
1207
|
if (payload?.action && payload.action !== action) {
|
|
972
|
-
console.warn("[Request] Action mismatch", {
|
|
1208
|
+
console.warn("[Request] Action mismatch", {
|
|
1209
|
+
pending: action,
|
|
1210
|
+
payload: payload.action
|
|
1211
|
+
});
|
|
973
1212
|
return;
|
|
974
1213
|
}
|
|
975
1214
|
if (command.from === "local") {
|
|
1215
|
+
if (!pendingAction && command.type === "REJECT") {
|
|
1216
|
+
send({
|
|
1217
|
+
type: "REJECT",
|
|
1218
|
+
payload: { action, reason: payload?.reason ?? "rejected" }
|
|
1219
|
+
});
|
|
1220
|
+
consoleLogger.debug("[session:request] local auto rejected", {
|
|
1221
|
+
action,
|
|
1222
|
+
reason: payload?.reason
|
|
1223
|
+
});
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
976
1226
|
if (command.type === "APPROVE") {
|
|
977
1227
|
if (!state.canAction("local", "APPROVE")) {
|
|
978
1228
|
console.warn("[Request] Cannot APPROVE from current state");
|
|
@@ -986,6 +1236,7 @@ var request = (command) => {
|
|
|
986
1236
|
}
|
|
987
1237
|
send({ type: "APPROVE", payload: { action } });
|
|
988
1238
|
state.clearPendingStates();
|
|
1239
|
+
consoleLogger.debug("[session:request] local approved", { action });
|
|
989
1240
|
return;
|
|
990
1241
|
}
|
|
991
1242
|
if (!state.canAction("local", "REJECT")) {
|
|
@@ -998,11 +1249,14 @@ var request = (command) => {
|
|
|
998
1249
|
payload: { action, reason: payload?.reason ?? "rejected" }
|
|
999
1250
|
});
|
|
1000
1251
|
state.clearPendingStates();
|
|
1252
|
+
consoleLogger.debug("[session:request] local rejected", { action });
|
|
1001
1253
|
return;
|
|
1002
1254
|
}
|
|
1003
1255
|
if (command.type === "APPROVE") {
|
|
1004
1256
|
if (!state.canAction("local", "APPROVE")) {
|
|
1005
|
-
console.warn(
|
|
1257
|
+
console.warn(
|
|
1258
|
+
"[Request] Cannot APPROVE from current state (remote approved)"
|
|
1259
|
+
);
|
|
1006
1260
|
return;
|
|
1007
1261
|
}
|
|
1008
1262
|
state.dispatchApprove();
|
|
@@ -1012,70 +1266,141 @@ var request = (command) => {
|
|
|
1012
1266
|
state.resetGame();
|
|
1013
1267
|
}
|
|
1014
1268
|
state.clearPendingStates();
|
|
1269
|
+
consoleLogger.debug("[session:request] remote approved", { action });
|
|
1015
1270
|
return;
|
|
1016
1271
|
}
|
|
1017
1272
|
if (!state.canAction("local", "REJECT")) {
|
|
1018
|
-
console.warn(
|
|
1273
|
+
console.warn(
|
|
1274
|
+
"[Request] Cannot REJECT from current state (remote rejected)"
|
|
1275
|
+
);
|
|
1019
1276
|
state.clearPendingStates();
|
|
1020
1277
|
return;
|
|
1021
1278
|
}
|
|
1022
1279
|
state.dispatchReject();
|
|
1023
1280
|
state.clearPendingStates();
|
|
1281
|
+
consoleLogger.debug("[session:request] remote rejected", { action });
|
|
1024
1282
|
};
|
|
1025
1283
|
|
|
1026
1284
|
// session/handlers/sync.ts
|
|
1027
|
-
var
|
|
1285
|
+
var fromPeerPerspective = (player) => player === "local" ? "remote" : "local";
|
|
1286
|
+
var getCurrentTurn = () => {
|
|
1028
1287
|
const state = getState();
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1288
|
+
return state.getResumeTurn() ?? (state.getState("local") === "turn" ? "local" : "remote");
|
|
1289
|
+
};
|
|
1290
|
+
var buildSyncPayload = () => {
|
|
1291
|
+
const state = getState();
|
|
1292
|
+
const turn = getCurrentTurn();
|
|
1293
|
+
return {
|
|
1294
|
+
history: state.getHistory(),
|
|
1295
|
+
lastStart: state.getLastStart(),
|
|
1296
|
+
turn,
|
|
1297
|
+
resumeTurn: state.getResumeTurn()
|
|
1298
|
+
};
|
|
1299
|
+
};
|
|
1300
|
+
var isInSyncRecovery = () => {
|
|
1301
|
+
const state = getState();
|
|
1302
|
+
return state.getState("local") === "syncing" || state.getState("remote") === "syncing" || state.getState("remote") === "offline" || state.getResumeTurn() !== null;
|
|
1303
|
+
};
|
|
1304
|
+
var enterSyncState = () => {
|
|
1305
|
+
const state = getState();
|
|
1306
|
+
if (state.getState("local") !== "syncing") {
|
|
1307
|
+
if (!state.canAction("local", "SYNC")) {
|
|
1308
|
+
consoleLogger.debug("[session:sync] local cannot enter sync", {
|
|
1309
|
+
local: state.getState("local")
|
|
1310
|
+
});
|
|
1311
|
+
return false;
|
|
1039
1312
|
}
|
|
1040
|
-
|
|
1041
|
-
history: state.getHistory(),
|
|
1042
|
-
lastStart: state.getLastStart(),
|
|
1043
|
-
turn: state.getState("local") === "turn" ? "local" : "remote",
|
|
1044
|
-
resumeTurn: state.getResumeTurn()
|
|
1045
|
-
// Send back the saved resume turn
|
|
1046
|
-
};
|
|
1047
|
-
send({ type: "SYNC_STATE", from: "", payload: payload2 });
|
|
1048
|
-
return;
|
|
1313
|
+
state.dispatch("local", "SYNC", "syncing");
|
|
1049
1314
|
}
|
|
1050
|
-
if (
|
|
1051
|
-
|
|
1315
|
+
if (state.getState("remote") === "offline") {
|
|
1316
|
+
if (!state.canAction("remote", "ONLINE")) {
|
|
1317
|
+
consoleLogger.debug("[session:sync] offline remote cannot enter sync", {
|
|
1318
|
+
remote: state.getState("remote")
|
|
1319
|
+
});
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
state.dispatch("remote", "ONLINE", "syncing");
|
|
1323
|
+
} else if (state.getState("remote") !== "syncing") {
|
|
1324
|
+
if (!state.canAction("remote", "SYNC")) {
|
|
1325
|
+
consoleLogger.debug("[session:sync] remote cannot enter sync", {
|
|
1326
|
+
remote: state.getState("remote")
|
|
1327
|
+
});
|
|
1328
|
+
return false;
|
|
1329
|
+
}
|
|
1330
|
+
state.dispatch("remote", "SYNC", "syncing");
|
|
1052
1331
|
}
|
|
1053
|
-
|
|
1332
|
+
return state.getState("local") === "syncing" && state.getState("remote") === "syncing";
|
|
1333
|
+
};
|
|
1334
|
+
var restoreFromPayload = (payload, mapPeerLabels) => {
|
|
1335
|
+
const state = getState();
|
|
1336
|
+
const mapPlayer = mapPeerLabels ? fromPeerPerspective : (player) => player;
|
|
1054
1337
|
if (payload.history && payload.history.length > 0) {
|
|
1055
|
-
state.replaceHistory(
|
|
1338
|
+
state.replaceHistory(
|
|
1339
|
+
payload.history.map((entry) => ({
|
|
1340
|
+
...entry,
|
|
1341
|
+
player: mapPlayer(entry.player)
|
|
1342
|
+
}))
|
|
1343
|
+
);
|
|
1056
1344
|
} else {
|
|
1057
1345
|
state.clearHistory();
|
|
1058
1346
|
}
|
|
1059
1347
|
if (payload.lastStart) {
|
|
1060
|
-
state.setLastStart(payload.lastStart);
|
|
1348
|
+
state.setLastStart(mapPlayer(payload.lastStart));
|
|
1061
1349
|
} else {
|
|
1062
1350
|
state.setLastStart(null);
|
|
1063
1351
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
nextPlayer = payload.resumeTurn;
|
|
1067
|
-
} else if (payload.turn) {
|
|
1068
|
-
nextPlayer = payload.turn === "local" ? "local" : "remote";
|
|
1069
|
-
} else {
|
|
1070
|
-
nextPlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1071
|
-
}
|
|
1072
|
-
console.log("[Sync] Restored game state", {
|
|
1352
|
+
const nextPlayer = payload.resumeTurn ? mapPlayer(payload.resumeTurn) : payload.turn ? mapPlayer(payload.turn) : getCurrentTurn();
|
|
1353
|
+
consoleLogger.debug("[session:sync] state restored", {
|
|
1073
1354
|
historyLength: state.getHistory().length,
|
|
1074
1355
|
lastStart: state.getLastStart(),
|
|
1075
|
-
nextTurnPlayer: nextPlayer
|
|
1356
|
+
nextTurnPlayer: nextPlayer,
|
|
1357
|
+
mapped: mapPeerLabels
|
|
1076
1358
|
});
|
|
1359
|
+
if (!enterSyncState()) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1077
1362
|
state.dispatchSyncComplete(nextPlayer);
|
|
1078
1363
|
};
|
|
1364
|
+
var sync = (command) => {
|
|
1365
|
+
const state = getState();
|
|
1366
|
+
consoleLogger.debug("[session:sync] received", {
|
|
1367
|
+
type: command.type,
|
|
1368
|
+
from: command.from,
|
|
1369
|
+
payload: command.payload,
|
|
1370
|
+
local: state.getState("local"),
|
|
1371
|
+
remote: state.getState("remote"),
|
|
1372
|
+
history: state.getHistory().length,
|
|
1373
|
+
resumeTurn: state.getResumeTurn()
|
|
1374
|
+
});
|
|
1375
|
+
if (command.type === "SYNC_REQUEST") {
|
|
1376
|
+
if (command.from === "local") {
|
|
1377
|
+
if (state.getState("local") !== "syncing" && !state.canAction("local", "SYNC")) {
|
|
1378
|
+
console.warn("[Sync] Cannot SYNC from current state");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (state.getState("local") !== "syncing") {
|
|
1382
|
+
state.dispatch("local", "SYNC", "syncing");
|
|
1383
|
+
}
|
|
1384
|
+
if (state.getState("remote") !== "syncing") {
|
|
1385
|
+
state.dispatch("remote", "SYNC", "syncing");
|
|
1386
|
+
}
|
|
1387
|
+
send({ type: "SYNC_REQUEST", from: "", payload: command.payload });
|
|
1388
|
+
consoleLogger.debug("[session:sync] request sent");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const payload = buildSyncPayload();
|
|
1392
|
+
send({ type: "SYNC_STATE", from: "", payload });
|
|
1393
|
+
consoleLogger.debug("[session:sync] state sent", payload);
|
|
1394
|
+
if (isInSyncRecovery()) {
|
|
1395
|
+
restoreFromPayload(payload, false);
|
|
1396
|
+
}
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (command.type !== "SYNC_STATE") {
|
|
1400
|
+
return;
|
|
1401
|
+
}
|
|
1402
|
+
restoreFromPayload(command.payload || {}, true);
|
|
1403
|
+
};
|
|
1079
1404
|
|
|
1080
1405
|
// session/handlers/undo.ts
|
|
1081
1406
|
var undo = (command) => {
|
|
@@ -1083,6 +1408,14 @@ var undo = (command) => {
|
|
|
1083
1408
|
return;
|
|
1084
1409
|
}
|
|
1085
1410
|
const state = getState();
|
|
1411
|
+
const bus = getBus();
|
|
1412
|
+
consoleLogger.debug("[session:undo] received", {
|
|
1413
|
+
from: command.from,
|
|
1414
|
+
local: state.getState("local"),
|
|
1415
|
+
remote: state.getState("remote"),
|
|
1416
|
+
pending: state.getPendingAction(),
|
|
1417
|
+
history: state.getHistory().length
|
|
1418
|
+
});
|
|
1086
1419
|
if (command.from === "local") {
|
|
1087
1420
|
if (state.hasPendingAction()) {
|
|
1088
1421
|
return;
|
|
@@ -1092,40 +1425,46 @@ var undo = (command) => {
|
|
|
1092
1425
|
return;
|
|
1093
1426
|
}
|
|
1094
1427
|
const localState = state.getState("local");
|
|
1095
|
-
const undoCount = localState === "turn" ?
|
|
1428
|
+
const undoCount = localState === "turn" ? 2 : 1;
|
|
1429
|
+
const rejectResumePlayer = localState === "turn" ? "local" : "remote";
|
|
1096
1430
|
if (state.getHistory().length < undoCount) {
|
|
1097
1431
|
console.warn("[Undo] Not enough history to undo", { count: undoCount });
|
|
1098
1432
|
return;
|
|
1099
1433
|
}
|
|
1100
|
-
state.initializeUndoRequest(undoCount,
|
|
1434
|
+
state.initializeUndoRequest(undoCount, rejectResumePlayer);
|
|
1101
1435
|
state.dispatch("local", "UNDO");
|
|
1102
1436
|
state.dispatch("remote", "REMOTE_UNDO");
|
|
1103
1437
|
send({ type: "UNDO", payload: { count: undoCount } });
|
|
1438
|
+
consoleLogger.debug("[session:undo] local requested", { undoCount });
|
|
1104
1439
|
return;
|
|
1105
1440
|
}
|
|
1106
1441
|
if (state.hasPendingAction()) {
|
|
1107
|
-
|
|
1442
|
+
bus.emit("REJECT", { action: "undo", reason: "busy" }, "local");
|
|
1108
1443
|
return;
|
|
1109
1444
|
}
|
|
1110
1445
|
if (!state.canAction("local", "REMOTE_UNDO")) {
|
|
1111
1446
|
console.warn("[Undo] Cannot accept remote UNDO request");
|
|
1112
|
-
|
|
1447
|
+
bus.emit("REJECT", { action: "undo", reason: "invalid_state" }, "local");
|
|
1113
1448
|
return;
|
|
1114
1449
|
}
|
|
1115
1450
|
const payload = command.payload;
|
|
1116
1451
|
const count = payload?.count === 2 ? 2 : 1;
|
|
1117
1452
|
if (payload?.count && payload.count !== 1 && payload.count !== 2) {
|
|
1118
|
-
|
|
1453
|
+
bus.emit("REJECT", { action: "undo", reason: "invalid" }, "local");
|
|
1119
1454
|
return;
|
|
1120
1455
|
}
|
|
1121
|
-
if (
|
|
1122
|
-
|
|
1456
|
+
if (state.getHistory().length < count) {
|
|
1457
|
+
bus.emit("REJECT", { action: "undo", reason: "no_history" }, "local");
|
|
1123
1458
|
return;
|
|
1124
1459
|
}
|
|
1125
1460
|
const resumePlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1126
1461
|
state.initializeUndoRequest(count, resumePlayer);
|
|
1127
1462
|
state.dispatch("local", "REMOTE_UNDO");
|
|
1128
1463
|
state.dispatch("remote", "UNDO");
|
|
1464
|
+
consoleLogger.debug("[session:undo] remote requested", {
|
|
1465
|
+
count,
|
|
1466
|
+
resumePlayer
|
|
1467
|
+
});
|
|
1129
1468
|
};
|
|
1130
1469
|
|
|
1131
1470
|
// session/handlers/restart.ts
|
|
@@ -1134,6 +1473,14 @@ var restart = (command) => {
|
|
|
1134
1473
|
return;
|
|
1135
1474
|
}
|
|
1136
1475
|
const state = getState();
|
|
1476
|
+
const bus = getBus();
|
|
1477
|
+
consoleLogger.debug("[session:restart] received", {
|
|
1478
|
+
from: command.from,
|
|
1479
|
+
local: state.getState("local"),
|
|
1480
|
+
remote: state.getState("remote"),
|
|
1481
|
+
pending: state.getPendingAction(),
|
|
1482
|
+
history: state.getHistory().length
|
|
1483
|
+
});
|
|
1137
1484
|
if (command.from === "local") {
|
|
1138
1485
|
if (state.hasPendingAction()) {
|
|
1139
1486
|
return;
|
|
@@ -1147,21 +1494,23 @@ var restart = (command) => {
|
|
|
1147
1494
|
state.dispatch("local", "RESTART");
|
|
1148
1495
|
state.dispatch("remote", "REMOTE_RESTART");
|
|
1149
1496
|
send({ type: "RESTART" });
|
|
1497
|
+
consoleLogger.debug("[session:restart] local requested", { resumePlayer: resumePlayer2 });
|
|
1150
1498
|
return;
|
|
1151
1499
|
}
|
|
1152
1500
|
if (state.hasPendingAction()) {
|
|
1153
|
-
|
|
1501
|
+
bus.emit("REJECT", { action: "restart", reason: "busy" }, "local");
|
|
1154
1502
|
return;
|
|
1155
1503
|
}
|
|
1156
1504
|
if (!state.canAction("local", "REMOTE_RESTART")) {
|
|
1157
1505
|
console.warn("[Restart] Cannot accept remote RESTART request");
|
|
1158
|
-
|
|
1506
|
+
bus.emit("REJECT", { action: "restart", reason: "invalid_state" }, "local");
|
|
1159
1507
|
return;
|
|
1160
1508
|
}
|
|
1161
1509
|
const resumePlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1162
1510
|
state.initializeRestartRequest(resumePlayer);
|
|
1163
1511
|
state.dispatch("local", "REMOTE_RESTART");
|
|
1164
1512
|
state.dispatch("remote", "RESTART");
|
|
1513
|
+
consoleLogger.debug("[session:restart] remote requested", { resumePlayer });
|
|
1165
1514
|
};
|
|
1166
1515
|
|
|
1167
1516
|
// session/handlers/offLine.ts
|
|
@@ -1171,17 +1520,40 @@ var offline = (command) => {
|
|
|
1171
1520
|
}
|
|
1172
1521
|
const state = getState();
|
|
1173
1522
|
const bus = getBus();
|
|
1523
|
+
consoleLogger.debug("[session:connection] received", {
|
|
1524
|
+
type: command.type,
|
|
1525
|
+
from: command.from,
|
|
1526
|
+
local: state.getState("local"),
|
|
1527
|
+
remote: state.getState("remote"),
|
|
1528
|
+
pending: state.getPendingAction(),
|
|
1529
|
+
resumeTurn: state.getResumeTurn()
|
|
1530
|
+
});
|
|
1174
1531
|
if (command.type === "OFFLINE") {
|
|
1532
|
+
if (state.hasPendingAction()) {
|
|
1533
|
+
const resumeTurn = state.getResumeTurn();
|
|
1534
|
+
if (state.canAction("local", "SYNC")) {
|
|
1535
|
+
state.dispatch("local", "SYNC", "syncing");
|
|
1536
|
+
}
|
|
1537
|
+
state.clearPendingStates();
|
|
1538
|
+
state.setResumeTurn(resumeTurn);
|
|
1539
|
+
}
|
|
1175
1540
|
if (!state.canAction("remote", "OFFLINE")) {
|
|
1176
1541
|
console.warn("[Offline] Cannot transition to OFFLINE from current state");
|
|
1177
1542
|
return;
|
|
1178
1543
|
}
|
|
1179
|
-
const currentTurn = state.getState("local") === "turn" ? "local" : "remote";
|
|
1544
|
+
const currentTurn = state.getResumeTurn() ?? (state.getState("local") === "turn" ? "local" : "remote");
|
|
1180
1545
|
state.setResumeTurn(currentTurn);
|
|
1181
1546
|
state.dispatch("remote", "OFFLINE", "offline");
|
|
1547
|
+
consoleLogger.debug("[session:connection] remote offline", { currentTurn });
|
|
1182
1548
|
return;
|
|
1183
1549
|
}
|
|
1184
1550
|
if (state.getState("remote") !== "offline") {
|
|
1551
|
+
consoleLogger.debug(
|
|
1552
|
+
"[session:connection] ignored online while remote is not offline",
|
|
1553
|
+
{
|
|
1554
|
+
remote: state.getState("remote")
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1185
1557
|
return;
|
|
1186
1558
|
}
|
|
1187
1559
|
if (!state.canAction("remote", "ONLINE")) {
|
|
@@ -1190,6 +1562,7 @@ var offline = (command) => {
|
|
|
1190
1562
|
}
|
|
1191
1563
|
state.dispatch("remote", "ONLINE", "syncing");
|
|
1192
1564
|
bus.emit("SYNC_REQUEST", void 0, "local");
|
|
1565
|
+
consoleLogger.debug("[session:connection] remote online, sync requested");
|
|
1193
1566
|
};
|
|
1194
1567
|
|
|
1195
1568
|
// session/handlers/busRegister.ts
|