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.
- package/dist/session/index.js +483 -119
- package/dist/session/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/serialization-smoke.mjs +371 -106
package/dist/session/index.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
727
|
+
this.dispatchPair(
|
|
728
|
+
"SYNC_COMPLETE",
|
|
729
|
+
"turn",
|
|
730
|
+
"SYNC_COMPLETE",
|
|
731
|
+
"remote_turn"
|
|
732
|
+
);
|
|
539
733
|
} else {
|
|
540
|
-
this.dispatchPair(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
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
|
|
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", {
|
|
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(
|
|
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(
|
|
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
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
-
|
|
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" ?
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1463
|
+
bus.emit("REJECT", { action: "undo", reason: "invalid" }, "local");
|
|
1130
1464
|
return;
|
|
1131
1465
|
}
|
|
1132
|
-
if (
|
|
1133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|