p2p-lockstep-kit-session 0.1.7 → 0.1.9

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