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.
@@ -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
- return this.history.pop() ?? null;
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("SYNC_COMPLETE", "turn", "SYNC_COMPLETE", "remote_turn");
726
+ this.dispatchPair(
727
+ "SYNC_COMPLETE",
728
+ "turn",
729
+ "SYNC_COMPLETE",
730
+ "remote_turn"
731
+ );
539
732
  } else {
540
- this.dispatchPair("SYNC_COMPLETE", "remote_turn", "SYNC_COMPLETE", "turn");
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
- return this.gamePlugin.validateMove(move2, gameState);
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
- return this.gamePlugin.checkWin(gameState, this.getHistory());
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
- console.log("[Move] Game over, winner:", winner2);
910
- bus.emit("GAME_OVER", { winner: winner2, turn: turn2 }, "local");
911
- send({
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
- console.log("[Move] Game over, winner:", winner);
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 action = state.getPendingAction();
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", { pending: action, payload: payload.action });
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("[Request] Cannot APPROVE from current state (remote approved)");
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("[Request] Cannot REJECT from current state (remote rejected)");
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 payload2 = {
1047
- history: state.getHistory(),
1048
- lastStart: state.getLastStart(),
1049
- turn: state.getState("local") === "turn" ? "local" : "remote",
1050
- resumeTurn: state.getResumeTurn()
1051
- // Send back the saved resume turn
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
- const payload = command.payload || {};
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" ? 1 : 2;
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, "local");
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
- send({ type: "REJECT", payload: { action: "undo", reason: "busy" } });
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
- send({ type: "REJECT", payload: { action: "undo", reason: "invalid_state" } });
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
- send({ type: "REJECT", payload: { action: "undo", reason: "invalid" } });
1453
+ bus.emit("REJECT", { action: "undo", reason: "invalid" }, "local");
1130
1454
  return;
1131
1455
  }
1132
- if (count === 2 && state.getHistory().length < 2) {
1133
- send({ type: "REJECT", payload: { action: "undo", reason: "no_history" } });
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
- send({ type: "REJECT", payload: { action: "restart", reason: "busy" } });
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
- send({ type: "REJECT", payload: { action: "restart", reason: "invalid_state" } });
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