p2p-lockstep-kit-session 0.1.8 → 0.1.10

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