p2p-lockstep-kit-session 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- import assert from "node:assert/strict";
2
- import { createSession } from "../dist/session/index.js";
1
+ import assert from 'node:assert/strict';
2
+ import { createSession } from '../dist/session/index.js';
3
3
 
4
4
  const waitForBus = () => new Promise((resolve) => setTimeout(resolve, 0));
5
5
 
@@ -14,7 +14,7 @@ class BoundaryClient {
14
14
 
15
15
  onStateChange(handler) {
16
16
  this.stateHandler = handler;
17
- handler("passive");
17
+ handler('passive');
18
18
  }
19
19
 
20
20
  onRemoteStream() {}
@@ -24,7 +24,7 @@ class BoundaryClient {
24
24
  }
25
25
 
26
26
  connect() {
27
- this.stateHandler?.("connected");
27
+ this.stateHandler?.('connected');
28
28
  }
29
29
 
30
30
  inbound(data) {
@@ -34,9 +34,9 @@ class BoundaryClient {
34
34
 
35
35
  const createConnectedSession = () => {
36
36
  const client = new BoundaryClient();
37
- const session = createSession(client, "demo-room");
37
+ const session = createSession(client, 'demo-room');
38
38
 
39
- session.net.setPeerIds({ local: "local", remote: "remote" });
39
+ session.net.setPeerIds({ local: 'local', remote: 'remote' });
40
40
  client.connect();
41
41
 
42
42
  return { client, session };
@@ -44,23 +44,51 @@ const createConnectedSession = () => {
44
44
 
45
45
  const startGame = async () => {
46
46
  const runtime = createConnectedSession();
47
- runtime.client.inbound({ type: "READY", sid: "demo-room", from: "remote" });
47
+ runtime.client.inbound({ type: 'READY', sid: 'demo-room', from: 'remote' });
48
48
  await waitForBus();
49
49
  runtime.session.actions.start();
50
50
  await waitForBus();
51
51
 
52
+ assert.match(runtime.session.state.getState('local'), /^(turn|remote_turn)$/);
52
53
  assert.match(
53
- runtime.session.state.getState("local"),
54
+ runtime.session.state.getState('remote'),
54
55
  /^(turn|remote_turn)$/,
55
56
  );
56
- assert.match(
57
- runtime.session.state.getState("remote"),
58
- /^(turn|remote_turn)$/,
57
+
58
+ return runtime;
59
+ };
60
+
61
+ const startGameWithFirstPlayer = async (firstPlayer) => {
62
+ const runtime = createConnectedSession();
63
+ runtime.client.inbound({ type: 'READY', sid: 'demo-room', from: 'remote' });
64
+ await waitForBus();
65
+ runtime.session.state.setLastStart(
66
+ firstPlayer === 'local' ? 'remote' : 'local',
67
+ );
68
+ runtime.session.actions.start();
69
+ await waitForBus();
70
+
71
+ assert.equal(
72
+ runtime.session.state.getState('local'),
73
+ firstPlayer === 'local' ? 'turn' : 'remote_turn',
74
+ );
75
+ assert.equal(
76
+ runtime.session.state.getState('remote'),
77
+ firstPlayer === 'local' ? 'remote_turn' : 'turn',
59
78
  );
60
79
 
61
80
  return runtime;
62
81
  };
63
82
 
83
+ const oneMoveWinPlugin = {
84
+ validateMove() {
85
+ return { valid: true };
86
+ },
87
+ checkWin(_gameState, history) {
88
+ return history.at(-1)?.player ?? null;
89
+ },
90
+ };
91
+
64
92
  {
65
93
  const { client, session } = createConnectedSession();
66
94
  await waitForBus();
@@ -69,22 +97,22 @@ const startGame = async () => {
69
97
  await waitForBus();
70
98
 
71
99
  const sent = client.sent.at(-1);
72
- assert.equal(typeof sent, "object");
73
- assert.equal(sent.type, "READY");
74
- assert.equal(sent.sid, "demo-room");
75
- assert.equal(session.state.getState("local"), "ready");
76
- assert.equal(session.state.getState("remote"), "could_start");
100
+ assert.equal(typeof sent, 'object');
101
+ assert.equal(sent.type, 'READY');
102
+ assert.equal(sent.sid, 'demo-room');
103
+ assert.equal(session.state.getState('local'), 'ready');
104
+ assert.equal(session.state.getState('remote'), 'could_start');
77
105
  }
78
106
 
79
107
  {
80
108
  const { client, session } = createConnectedSession();
81
109
  await waitForBus();
82
110
 
83
- client.inbound({ type: "READY", sid: "demo-room", from: "remote" });
111
+ client.inbound({ type: 'READY', sid: 'demo-room', from: 'remote' });
84
112
  await waitForBus();
85
113
 
86
- assert.equal(session.state.getState("local"), "could_start");
87
- assert.equal(session.state.getState("remote"), "ready");
114
+ assert.equal(session.state.getState('local'), 'could_start');
115
+ assert.equal(session.state.getState('remote'), 'ready');
88
116
  }
89
117
 
90
118
  {
@@ -92,12 +120,12 @@ const startGame = async () => {
92
120
  await waitForBus();
93
121
 
94
122
  client.inbound(
95
- JSON.stringify({ type: "READY", sid: "demo-room", from: "remote" }),
123
+ JSON.stringify({ type: 'READY', sid: 'demo-room', from: 'remote' }),
96
124
  );
97
125
  await waitForBus();
98
126
 
99
- assert.equal(session.state.getState("local"), "could_start");
100
- assert.equal(session.state.getState("remote"), "ready");
127
+ assert.equal(session.state.getState('local'), 'could_start');
128
+ assert.equal(session.state.getState('remote'), 'ready');
101
129
  }
102
130
 
103
131
  {
@@ -113,51 +141,276 @@ const startGame = async () => {
113
141
  session.actions.restart();
114
142
  await waitForBus();
115
143
 
116
- assert.equal(session.state.getPendingAction(), "restart");
117
- assert.equal(session.state.getState("local"), "waiting_approval");
118
- assert.equal(session.state.getState("remote"), "approving");
144
+ assert.equal(session.state.getPendingAction(), 'restart');
145
+ assert.equal(session.state.getState('local'), 'waiting_approval');
146
+ assert.equal(session.state.getState('remote'), 'approving');
119
147
 
120
148
  client.inbound({
121
- type: "APPROVE",
122
- payload: { action: "restart" },
123
- from: "remote",
149
+ type: 'APPROVE',
150
+ payload: { action: 'restart' },
151
+ from: 'remote',
124
152
  });
125
153
  await waitForBus();
126
154
 
127
155
  assert.equal(session.state.getPendingAction(), null);
128
- assert.equal(session.state.getState("local"), "idle");
129
- assert.equal(session.state.getState("remote"), "idle");
156
+ assert.equal(session.state.getState('local'), 'idle');
157
+ assert.equal(session.state.getState('remote'), 'idle');
130
158
  assert.equal(
131
159
  snapshots.some(
132
160
  (snapshot) =>
133
- snapshot.localState === "idle" &&
134
- snapshot.remoteState === "idle" &&
135
- snapshot.pendingAction === "restart",
161
+ snapshot.localState === 'idle' &&
162
+ snapshot.remoteState === 'idle' &&
163
+ snapshot.pendingAction === 'restart',
136
164
  ),
137
165
  false,
138
166
  );
139
167
  }
140
168
 
169
+ {
170
+ const { client, session } = await startGame();
171
+ const snapshots = [];
172
+ session.observer.subscribe({
173
+ onStateChange(snapshot) {
174
+ snapshots.push(snapshot);
175
+ },
176
+ onGameEvent() {},
177
+ });
178
+
179
+ if (session.state.getState('local') !== 'turn') {
180
+ client.inbound({
181
+ type: 'MOVE',
182
+ payload: { step: 'remote-first' },
183
+ from: 'remote',
184
+ });
185
+ await waitForBus();
186
+ snapshots.splice(0, snapshots.length);
187
+ }
188
+
189
+ const beforeHistory = session.state.getHistory().length;
190
+ session.actions.move({ step: 'local-move' });
191
+ await waitForBus();
192
+
193
+ assert.equal(session.state.getHistory().length, beforeHistory + 1);
194
+ assert.equal(
195
+ snapshots.some((snapshot) => snapshot.history.length === beforeHistory + 1),
196
+ true,
197
+ );
198
+ }
199
+
200
+ {
201
+ const { client, session } = await startGameWithFirstPlayer('local');
202
+ session.state.setGamePlugin(oneMoveWinPlugin);
203
+
204
+ session.actions.move({ step: 'winning-local-move' });
205
+ await waitForBus();
206
+
207
+ assert.equal(client.sent.at(-1).type, 'MOVE');
208
+ assert.equal(session.state.getState('local'), 'idle');
209
+ assert.equal(session.state.getState('remote'), 'idle');
210
+ assert.equal(session.state.getHistory().length, 1);
211
+ assert.equal(session.state.getHistory()[0].move.step, 'winning-local-move');
212
+ }
213
+
214
+ {
215
+ const { client, session } = await startGameWithFirstPlayer('remote');
216
+ session.state.setGamePlugin(oneMoveWinPlugin);
217
+
218
+ client.inbound({
219
+ type: 'MOVE',
220
+ payload: { step: 'winning-remote-move' },
221
+ from: 'remote',
222
+ });
223
+ await waitForBus();
224
+
225
+ assert.equal(session.state.getState('local'), 'idle');
226
+ assert.equal(session.state.getState('remote'), 'idle');
227
+ assert.equal(session.state.getHistory().length, 1);
228
+ assert.equal(session.state.getHistory()[0].move.step, 'winning-remote-move');
229
+ }
230
+
231
+ {
232
+ const { client, session } = await startGameWithFirstPlayer('local');
233
+ session.state.setGamePlugin(oneMoveWinPlugin);
234
+
235
+ session.actions.move({ step: 'winning-local-move' });
236
+ await waitForBus();
237
+ assert.equal(session.state.getHistory().length, 1);
238
+ assert.equal(session.state.getState('local'), 'idle');
239
+ assert.equal(session.state.getState('remote'), 'idle');
240
+
241
+ client.inbound({ type: 'READY', sid: 'demo-room', from: 'remote' });
242
+ await waitForBus();
243
+ session.actions.start();
244
+ await waitForBus();
245
+
246
+ assert.equal(session.state.getHistory().length, 0);
247
+ assert.match(session.state.getState('local'), /^(turn|remote_turn)$/);
248
+ assert.match(session.state.getState('remote'), /^(turn|remote_turn)$/);
249
+ }
250
+
251
+ {
252
+ const { client, session } = await startGameWithFirstPlayer('remote');
253
+ session.state.setGamePlugin(oneMoveWinPlugin);
254
+
255
+ client.inbound({
256
+ type: 'MOVE',
257
+ payload: { step: 'winning-remote-move' },
258
+ from: 'remote',
259
+ });
260
+ await waitForBus();
261
+ assert.equal(session.state.getHistory().length, 1);
262
+ assert.equal(session.state.getState('local'), 'idle');
263
+ assert.equal(session.state.getState('remote'), 'idle');
264
+
265
+ session.actions.ready();
266
+ await waitForBus();
267
+ client.inbound({
268
+ type: 'START',
269
+ payload: { starter: 'sender' },
270
+ from: 'remote',
271
+ });
272
+ await waitForBus();
273
+
274
+ assert.equal(session.state.getHistory().length, 0);
275
+ assert.equal(session.state.getState('local'), 'remote_turn');
276
+ assert.equal(session.state.getState('remote'), 'turn');
277
+ }
278
+
279
+ {
280
+ const { client, session } = await startGameWithFirstPlayer('local');
281
+
282
+ session.actions.move({ step: 'local-just-moved' });
283
+ await waitForBus();
284
+ assert.equal(session.state.getState('local'), 'remote_turn');
285
+ assert.equal(session.state.getHistory().length, 1);
286
+
287
+ session.actions.undo();
288
+ await waitForBus();
289
+
290
+ assert.equal(client.sent.at(-1).type, 'UNDO');
291
+ assert.equal(client.sent.at(-1).payload.count, 1);
292
+ assert.equal(session.state.getPendingUndoCount(), 1);
293
+
294
+ client.inbound({
295
+ type: 'APPROVE',
296
+ payload: { action: 'undo' },
297
+ from: 'remote',
298
+ });
299
+ await waitForBus();
300
+
301
+ assert.equal(session.state.getHistory().length, 0);
302
+ assert.equal(session.state.getState('local'), 'turn');
303
+ assert.equal(session.state.getState('remote'), 'remote_turn');
304
+ }
305
+
306
+ {
307
+ const { client, session } = await startGameWithFirstPlayer('local');
308
+
309
+ session.actions.move({ step: 'local-first' });
310
+ await waitForBus();
311
+ client.inbound({
312
+ type: 'MOVE',
313
+ payload: { step: 'remote-reply' },
314
+ from: 'remote',
315
+ });
316
+ await waitForBus();
317
+ assert.equal(session.state.getState('local'), 'turn');
318
+ assert.equal(session.state.getHistory().length, 2);
319
+
320
+ session.actions.undo();
321
+ await waitForBus();
322
+
323
+ assert.equal(client.sent.at(-1).type, 'UNDO');
324
+ assert.equal(client.sent.at(-1).payload.count, 2);
325
+ assert.equal(session.state.getPendingUndoCount(), 2);
326
+
327
+ client.inbound({
328
+ type: 'APPROVE',
329
+ payload: { action: 'undo' },
330
+ from: 'remote',
331
+ });
332
+ await waitForBus();
333
+
334
+ assert.equal(session.state.getHistory().length, 0);
335
+ assert.equal(session.state.getState('local'), 'turn');
336
+ assert.equal(session.state.getState('remote'), 'remote_turn');
337
+ }
338
+
339
+ {
340
+ const { client, session } = await startGameWithFirstPlayer('local');
341
+
342
+ session.actions.move({ step: 'local-just-moved' });
343
+ await waitForBus();
344
+ assert.equal(session.state.getState('local'), 'remote_turn');
345
+
346
+ session.actions.undo();
347
+ await waitForBus();
348
+ client.inbound({
349
+ type: 'REJECT',
350
+ payload: { action: 'undo' },
351
+ from: 'remote',
352
+ });
353
+ await waitForBus();
354
+
355
+ assert.equal(session.state.getHistory().length, 1);
356
+ assert.equal(session.state.getState('local'), 'remote_turn');
357
+ assert.equal(session.state.getState('remote'), 'turn');
358
+ assert.equal(
359
+ client.sent.some((message) => message.type === 'UNDO'),
360
+ true,
361
+ );
362
+ }
363
+
364
+ {
365
+ const { client } = createConnectedSession();
366
+ await waitForBus();
367
+
368
+ client.inbound({ type: 'UNDO', payload: { count: 3 }, from: 'remote' });
369
+ await waitForBus();
370
+ await waitForBus();
371
+
372
+ const sent = client.sent.at(-1);
373
+ assert.equal(sent.type, 'REJECT');
374
+ assert.equal(sent.payload.action, 'undo');
375
+ assert.equal(sent.payload.reason, 'invalid_state');
376
+ }
377
+
378
+ {
379
+ const { client, session } = createConnectedSession();
380
+ await waitForBus();
381
+
382
+ session.actions.ready();
383
+ await waitForBus();
384
+ client.inbound({ type: 'RESTART', from: 'remote' });
385
+ await waitForBus();
386
+ await waitForBus();
387
+
388
+ const sent = client.sent.at(-1);
389
+ assert.equal(sent.type, 'REJECT');
390
+ assert.equal(sent.payload.action, 'restart');
391
+ assert.equal(sent.payload.reason, 'invalid_state');
392
+ }
393
+
141
394
  {
142
395
  const { client, session } = await startGame();
143
396
  const beforeRestart = {
144
- local: session.state.getState("local"),
145
- remote: session.state.getState("remote"),
397
+ local: session.state.getState('local'),
398
+ remote: session.state.getState('remote'),
146
399
  };
147
400
 
148
401
  session.actions.restart();
149
402
  await waitForBus();
150
403
 
151
404
  client.inbound({
152
- type: "REJECT",
153
- payload: { action: "restart" },
154
- from: "remote",
405
+ type: 'REJECT',
406
+ payload: { action: 'restart' },
407
+ from: 'remote',
155
408
  });
156
409
  await waitForBus();
157
410
 
158
411
  assert.equal(session.state.getPendingAction(), null);
159
- assert.equal(session.state.getState("local"), beforeRestart.local);
160
- assert.equal(session.state.getState("remote"), beforeRestart.remote);
412
+ assert.equal(session.state.getState('local'), beforeRestart.local);
413
+ assert.equal(session.state.getState('remote'), beforeRestart.remote);
161
414
  }
162
415
 
163
416
  {
@@ -165,42 +418,42 @@ const startGame = async () => {
165
418
  session.actions.restart();
166
419
  await waitForBus();
167
420
 
168
- assert.equal(session.state.getPendingAction(), "restart");
421
+ assert.equal(session.state.getPendingAction(), 'restart');
169
422
 
170
- session.bus.dispatch({ type: "OFFLINE", from: "local" });
423
+ session.bus.dispatch({ type: 'OFFLINE', from: 'local' });
171
424
  await waitForBus();
172
425
 
173
426
  assert.equal(session.state.getPendingAction(), null);
174
- assert.equal(session.state.getState("local"), "syncing");
175
- assert.equal(session.state.getState("remote"), "offline");
427
+ assert.equal(session.state.getState('local'), 'syncing');
428
+ assert.equal(session.state.getState('remote'), 'offline');
176
429
 
177
- session.bus.dispatch({ type: "ONLINE", from: "local" });
430
+ session.bus.dispatch({ type: 'ONLINE', from: 'local' });
178
431
  await waitForBus();
179
432
  await waitForBus();
180
433
 
181
434
  assert.equal(session.state.getPendingAction(), null);
182
- assert.equal(session.state.getState("local"), "syncing");
183
- assert.equal(session.state.getState("remote"), "syncing");
184
- assert.equal(client.sent.at(-1).type, "SYNC_REQUEST");
435
+ assert.equal(session.state.getState('local'), 'syncing');
436
+ assert.equal(session.state.getState('remote'), 'syncing');
437
+ assert.equal(client.sent.at(-1).type, 'SYNC_REQUEST');
185
438
 
186
439
  client.inbound({
187
- type: "SYNC_STATE",
440
+ type: 'SYNC_STATE',
188
441
  payload: {
189
- history: [{ turn: 1, player: "local", move: { step: "peer-move" } }],
190
- lastStart: "local",
191
- turn: "local",
192
- resumeTurn: "local",
442
+ history: [{ turn: 1, player: 'local', move: { step: 'peer-move' } }],
443
+ lastStart: 'local',
444
+ turn: 'local',
445
+ resumeTurn: 'local',
193
446
  },
194
- from: "remote",
447
+ from: 'remote',
195
448
  });
196
449
  await waitForBus();
197
450
 
198
451
  assert.equal(session.state.getPendingAction(), null);
199
- assert.equal(session.state.getState("local"), "remote_turn");
200
- assert.equal(session.state.getState("remote"), "turn");
452
+ assert.equal(session.state.getState('local'), 'remote_turn');
453
+ assert.equal(session.state.getState('remote'), 'turn');
201
454
  assert.equal(session.state.getHistory().length, 1);
202
- assert.equal(session.state.getHistory()[0].player, "remote");
203
- assert.equal(session.state.getLastStart(), "remote");
455
+ assert.equal(session.state.getHistory()[0].player, 'remote');
456
+ assert.equal(session.state.getLastStart(), 'remote');
204
457
  }
205
458
 
206
459
  {
@@ -213,28 +466,28 @@ const startGame = async () => {
213
466
  onGameEvent() {},
214
467
  });
215
468
 
216
- client.inbound({ type: "RESTART", from: "remote" });
469
+ client.inbound({ type: 'RESTART', from: 'remote' });
217
470
  await waitForBus();
218
471
 
219
- assert.equal(session.state.getPendingAction(), "restart");
220
- assert.equal(session.state.getState("local"), "approving");
221
- assert.equal(session.state.getState("remote"), "waiting_approval");
472
+ assert.equal(session.state.getPendingAction(), 'restart');
473
+ assert.equal(session.state.getState('local'), 'approving');
474
+ assert.equal(session.state.getState('remote'), 'waiting_approval');
222
475
 
223
476
  session.actions.approve();
224
477
  await waitForBus();
225
478
 
226
479
  const sent = client.sent.at(-1);
227
- assert.equal(sent.type, "APPROVE");
228
- assert.equal(sent.payload.action, "restart");
480
+ assert.equal(sent.type, 'APPROVE');
481
+ assert.equal(sent.payload.action, 'restart');
229
482
  assert.equal(session.state.getPendingAction(), null);
230
- assert.equal(session.state.getState("local"), "idle");
231
- assert.equal(session.state.getState("remote"), "idle");
483
+ assert.equal(session.state.getState('local'), 'idle');
484
+ assert.equal(session.state.getState('remote'), 'idle');
232
485
  assert.equal(
233
486
  snapshots.some(
234
487
  (snapshot) =>
235
- snapshot.localState === "idle" &&
236
- snapshot.remoteState === "idle" &&
237
- snapshot.pendingAction === "restart",
488
+ snapshot.localState === 'idle' &&
489
+ snapshot.remoteState === 'idle' &&
490
+ snapshot.pendingAction === 'restart',
238
491
  ),
239
492
  false,
240
493
  );
@@ -243,65 +496,92 @@ const startGame = async () => {
243
496
  {
244
497
  const { client, session } = await startGame();
245
498
  const beforeRestart = {
246
- local: session.state.getState("local"),
247
- remote: session.state.getState("remote"),
499
+ local: session.state.getState('local'),
500
+ remote: session.state.getState('remote'),
248
501
  };
249
502
 
250
- client.inbound({ type: "RESTART", from: "remote" });
503
+ client.inbound({ type: 'RESTART', from: 'remote' });
251
504
  await waitForBus();
252
505
 
253
506
  session.actions.reject();
254
507
  await waitForBus();
255
508
 
256
509
  const sent = client.sent.at(-1);
257
- assert.equal(sent.type, "REJECT");
258
- assert.equal(sent.payload.action, "restart");
510
+ assert.equal(sent.type, 'REJECT');
511
+ assert.equal(sent.payload.action, 'restart');
259
512
  assert.equal(session.state.getPendingAction(), null);
260
- assert.equal(session.state.getState("local"), beforeRestart.local);
261
- assert.equal(session.state.getState("remote"), beforeRestart.remote);
513
+ assert.equal(session.state.getState('local'), beforeRestart.local);
514
+ assert.equal(session.state.getState('remote'), beforeRestart.remote);
262
515
  }
263
516
 
264
517
  {
265
518
  const { client, session } = await startGame();
266
- client.inbound({ type: "RESTART", from: "remote" });
519
+ client.inbound({ type: 'RESTART', from: 'remote' });
267
520
  await waitForBus();
268
521
 
269
- assert.equal(session.state.getPendingAction(), "restart");
522
+ assert.equal(session.state.getPendingAction(), 'restart');
270
523
 
271
- session.bus.dispatch({ type: "OFFLINE", from: "local" });
524
+ session.bus.dispatch({ type: 'OFFLINE', from: 'local' });
272
525
  await waitForBus();
273
526
 
274
527
  assert.equal(session.state.getPendingAction(), null);
275
- assert.equal(session.state.getState("local"), "syncing");
276
- assert.equal(session.state.getState("remote"), "offline");
528
+ assert.equal(session.state.getState('local'), 'syncing');
529
+ assert.equal(session.state.getState('remote'), 'offline');
277
530
 
278
- session.bus.dispatch({ type: "ONLINE", from: "local" });
531
+ session.bus.dispatch({ type: 'ONLINE', from: 'local' });
279
532
  await waitForBus();
280
533
  await waitForBus();
281
534
 
282
535
  assert.equal(session.state.getPendingAction(), null);
283
- assert.equal(session.state.getState("local"), "syncing");
284
- assert.equal(session.state.getState("remote"), "syncing");
285
- assert.equal(client.sent.at(-1).type, "SYNC_REQUEST");
536
+ assert.equal(session.state.getState('local'), 'syncing');
537
+ assert.equal(session.state.getState('remote'), 'syncing');
538
+ assert.equal(client.sent.at(-1).type, 'SYNC_REQUEST');
286
539
 
287
540
  client.inbound({
288
- type: "SYNC_STATE",
541
+ type: 'SYNC_STATE',
289
542
  payload: {
290
- history: [{ turn: 1, player: "remote", move: { step: "local-move" } }],
291
- lastStart: "remote",
292
- turn: "remote",
293
- resumeTurn: "remote",
543
+ history: [{ turn: 1, player: 'remote', move: { step: 'local-move' } }],
544
+ lastStart: 'remote',
545
+ turn: 'remote',
546
+ resumeTurn: 'remote',
294
547
  },
295
- from: "remote",
548
+ from: 'remote',
296
549
  });
297
550
  await waitForBus();
298
551
 
299
552
  assert.equal(session.state.getPendingAction(), null);
300
- assert.equal(session.state.getState("local"), "turn");
301
- assert.equal(session.state.getState("remote"), "remote_turn");
553
+ assert.equal(session.state.getState('local'), 'turn');
554
+ assert.equal(session.state.getState('remote'), 'remote_turn');
555
+ assert.equal(session.state.getHistory().length, 1);
556
+ assert.equal(session.state.getHistory()[0].player, 'local');
557
+ assert.equal(session.state.getLastStart(), 'local');
558
+ }
559
+
560
+ {
561
+ const { client, session } = await startGameWithFirstPlayer('local');
562
+ session.actions.move({ step: 'local-before-disconnect' });
563
+ await waitForBus();
564
+
565
+ assert.equal(session.state.getState('local'), 'remote_turn');
566
+ assert.equal(session.state.getState('remote'), 'turn');
567
+ assert.equal(session.state.getHistory().length, 1);
568
+
569
+ session.bus.dispatch({ type: 'OFFLINE', from: 'local' });
570
+ await waitForBus();
571
+
572
+ assert.equal(session.state.getState('local'), 'remote_turn');
573
+ assert.equal(session.state.getState('remote'), 'offline');
574
+
575
+ client.inbound({ type: 'SYNC_REQUEST', from: 'remote' });
576
+ await waitForBus();
577
+
578
+ const sent = client.sent.at(-1);
579
+ assert.equal(sent.type, 'SYNC_STATE');
580
+ assert.equal(sent.payload.resumeTurn, 'remote');
581
+ assert.equal(session.state.getState('local'), 'remote_turn');
582
+ assert.equal(session.state.getState('remote'), 'turn');
302
583
  assert.equal(session.state.getHistory().length, 1);
303
- assert.equal(session.state.getHistory()[0].player, "local");
304
- assert.equal(session.state.getLastStart(), "local");
584
+ assert.equal(session.state.getHistory()[0].player, 'local');
305
585
  }
306
586
 
307
- console.log("serialization smoke passed");
587
+ console.log('serialization smoke passed');