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.
@@ -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,319 @@ 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',
408
+ });
409
+ await waitForBus();
410
+
411
+ assert.equal(session.state.getPendingAction(), null);
412
+ assert.equal(session.state.getState('local'), beforeRestart.local);
413
+ assert.equal(session.state.getState('remote'), beforeRestart.remote);
414
+ }
415
+
416
+ {
417
+ const { client, session } = await startGame();
418
+ session.actions.restart();
419
+ await waitForBus();
420
+
421
+ assert.equal(session.state.getPendingAction(), 'restart');
422
+
423
+ session.bus.dispatch({ type: 'OFFLINE', from: 'local' });
424
+ await waitForBus();
425
+
426
+ assert.equal(session.state.getPendingAction(), null);
427
+ assert.equal(session.state.getState('local'), 'syncing');
428
+ assert.equal(session.state.getState('remote'), 'offline');
429
+
430
+ session.bus.dispatch({ type: 'ONLINE', from: 'local' });
431
+ await waitForBus();
432
+ await waitForBus();
433
+
434
+ assert.equal(session.state.getPendingAction(), null);
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');
438
+
439
+ client.inbound({
440
+ type: 'SYNC_STATE',
441
+ payload: {
442
+ history: [{ turn: 1, player: 'local', move: { step: 'peer-move' } }],
443
+ lastStart: 'local',
444
+ turn: 'local',
445
+ resumeTurn: 'local',
446
+ },
447
+ from: 'remote',
155
448
  });
156
449
  await waitForBus();
157
450
 
158
451
  assert.equal(session.state.getPendingAction(), null);
159
- assert.equal(session.state.getState("local"), beforeRestart.local);
160
- assert.equal(session.state.getState("remote"), beforeRestart.remote);
452
+ assert.equal(session.state.getState('local'), 'remote_turn');
453
+ assert.equal(session.state.getState('remote'), 'turn');
454
+ assert.equal(session.state.getHistory().length, 1);
455
+ assert.equal(session.state.getHistory()[0].player, 'remote');
456
+ assert.equal(session.state.getLastStart(), 'remote');
161
457
  }
162
458
 
163
459
  {
@@ -170,28 +466,28 @@ const startGame = async () => {
170
466
  onGameEvent() {},
171
467
  });
172
468
 
173
- client.inbound({ type: "RESTART", from: "remote" });
469
+ client.inbound({ type: 'RESTART', from: 'remote' });
174
470
  await waitForBus();
175
471
 
176
- assert.equal(session.state.getPendingAction(), "restart");
177
- assert.equal(session.state.getState("local"), "approving");
178
- 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');
179
475
 
180
476
  session.actions.approve();
181
477
  await waitForBus();
182
478
 
183
479
  const sent = client.sent.at(-1);
184
- assert.equal(sent.type, "APPROVE");
185
- assert.equal(sent.payload.action, "restart");
480
+ assert.equal(sent.type, 'APPROVE');
481
+ assert.equal(sent.payload.action, 'restart');
186
482
  assert.equal(session.state.getPendingAction(), null);
187
- assert.equal(session.state.getState("local"), "idle");
188
- assert.equal(session.state.getState("remote"), "idle");
483
+ assert.equal(session.state.getState('local'), 'idle');
484
+ assert.equal(session.state.getState('remote'), 'idle');
189
485
  assert.equal(
190
486
  snapshots.some(
191
487
  (snapshot) =>
192
- snapshot.localState === "idle" &&
193
- snapshot.remoteState === "idle" &&
194
- snapshot.pendingAction === "restart",
488
+ snapshot.localState === 'idle' &&
489
+ snapshot.remoteState === 'idle' &&
490
+ snapshot.pendingAction === 'restart',
195
491
  ),
196
492
  false,
197
493
  );
@@ -200,22 +496,92 @@ const startGame = async () => {
200
496
  {
201
497
  const { client, session } = await startGame();
202
498
  const beforeRestart = {
203
- local: session.state.getState("local"),
204
- remote: session.state.getState("remote"),
499
+ local: session.state.getState('local'),
500
+ remote: session.state.getState('remote'),
205
501
  };
206
502
 
207
- client.inbound({ type: "RESTART", from: "remote" });
503
+ client.inbound({ type: 'RESTART', from: 'remote' });
208
504
  await waitForBus();
209
505
 
210
506
  session.actions.reject();
211
507
  await waitForBus();
212
508
 
213
509
  const sent = client.sent.at(-1);
214
- assert.equal(sent.type, "REJECT");
215
- assert.equal(sent.payload.action, "restart");
510
+ assert.equal(sent.type, 'REJECT');
511
+ assert.equal(sent.payload.action, 'restart');
512
+ assert.equal(session.state.getPendingAction(), null);
513
+ assert.equal(session.state.getState('local'), beforeRestart.local);
514
+ assert.equal(session.state.getState('remote'), beforeRestart.remote);
515
+ }
516
+
517
+ {
518
+ const { client, session } = await startGame();
519
+ client.inbound({ type: 'RESTART', from: 'remote' });
520
+ await waitForBus();
521
+
522
+ assert.equal(session.state.getPendingAction(), 'restart');
523
+
524
+ session.bus.dispatch({ type: 'OFFLINE', from: 'local' });
525
+ await waitForBus();
526
+
216
527
  assert.equal(session.state.getPendingAction(), null);
217
- assert.equal(session.state.getState("local"), beforeRestart.local);
218
- assert.equal(session.state.getState("remote"), beforeRestart.remote);
528
+ assert.equal(session.state.getState('local'), 'syncing');
529
+ assert.equal(session.state.getState('remote'), 'offline');
530
+
531
+ session.bus.dispatch({ type: 'ONLINE', from: 'local' });
532
+ await waitForBus();
533
+ await waitForBus();
534
+
535
+ assert.equal(session.state.getPendingAction(), null);
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');
539
+
540
+ client.inbound({
541
+ type: 'SYNC_STATE',
542
+ payload: {
543
+ history: [{ turn: 1, player: 'remote', move: { step: 'local-move' } }],
544
+ lastStart: 'remote',
545
+ turn: 'remote',
546
+ resumeTurn: 'remote',
547
+ },
548
+ from: 'remote',
549
+ });
550
+ await waitForBus();
551
+
552
+ assert.equal(session.state.getPendingAction(), null);
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');
583
+ assert.equal(session.state.getHistory().length, 1);
584
+ assert.equal(session.state.getHistory()[0].player, 'local');
219
585
  }
220
586
 
221
- console.log("serialization smoke passed");
587
+ console.log('serialization smoke passed');