p2p-lockstep-kit-session 0.1.8 → 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 +473 -119
- package/dist/session/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/serialization-smoke.mjs +376 -96
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
|
};
|
|
@@ -258,11 +319,21 @@ var UINotificationAdapter = class {
|
|
|
258
319
|
if (this.lastNotificationTime + this.notificationThrottleMs > now) return;
|
|
259
320
|
this.lastNotificationTime = now;
|
|
260
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
|
+
});
|
|
261
330
|
this.uiObserver.notifyStateChange(snapshot);
|
|
262
331
|
}
|
|
263
332
|
onHistoryChanged() {
|
|
333
|
+
this.onStateChanged();
|
|
264
334
|
}
|
|
265
335
|
onGameReset() {
|
|
336
|
+
this.onStateChanged();
|
|
266
337
|
}
|
|
267
338
|
emitEvent(event) {
|
|
268
339
|
this.uiObserver.notifyGameEvent(event);
|
|
@@ -300,6 +371,7 @@ var State = class {
|
|
|
300
371
|
if (remoteId) {
|
|
301
372
|
this.remoteId = remoteId;
|
|
302
373
|
}
|
|
374
|
+
consoleLogger.debug("[session:state] created", { localId: id, remoteId });
|
|
303
375
|
}
|
|
304
376
|
/**
|
|
305
377
|
* Register an internal observer (like plugin pattern)
|
|
@@ -317,6 +389,7 @@ var State = class {
|
|
|
317
389
|
}
|
|
318
390
|
setremoteId(id) {
|
|
319
391
|
this.remoteId = id;
|
|
392
|
+
consoleLogger.debug("[session:state] remote id set", { remoteId: id });
|
|
320
393
|
}
|
|
321
394
|
getState(player) {
|
|
322
395
|
return this.getPlayerFsm(player).getState();
|
|
@@ -328,6 +401,7 @@ var State = class {
|
|
|
328
401
|
return this.history.slice();
|
|
329
402
|
}
|
|
330
403
|
replaceHistory(entries) {
|
|
404
|
+
consoleLogger.debug("[session:history] replace", { count: entries.length });
|
|
331
405
|
this.clearHistory();
|
|
332
406
|
entries.forEach((entry) => {
|
|
333
407
|
this.pushHistory({
|
|
@@ -338,15 +412,33 @@ var State = class {
|
|
|
338
412
|
});
|
|
339
413
|
}
|
|
340
414
|
clearHistory() {
|
|
415
|
+
const count = this.history.length;
|
|
341
416
|
this.history.splice(0, this.history.length);
|
|
417
|
+
consoleLogger.debug("[session:history] clear", { count });
|
|
342
418
|
this.notifyHistoryChanged();
|
|
343
419
|
}
|
|
344
420
|
pushHistory(entry) {
|
|
345
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
|
+
});
|
|
346
428
|
this.notifyHistoryChanged();
|
|
347
429
|
}
|
|
348
430
|
popHistory() {
|
|
349
|
-
|
|
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;
|
|
350
442
|
}
|
|
351
443
|
canAction(player, action) {
|
|
352
444
|
return this.getPlayerFsm(player).hasNextState(action);
|
|
@@ -359,28 +451,56 @@ var State = class {
|
|
|
359
451
|
* so we automatically find and apply it.
|
|
360
452
|
*/
|
|
361
453
|
dispatch(player, action, to) {
|
|
454
|
+
const before = this.getState(player);
|
|
362
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
|
+
});
|
|
363
467
|
this.notifyStateChanged();
|
|
364
468
|
}
|
|
365
469
|
setPendingAction(action) {
|
|
470
|
+
consoleLogger.debug("[session:state] pending action set", {
|
|
471
|
+
from: this.pendingAction,
|
|
472
|
+
to: action
|
|
473
|
+
});
|
|
366
474
|
this.pendingAction = action;
|
|
367
475
|
}
|
|
368
476
|
getPendingAction() {
|
|
369
477
|
return this.pendingAction;
|
|
370
478
|
}
|
|
371
479
|
setPendingUndoCount(count) {
|
|
480
|
+
consoleLogger.debug("[session:state] pending undo count set", {
|
|
481
|
+
from: this.pendingUndoCount,
|
|
482
|
+
to: count
|
|
483
|
+
});
|
|
372
484
|
this.pendingUndoCount = count;
|
|
373
485
|
}
|
|
374
486
|
getPendingUndoCount() {
|
|
375
487
|
return this.pendingUndoCount;
|
|
376
488
|
}
|
|
377
489
|
setLastStart(player) {
|
|
490
|
+
consoleLogger.debug("[session:state] last start set", {
|
|
491
|
+
from: this.lastStart,
|
|
492
|
+
to: player
|
|
493
|
+
});
|
|
378
494
|
this.lastStart = player;
|
|
379
495
|
}
|
|
380
496
|
getLastStart() {
|
|
381
497
|
return this.lastStart;
|
|
382
498
|
}
|
|
383
499
|
setResumeTurn(player) {
|
|
500
|
+
consoleLogger.debug("[session:state] resume turn set", {
|
|
501
|
+
from: this.resumeTurn,
|
|
502
|
+
to: player
|
|
503
|
+
});
|
|
384
504
|
this.resumeTurn = player;
|
|
385
505
|
}
|
|
386
506
|
getResumeTurn() {
|
|
@@ -390,27 +510,60 @@ var State = class {
|
|
|
390
510
|
return player === "local" ? this.local : this.remote;
|
|
391
511
|
}
|
|
392
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
|
+
});
|
|
393
520
|
this.stateObserverManager.notifyStateChanged();
|
|
394
521
|
}
|
|
395
522
|
notifyHistoryChanged() {
|
|
523
|
+
consoleLogger.debug("[session:state] notify history changed", {
|
|
524
|
+
turn: this.getTurnCount(),
|
|
525
|
+
history: this.history.length,
|
|
526
|
+
pending: this.pendingAction
|
|
527
|
+
});
|
|
396
528
|
this.stateObserverManager.notifyHistoryChanged();
|
|
397
529
|
}
|
|
398
530
|
notifyGameReset() {
|
|
531
|
+
consoleLogger.debug("[session:state] notify game reset");
|
|
399
532
|
this.stateObserverManager.notifyGameReset();
|
|
400
533
|
}
|
|
401
534
|
dispatchPair(localAction, localTo, remoteAction, remoteTo) {
|
|
535
|
+
const before = {
|
|
536
|
+
local: this.local.getState(),
|
|
537
|
+
remote: this.remote.getState()
|
|
538
|
+
};
|
|
402
539
|
this.local.dispatch(localAction, localTo);
|
|
403
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
|
+
});
|
|
404
555
|
this.notifyStateChanged();
|
|
405
556
|
}
|
|
406
557
|
saveGameSnapshot(snapshot) {
|
|
407
558
|
this.gameSnapshot = snapshot;
|
|
559
|
+
consoleLogger.debug("[session:state] game snapshot saved", { snapshot });
|
|
408
560
|
}
|
|
409
561
|
getGameSnapshot() {
|
|
410
562
|
return this.gameSnapshot;
|
|
411
563
|
}
|
|
412
564
|
clearGameSnapshot() {
|
|
413
565
|
this.gameSnapshot = null;
|
|
566
|
+
consoleLogger.debug("[session:state] game snapshot cleared");
|
|
414
567
|
}
|
|
415
568
|
/**
|
|
416
569
|
* Check if there's a pending action (undo/restart)
|
|
@@ -422,6 +575,11 @@ var State = class {
|
|
|
422
575
|
* Clear all pending states (called after approval/rejection)
|
|
423
576
|
*/
|
|
424
577
|
clearPendingStates() {
|
|
578
|
+
consoleLogger.debug("[session:state] pending states cleared", {
|
|
579
|
+
pending: this.pendingAction,
|
|
580
|
+
pendingUndoCount: this.pendingUndoCount,
|
|
581
|
+
resumeTurn: this.resumeTurn
|
|
582
|
+
});
|
|
425
583
|
this.pendingAction = null;
|
|
426
584
|
this.pendingUndoCount = null;
|
|
427
585
|
this.resumeTurn = null;
|
|
@@ -434,6 +592,10 @@ var State = class {
|
|
|
434
592
|
this.pendingAction = "undo";
|
|
435
593
|
this.pendingUndoCount = undoCount;
|
|
436
594
|
this.resumeTurn = resumeTurn;
|
|
595
|
+
consoleLogger.debug("[session:state] undo request initialized", {
|
|
596
|
+
undoCount,
|
|
597
|
+
resumeTurn
|
|
598
|
+
});
|
|
437
599
|
}
|
|
438
600
|
/**
|
|
439
601
|
* Initialize restart request with resume turn
|
|
@@ -441,6 +603,9 @@ var State = class {
|
|
|
441
603
|
initializeRestartRequest(resumeTurn) {
|
|
442
604
|
this.pendingAction = "restart";
|
|
443
605
|
this.resumeTurn = resumeTurn;
|
|
606
|
+
consoleLogger.debug("[session:state] restart request initialized", {
|
|
607
|
+
resumeTurn
|
|
608
|
+
});
|
|
444
609
|
}
|
|
445
610
|
/**
|
|
446
611
|
* Check if pending action is undo
|
|
@@ -458,6 +623,7 @@ var State = class {
|
|
|
458
623
|
* Apply undo by popping history N times
|
|
459
624
|
*/
|
|
460
625
|
applyUndo(count = 1) {
|
|
626
|
+
consoleLogger.debug("[session:history] apply undo", { count });
|
|
461
627
|
for (let i = 0; i < count; i++) {
|
|
462
628
|
this.popHistory();
|
|
463
629
|
}
|
|
@@ -466,6 +632,13 @@ var State = class {
|
|
|
466
632
|
* Reset game state to initial (for restart)
|
|
467
633
|
*/
|
|
468
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
|
+
});
|
|
469
642
|
this.clearHistory();
|
|
470
643
|
this.local = new SessionFsm("idle");
|
|
471
644
|
this.remote = new SessionFsm("idle");
|
|
@@ -481,6 +654,7 @@ var State = class {
|
|
|
481
654
|
*/
|
|
482
655
|
recordStartPlayer(player) {
|
|
483
656
|
this.lastStart = player;
|
|
657
|
+
consoleLogger.debug("[session:state] start player recorded", { player });
|
|
484
658
|
}
|
|
485
659
|
/**
|
|
486
660
|
* Get move to undo from history
|
|
@@ -518,6 +692,11 @@ var State = class {
|
|
|
518
692
|
* Determines who plays first based on starter parameter
|
|
519
693
|
*/
|
|
520
694
|
dispatchStart(firstPlayer) {
|
|
695
|
+
const before = {
|
|
696
|
+
local: this.local.getState(),
|
|
697
|
+
remote: this.remote.getState(),
|
|
698
|
+
lastStart: this.lastStart
|
|
699
|
+
};
|
|
521
700
|
if (firstPlayer === "local") {
|
|
522
701
|
this.local.dispatch("START", "turn");
|
|
523
702
|
this.remote.dispatch("START", "remote_turn");
|
|
@@ -527,18 +706,38 @@ var State = class {
|
|
|
527
706
|
this.remote.dispatch("START", "turn");
|
|
528
707
|
this.lastStart = "remote";
|
|
529
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();
|
|
530
719
|
}
|
|
531
720
|
/**
|
|
532
721
|
* Dispatch SYNC_COMPLETE with automatic target state resolution
|
|
533
722
|
* Based on who should have the turn after sync
|
|
534
723
|
*/
|
|
535
724
|
dispatchSyncComplete(nextPlayer) {
|
|
536
|
-
this.resumeTurn = nextPlayer;
|
|
537
725
|
if (nextPlayer === "local") {
|
|
538
|
-
this.dispatchPair(
|
|
726
|
+
this.dispatchPair(
|
|
727
|
+
"SYNC_COMPLETE",
|
|
728
|
+
"turn",
|
|
729
|
+
"SYNC_COMPLETE",
|
|
730
|
+
"remote_turn"
|
|
731
|
+
);
|
|
539
732
|
} else {
|
|
540
|
-
this.dispatchPair(
|
|
733
|
+
this.dispatchPair(
|
|
734
|
+
"SYNC_COMPLETE",
|
|
735
|
+
"remote_turn",
|
|
736
|
+
"SYNC_COMPLETE",
|
|
737
|
+
"turn"
|
|
738
|
+
);
|
|
541
739
|
}
|
|
740
|
+
this.resumeTurn = null;
|
|
542
741
|
}
|
|
543
742
|
// ===== Game Plugin Integration (Proxy Pattern) =====
|
|
544
743
|
/**
|
|
@@ -547,6 +746,10 @@ var State = class {
|
|
|
547
746
|
*/
|
|
548
747
|
setGamePlugin(plugin) {
|
|
549
748
|
this.gamePlugin = plugin;
|
|
749
|
+
consoleLogger.debug("[session:plugin] game plugin set", {
|
|
750
|
+
hasInitialize: Boolean(plugin.initialize),
|
|
751
|
+
hasCleanup: Boolean(plugin.cleanup)
|
|
752
|
+
});
|
|
550
753
|
if (plugin.initialize) {
|
|
551
754
|
plugin.initialize();
|
|
552
755
|
}
|
|
@@ -565,7 +768,16 @@ var State = class {
|
|
|
565
768
|
*/
|
|
566
769
|
validateMove(move2) {
|
|
567
770
|
const gameState = this.buildGameState();
|
|
568
|
-
|
|
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;
|
|
569
781
|
}
|
|
570
782
|
/**
|
|
571
783
|
* Check if game has ended (someone won)
|
|
@@ -574,7 +786,13 @@ var State = class {
|
|
|
574
786
|
*/
|
|
575
787
|
checkWin() {
|
|
576
788
|
const gameState = this.buildGameState();
|
|
577
|
-
|
|
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;
|
|
578
796
|
}
|
|
579
797
|
/**
|
|
580
798
|
* Cleanup when game ends (for plugin to reset internal state)
|
|
@@ -583,6 +801,7 @@ var State = class {
|
|
|
583
801
|
if (this.gamePlugin.cleanup) {
|
|
584
802
|
this.gamePlugin.cleanup();
|
|
585
803
|
}
|
|
804
|
+
consoleLogger.debug("[session:plugin] cleanup game");
|
|
586
805
|
}
|
|
587
806
|
/**
|
|
588
807
|
* Build game state for plugin
|
|
@@ -599,53 +818,6 @@ var State = class {
|
|
|
599
818
|
}
|
|
600
819
|
};
|
|
601
820
|
|
|
602
|
-
// utils/logger.ts
|
|
603
|
-
var logWith = (level) => (message, meta) => {
|
|
604
|
-
if (meta !== void 0) {
|
|
605
|
-
console[level](message, meta);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
console[level](message);
|
|
609
|
-
};
|
|
610
|
-
var consoleLogger = {
|
|
611
|
-
debug: logWith("debug"),
|
|
612
|
-
info: logWith("info"),
|
|
613
|
-
warn: logWith("warn"),
|
|
614
|
-
error: logWith("error")
|
|
615
|
-
};
|
|
616
|
-
|
|
617
|
-
// utils/serialization/json.ts
|
|
618
|
-
var decodeSafe = (raw) => {
|
|
619
|
-
if (typeof raw !== "string") {
|
|
620
|
-
return {
|
|
621
|
-
ok: false,
|
|
622
|
-
error: new TypeError("decodeSafe expects a serialized string")
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
try {
|
|
626
|
-
return { ok: true, value: JSON.parse(raw) };
|
|
627
|
-
} catch (error) {
|
|
628
|
-
return { ok: false, error };
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
// utils/protocol/session.ts
|
|
633
|
-
var parseSessionMessage = (data) => {
|
|
634
|
-
if (typeof data !== "string") {
|
|
635
|
-
if (!data || typeof data !== "object") {
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
return data;
|
|
639
|
-
}
|
|
640
|
-
const decoded = decodeSafe(
|
|
641
|
-
data
|
|
642
|
-
);
|
|
643
|
-
if (!decoded.ok || !decoded.value || typeof decoded.value !== "object") {
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
return decoded.value;
|
|
647
|
-
};
|
|
648
|
-
|
|
649
821
|
// session/net.ts
|
|
650
822
|
var NetClient = class {
|
|
651
823
|
constructor(client, bus, peerId) {
|
|
@@ -792,6 +964,13 @@ var ready = (command) => {
|
|
|
792
964
|
const state = getState();
|
|
793
965
|
const bus = getBus();
|
|
794
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
|
+
});
|
|
795
974
|
if (command.from === "local") {
|
|
796
975
|
if (!state.canAction("local", "READY")) {
|
|
797
976
|
console.warn("[Ready] Cannot dispatch READY from current state", {
|
|
@@ -806,6 +985,10 @@ var ready = (command) => {
|
|
|
806
985
|
sid: localSid
|
|
807
986
|
};
|
|
808
987
|
send(message);
|
|
988
|
+
consoleLogger.debug("[session:ready] local toggled", {
|
|
989
|
+
local: state.getState("local"),
|
|
990
|
+
remote: state.getState("remote")
|
|
991
|
+
});
|
|
809
992
|
return;
|
|
810
993
|
}
|
|
811
994
|
const remoteSid = command.sid;
|
|
@@ -825,6 +1008,10 @@ var ready = (command) => {
|
|
|
825
1008
|
}
|
|
826
1009
|
state.dispatch("remote", "READY");
|
|
827
1010
|
state.dispatch("local", "REMOTE_READY");
|
|
1011
|
+
consoleLogger.debug("[session:ready] remote toggled", {
|
|
1012
|
+
local: state.getState("local"),
|
|
1013
|
+
remote: state.getState("remote")
|
|
1014
|
+
});
|
|
828
1015
|
};
|
|
829
1016
|
|
|
830
1017
|
// session/handlers/start.ts
|
|
@@ -836,6 +1023,13 @@ var getNextStarter = (lastStarter) => {
|
|
|
836
1023
|
};
|
|
837
1024
|
var start = (command) => {
|
|
838
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
|
+
});
|
|
839
1033
|
if (command.from === "local") {
|
|
840
1034
|
if (!state.canAction("local", "START") || !state.canAction("remote", "REMOTE_START")) {
|
|
841
1035
|
console.warn("[Start] Cannot START from current state", {
|
|
@@ -847,6 +1041,10 @@ var start = (command) => {
|
|
|
847
1041
|
const nextStarter = getNextStarter(state.getLastStart());
|
|
848
1042
|
const localTarget2 = nextStarter === "local" ? "turn" : "remote_turn";
|
|
849
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
|
+
}
|
|
850
1048
|
state.setLastStart(nextStarter);
|
|
851
1049
|
state.dispatch("local", "START", localTarget2);
|
|
852
1050
|
state.dispatch("remote", "REMOTE_START", remoteTarget2);
|
|
@@ -854,6 +1052,7 @@ var start = (command) => {
|
|
|
854
1052
|
type: "START",
|
|
855
1053
|
payload: { starter: nextStarter === "local" ? "sender" : "receiver" }
|
|
856
1054
|
});
|
|
1055
|
+
consoleLogger.debug("[session:start] local started", { nextStarter });
|
|
857
1056
|
return;
|
|
858
1057
|
}
|
|
859
1058
|
const starterInfo = command.payload?.starter;
|
|
@@ -871,16 +1070,28 @@ var start = (command) => {
|
|
|
871
1070
|
const starter = starterInfo === "sender" ? "remote" : "local";
|
|
872
1071
|
const localTarget = starter === "local" ? "turn" : "remote_turn";
|
|
873
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
|
+
}
|
|
874
1077
|
state.setLastStart(starter);
|
|
875
1078
|
state.dispatch("local", "REMOTE_START", localTarget);
|
|
876
1079
|
state.dispatch("remote", "START", remoteTarget);
|
|
1080
|
+
consoleLogger.debug("[session:start] remote started", { starter });
|
|
877
1081
|
};
|
|
878
1082
|
|
|
879
1083
|
// session/handlers/move.ts
|
|
880
1084
|
var move = (command) => {
|
|
881
1085
|
const state = getState();
|
|
882
|
-
const bus = getBus();
|
|
883
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
|
+
});
|
|
884
1095
|
if (command.from === "local") {
|
|
885
1096
|
if (!state.canAction("local", "MOVE")) {
|
|
886
1097
|
console.warn("[Move] Cannot MOVE from current state", {
|
|
@@ -904,25 +1115,31 @@ var move = (command) => {
|
|
|
904
1115
|
player: "local",
|
|
905
1116
|
move: movePayload
|
|
906
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
|
+
});
|
|
907
1128
|
const winner2 = state.checkWin();
|
|
908
1129
|
if (winner2) {
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
type: "GAME_OVER",
|
|
913
|
-
payload: { winner: winner2, turn: turn2 }
|
|
1130
|
+
consoleLogger.debug("[session:move] game over detected", {
|
|
1131
|
+
winner: winner2,
|
|
1132
|
+
turn: turn2
|
|
914
1133
|
});
|
|
915
1134
|
state.dispatch("local", "GAME_OVER");
|
|
916
1135
|
state.dispatch("remote", "GAME_OVER");
|
|
917
1136
|
state.cleanupGame();
|
|
1137
|
+
consoleLogger.debug("[session:move] local game over applied", {
|
|
1138
|
+
winner: winner2,
|
|
1139
|
+
turn: turn2
|
|
1140
|
+
});
|
|
918
1141
|
return;
|
|
919
1142
|
}
|
|
920
|
-
const message = {
|
|
921
|
-
type: "MOVE",
|
|
922
|
-
turn: turn2,
|
|
923
|
-
payload: movePayload
|
|
924
|
-
};
|
|
925
|
-
send(message);
|
|
926
1143
|
return;
|
|
927
1144
|
}
|
|
928
1145
|
if (!state.canAction("remote", "MOVE")) {
|
|
@@ -949,31 +1166,63 @@ var move = (command) => {
|
|
|
949
1166
|
});
|
|
950
1167
|
const winner = state.checkWin();
|
|
951
1168
|
if (winner) {
|
|
952
|
-
|
|
953
|
-
bus.emit("GAME_OVER", { winner, turn }, "local");
|
|
1169
|
+
consoleLogger.debug("[session:move] game over detected", { winner, turn });
|
|
954
1170
|
state.dispatch("local", "GAME_OVER");
|
|
955
1171
|
state.dispatch("remote", "GAME_OVER");
|
|
956
1172
|
state.cleanupGame();
|
|
1173
|
+
consoleLogger.debug("[session:move] remote game over applied", {
|
|
1174
|
+
winner,
|
|
1175
|
+
turn
|
|
1176
|
+
});
|
|
1177
|
+
return;
|
|
957
1178
|
}
|
|
1179
|
+
consoleLogger.debug("[session:move] remote move applied", {
|
|
1180
|
+
turn,
|
|
1181
|
+
payload: movePayload
|
|
1182
|
+
});
|
|
958
1183
|
};
|
|
959
1184
|
|
|
960
1185
|
// session/handlers/request.ts
|
|
1186
|
+
var isRequestAction = (value) => value === "undo" || value === "restart";
|
|
961
1187
|
var request = (command) => {
|
|
962
1188
|
if (command.type !== "APPROVE" && command.type !== "REJECT") {
|
|
963
1189
|
return;
|
|
964
1190
|
}
|
|
965
1191
|
const state = getState();
|
|
966
|
-
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
|
+
});
|
|
967
1203
|
if (!action) {
|
|
968
1204
|
console.warn("[Request] No pending action", { commandType: command.type });
|
|
969
1205
|
return;
|
|
970
1206
|
}
|
|
971
|
-
const payload = command.payload;
|
|
972
1207
|
if (payload?.action && payload.action !== action) {
|
|
973
|
-
console.warn("[Request] Action mismatch", {
|
|
1208
|
+
console.warn("[Request] Action mismatch", {
|
|
1209
|
+
pending: action,
|
|
1210
|
+
payload: payload.action
|
|
1211
|
+
});
|
|
974
1212
|
return;
|
|
975
1213
|
}
|
|
976
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
|
+
}
|
|
977
1226
|
if (command.type === "APPROVE") {
|
|
978
1227
|
if (!state.canAction("local", "APPROVE")) {
|
|
979
1228
|
console.warn("[Request] Cannot APPROVE from current state");
|
|
@@ -987,6 +1236,7 @@ var request = (command) => {
|
|
|
987
1236
|
}
|
|
988
1237
|
send({ type: "APPROVE", payload: { action } });
|
|
989
1238
|
state.clearPendingStates();
|
|
1239
|
+
consoleLogger.debug("[session:request] local approved", { action });
|
|
990
1240
|
return;
|
|
991
1241
|
}
|
|
992
1242
|
if (!state.canAction("local", "REJECT")) {
|
|
@@ -999,11 +1249,14 @@ var request = (command) => {
|
|
|
999
1249
|
payload: { action, reason: payload?.reason ?? "rejected" }
|
|
1000
1250
|
});
|
|
1001
1251
|
state.clearPendingStates();
|
|
1252
|
+
consoleLogger.debug("[session:request] local rejected", { action });
|
|
1002
1253
|
return;
|
|
1003
1254
|
}
|
|
1004
1255
|
if (command.type === "APPROVE") {
|
|
1005
1256
|
if (!state.canAction("local", "APPROVE")) {
|
|
1006
|
-
console.warn(
|
|
1257
|
+
console.warn(
|
|
1258
|
+
"[Request] Cannot APPROVE from current state (remote approved)"
|
|
1259
|
+
);
|
|
1007
1260
|
return;
|
|
1008
1261
|
}
|
|
1009
1262
|
state.dispatchApprove();
|
|
@@ -1013,21 +1266,112 @@ var request = (command) => {
|
|
|
1013
1266
|
state.resetGame();
|
|
1014
1267
|
}
|
|
1015
1268
|
state.clearPendingStates();
|
|
1269
|
+
consoleLogger.debug("[session:request] remote approved", { action });
|
|
1016
1270
|
return;
|
|
1017
1271
|
}
|
|
1018
1272
|
if (!state.canAction("local", "REJECT")) {
|
|
1019
|
-
console.warn(
|
|
1273
|
+
console.warn(
|
|
1274
|
+
"[Request] Cannot REJECT from current state (remote rejected)"
|
|
1275
|
+
);
|
|
1020
1276
|
state.clearPendingStates();
|
|
1021
1277
|
return;
|
|
1022
1278
|
}
|
|
1023
1279
|
state.dispatchReject();
|
|
1024
1280
|
state.clearPendingStates();
|
|
1281
|
+
consoleLogger.debug("[session:request] remote rejected", { action });
|
|
1025
1282
|
};
|
|
1026
1283
|
|
|
1027
1284
|
// session/handlers/sync.ts
|
|
1028
1285
|
var fromPeerPerspective = (player) => player === "local" ? "remote" : "local";
|
|
1286
|
+
var getCurrentTurn = () => {
|
|
1287
|
+
const state = getState();
|
|
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;
|
|
1312
|
+
}
|
|
1313
|
+
state.dispatch("local", "SYNC", "syncing");
|
|
1314
|
+
}
|
|
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");
|
|
1331
|
+
}
|
|
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;
|
|
1337
|
+
if (payload.history && payload.history.length > 0) {
|
|
1338
|
+
state.replaceHistory(
|
|
1339
|
+
payload.history.map((entry) => ({
|
|
1340
|
+
...entry,
|
|
1341
|
+
player: mapPlayer(entry.player)
|
|
1342
|
+
}))
|
|
1343
|
+
);
|
|
1344
|
+
} else {
|
|
1345
|
+
state.clearHistory();
|
|
1346
|
+
}
|
|
1347
|
+
if (payload.lastStart) {
|
|
1348
|
+
state.setLastStart(mapPlayer(payload.lastStart));
|
|
1349
|
+
} else {
|
|
1350
|
+
state.setLastStart(null);
|
|
1351
|
+
}
|
|
1352
|
+
const nextPlayer = payload.resumeTurn ? mapPlayer(payload.resumeTurn) : payload.turn ? mapPlayer(payload.turn) : getCurrentTurn();
|
|
1353
|
+
consoleLogger.debug("[session:sync] state restored", {
|
|
1354
|
+
historyLength: state.getHistory().length,
|
|
1355
|
+
lastStart: state.getLastStart(),
|
|
1356
|
+
nextTurnPlayer: nextPlayer,
|
|
1357
|
+
mapped: mapPeerLabels
|
|
1358
|
+
});
|
|
1359
|
+
if (!enterSyncState()) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
state.dispatchSyncComplete(nextPlayer);
|
|
1363
|
+
};
|
|
1029
1364
|
var sync = (command) => {
|
|
1030
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
|
+
});
|
|
1031
1375
|
if (command.type === "SYNC_REQUEST") {
|
|
1032
1376
|
if (command.from === "local") {
|
|
1033
1377
|
if (state.getState("local") !== "syncing" && !state.canAction("local", "SYNC")) {
|
|
@@ -1041,51 +1385,21 @@ var sync = (command) => {
|
|
|
1041
1385
|
state.dispatch("remote", "SYNC", "syncing");
|
|
1042
1386
|
}
|
|
1043
1387
|
send({ type: "SYNC_REQUEST", from: "", payload: command.payload });
|
|
1388
|
+
consoleLogger.debug("[session:sync] request sent");
|
|
1044
1389
|
return;
|
|
1045
1390
|
}
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
};
|
|
1053
|
-
send({ type: "SYNC_STATE", from: "", payload: payload2 });
|
|
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
|
+
}
|
|
1054
1397
|
return;
|
|
1055
1398
|
}
|
|
1056
1399
|
if (command.type !== "SYNC_STATE") {
|
|
1057
1400
|
return;
|
|
1058
1401
|
}
|
|
1059
|
-
|
|
1060
|
-
if (payload.history && payload.history.length > 0) {
|
|
1061
|
-
state.replaceHistory(
|
|
1062
|
-
payload.history.map((entry) => ({
|
|
1063
|
-
...entry,
|
|
1064
|
-
player: fromPeerPerspective(entry.player)
|
|
1065
|
-
}))
|
|
1066
|
-
);
|
|
1067
|
-
} else {
|
|
1068
|
-
state.clearHistory();
|
|
1069
|
-
}
|
|
1070
|
-
if (payload.lastStart) {
|
|
1071
|
-
state.setLastStart(fromPeerPerspective(payload.lastStart));
|
|
1072
|
-
} else {
|
|
1073
|
-
state.setLastStart(null);
|
|
1074
|
-
}
|
|
1075
|
-
let nextPlayer;
|
|
1076
|
-
if (payload.resumeTurn) {
|
|
1077
|
-
nextPlayer = fromPeerPerspective(payload.resumeTurn);
|
|
1078
|
-
} else if (payload.turn) {
|
|
1079
|
-
nextPlayer = fromPeerPerspective(payload.turn);
|
|
1080
|
-
} else {
|
|
1081
|
-
nextPlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1082
|
-
}
|
|
1083
|
-
console.log("[Sync] Restored game state", {
|
|
1084
|
-
historyLength: state.getHistory().length,
|
|
1085
|
-
lastStart: state.getLastStart(),
|
|
1086
|
-
nextTurnPlayer: nextPlayer
|
|
1087
|
-
});
|
|
1088
|
-
state.dispatchSyncComplete(nextPlayer);
|
|
1402
|
+
restoreFromPayload(command.payload || {}, true);
|
|
1089
1403
|
};
|
|
1090
1404
|
|
|
1091
1405
|
// session/handlers/undo.ts
|
|
@@ -1094,6 +1408,14 @@ var undo = (command) => {
|
|
|
1094
1408
|
return;
|
|
1095
1409
|
}
|
|
1096
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
|
+
});
|
|
1097
1419
|
if (command.from === "local") {
|
|
1098
1420
|
if (state.hasPendingAction()) {
|
|
1099
1421
|
return;
|
|
@@ -1103,40 +1425,46 @@ var undo = (command) => {
|
|
|
1103
1425
|
return;
|
|
1104
1426
|
}
|
|
1105
1427
|
const localState = state.getState("local");
|
|
1106
|
-
const undoCount = localState === "turn" ?
|
|
1428
|
+
const undoCount = localState === "turn" ? 2 : 1;
|
|
1429
|
+
const rejectResumePlayer = localState === "turn" ? "local" : "remote";
|
|
1107
1430
|
if (state.getHistory().length < undoCount) {
|
|
1108
1431
|
console.warn("[Undo] Not enough history to undo", { count: undoCount });
|
|
1109
1432
|
return;
|
|
1110
1433
|
}
|
|
1111
|
-
state.initializeUndoRequest(undoCount,
|
|
1434
|
+
state.initializeUndoRequest(undoCount, rejectResumePlayer);
|
|
1112
1435
|
state.dispatch("local", "UNDO");
|
|
1113
1436
|
state.dispatch("remote", "REMOTE_UNDO");
|
|
1114
1437
|
send({ type: "UNDO", payload: { count: undoCount } });
|
|
1438
|
+
consoleLogger.debug("[session:undo] local requested", { undoCount });
|
|
1115
1439
|
return;
|
|
1116
1440
|
}
|
|
1117
1441
|
if (state.hasPendingAction()) {
|
|
1118
|
-
|
|
1442
|
+
bus.emit("REJECT", { action: "undo", reason: "busy" }, "local");
|
|
1119
1443
|
return;
|
|
1120
1444
|
}
|
|
1121
1445
|
if (!state.canAction("local", "REMOTE_UNDO")) {
|
|
1122
1446
|
console.warn("[Undo] Cannot accept remote UNDO request");
|
|
1123
|
-
|
|
1447
|
+
bus.emit("REJECT", { action: "undo", reason: "invalid_state" }, "local");
|
|
1124
1448
|
return;
|
|
1125
1449
|
}
|
|
1126
1450
|
const payload = command.payload;
|
|
1127
1451
|
const count = payload?.count === 2 ? 2 : 1;
|
|
1128
1452
|
if (payload?.count && payload.count !== 1 && payload.count !== 2) {
|
|
1129
|
-
|
|
1453
|
+
bus.emit("REJECT", { action: "undo", reason: "invalid" }, "local");
|
|
1130
1454
|
return;
|
|
1131
1455
|
}
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
1456
|
+
if (state.getHistory().length < count) {
|
|
1457
|
+
bus.emit("REJECT", { action: "undo", reason: "no_history" }, "local");
|
|
1134
1458
|
return;
|
|
1135
1459
|
}
|
|
1136
1460
|
const resumePlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1137
1461
|
state.initializeUndoRequest(count, resumePlayer);
|
|
1138
1462
|
state.dispatch("local", "REMOTE_UNDO");
|
|
1139
1463
|
state.dispatch("remote", "UNDO");
|
|
1464
|
+
consoleLogger.debug("[session:undo] remote requested", {
|
|
1465
|
+
count,
|
|
1466
|
+
resumePlayer
|
|
1467
|
+
});
|
|
1140
1468
|
};
|
|
1141
1469
|
|
|
1142
1470
|
// session/handlers/restart.ts
|
|
@@ -1145,6 +1473,14 @@ var restart = (command) => {
|
|
|
1145
1473
|
return;
|
|
1146
1474
|
}
|
|
1147
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
|
+
});
|
|
1148
1484
|
if (command.from === "local") {
|
|
1149
1485
|
if (state.hasPendingAction()) {
|
|
1150
1486
|
return;
|
|
@@ -1158,21 +1494,23 @@ var restart = (command) => {
|
|
|
1158
1494
|
state.dispatch("local", "RESTART");
|
|
1159
1495
|
state.dispatch("remote", "REMOTE_RESTART");
|
|
1160
1496
|
send({ type: "RESTART" });
|
|
1497
|
+
consoleLogger.debug("[session:restart] local requested", { resumePlayer: resumePlayer2 });
|
|
1161
1498
|
return;
|
|
1162
1499
|
}
|
|
1163
1500
|
if (state.hasPendingAction()) {
|
|
1164
|
-
|
|
1501
|
+
bus.emit("REJECT", { action: "restart", reason: "busy" }, "local");
|
|
1165
1502
|
return;
|
|
1166
1503
|
}
|
|
1167
1504
|
if (!state.canAction("local", "REMOTE_RESTART")) {
|
|
1168
1505
|
console.warn("[Restart] Cannot accept remote RESTART request");
|
|
1169
|
-
|
|
1506
|
+
bus.emit("REJECT", { action: "restart", reason: "invalid_state" }, "local");
|
|
1170
1507
|
return;
|
|
1171
1508
|
}
|
|
1172
1509
|
const resumePlayer = state.getState("local") === "turn" ? "local" : "remote";
|
|
1173
1510
|
state.initializeRestartRequest(resumePlayer);
|
|
1174
1511
|
state.dispatch("local", "REMOTE_RESTART");
|
|
1175
1512
|
state.dispatch("remote", "RESTART");
|
|
1513
|
+
consoleLogger.debug("[session:restart] remote requested", { resumePlayer });
|
|
1176
1514
|
};
|
|
1177
1515
|
|
|
1178
1516
|
// session/handlers/offLine.ts
|
|
@@ -1182,6 +1520,14 @@ var offline = (command) => {
|
|
|
1182
1520
|
}
|
|
1183
1521
|
const state = getState();
|
|
1184
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
|
+
});
|
|
1185
1531
|
if (command.type === "OFFLINE") {
|
|
1186
1532
|
if (state.hasPendingAction()) {
|
|
1187
1533
|
const resumeTurn = state.getResumeTurn();
|
|
@@ -1198,9 +1544,16 @@ var offline = (command) => {
|
|
|
1198
1544
|
const currentTurn = state.getResumeTurn() ?? (state.getState("local") === "turn" ? "local" : "remote");
|
|
1199
1545
|
state.setResumeTurn(currentTurn);
|
|
1200
1546
|
state.dispatch("remote", "OFFLINE", "offline");
|
|
1547
|
+
consoleLogger.debug("[session:connection] remote offline", { currentTurn });
|
|
1201
1548
|
return;
|
|
1202
1549
|
}
|
|
1203
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
|
+
);
|
|
1204
1557
|
return;
|
|
1205
1558
|
}
|
|
1206
1559
|
if (!state.canAction("remote", "ONLINE")) {
|
|
@@ -1209,6 +1562,7 @@ var offline = (command) => {
|
|
|
1209
1562
|
}
|
|
1210
1563
|
state.dispatch("remote", "ONLINE", "syncing");
|
|
1211
1564
|
bus.emit("SYNC_REQUEST", void 0, "local");
|
|
1565
|
+
consoleLogger.debug("[session:connection] remote online, sync requested");
|
|
1212
1566
|
};
|
|
1213
1567
|
|
|
1214
1568
|
// session/handlers/busRegister.ts
|