croupier: Combine Watch for Game Setup and Log

By combining these two, we end up with a much faster Resume Game
experience and fewer ongoing watch streams.

Reasons:
- slow to have 2 watch streams
- resume game would still animate the final scanned moves because
  the game log's watch wasn't the one we wait for when restarting a
  game

Tested using Solitaire on 2 devices and restarting them.

Change-Id: I89a26614449dfd0e46cf87645ca8532cf6176aa2
diff --git a/Makefile b/Makefile
index dca4438..24f222a 100644
--- a/Makefile
+++ b/Makefile
@@ -189,13 +189,16 @@
 mock:
 	mv lib/src/syncbase/log_writer.dart lib/src/syncbase/log_writer.dart.backup
 	mv lib/src/syncbase/settings_manager.dart lib/src/syncbase/settings_manager.dart.backup
+	mv lib/src/syncbase/util.dart lib/src/syncbase/util.dart.backup
 	cp lib/src/mocks/log_writer.dart lib/src/syncbase/
 	cp lib/src/mocks/settings_manager.dart lib/src/syncbase/
+	cp lib/src/mocks/util.dart lib/src/syncbase/
 
 .PHONY: unmock
 unmock:
 	mv lib/src/syncbase/log_writer.dart.backup lib/src/syncbase/log_writer.dart
 	mv lib/src/syncbase/settings_manager.dart.backup lib/src/syncbase/settings_manager.dart
+	mv lib/src/syncbase/util.dart.backup lib/src/syncbase/util.dart
 
 .PHONY: env-check
 env-check:
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 146d5d3..5c33aa2 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -51,7 +51,8 @@
         _updateSettingsEveryoneCb,
         _updateGamesFoundCb,
         _updatePlayerFoundCb,
-        _updateGameStatusCb);
+        _updateGameStatusCb,
+        _gameLogUpdateCb);
 
     settings_manager.load().then((String csString) {
       settings = new CroupierSettings.fromJSONString(csString);
@@ -84,6 +85,12 @@
     }
   }
 
+  Future _gameLogUpdateCb(String key, String value, bool duringScan) async {
+    if (game != null && game.gamelog.watchUpdateCb != null) {
+      await game.gamelog.watchUpdateCb(key, value, duringScan);
+    }
+  }
+
   int userIDFromPlayerNumber(int playerNumber) {
     return players_found.keys.firstWhere(
         (int user) => players_found[user] == playerNumber,
@@ -107,7 +114,7 @@
 
   void _quitGame() {
     if (game != null) {
-      game.quit();
+      settings_manager.quitGame();
       game = null;
     }
   }
diff --git a/lib/logic/game/game.dart b/lib/logic/game/game.dart
index 1e139a3..1b95cb1 100644
--- a/lib/logic/game/game.dart
+++ b/lib/logic/game/game.dart
@@ -9,6 +9,7 @@
 
 import '../card.dart' show Card;
 import '../../src/syncbase/log_writer.dart' show SimulLevel;
+import '../../src/syncbase/util.dart';
 
 part 'game_def.part.dart';
 part 'game_command.part.dart';
diff --git a/lib/logic/game/game_def.part.dart b/lib/logic/game/game_def.part.dart
index 43deaa6..b6d5cd2 100644
--- a/lib/logic/game/game_def.part.dart
+++ b/lib/logic/game/game_def.part.dart
@@ -149,10 +149,6 @@
     deck.addAll(Card.All);
   }
 
-  void quit() {
-    this.gamelog.close();
-  }
-
   // UNIMPLEMENTED
   void move(Card card, List<Card> dest);
   void triggerEvents();
diff --git a/lib/logic/game/game_log.part.dart b/lib/logic/game/game_log.part.dart
index ea238cd..f502184 100644
--- a/lib/logic/game/game_log.part.dart
+++ b/lib/logic/game/game_log.part.dart
@@ -9,7 +9,8 @@
   List<GameCommand> log = new List<GameCommand>();
   // This list is normally empty, but may grow if multiple commands arrive.
   List<GameCommand> pendingCommands = new List<GameCommand>();
-  bool hasFired = false; // if true, halts processing of later pendingCommands
+  bool hasFired = false; // if true, halts processing of later pendingCommands}
+  asyncKeyValueCallback watchUpdateCb; // May be null.
 
   void setGame(Game g) {
     this.game = g;
@@ -101,5 +102,4 @@
   void addToLogCb(List<GameCommand> log, GameCommand newCommand);
   List<GameCommand> updateLogCb(
       List<GameCommand> current, List<GameCommand> other, int mismatchIndex);
-  void close();
 }
diff --git a/lib/logic/hearts/hearts_log.part.dart b/lib/logic/hearts/hearts_log.part.dart
index c6ef13e..22849c5 100644
--- a/lib/logic/hearts/hearts_log.part.dart
+++ b/lib/logic/hearts/hearts_log.part.dart
@@ -19,6 +19,8 @@
     logWriter = new LogWriter(handleSyncUpdate, [0, 1, 2, 3]);
     logWriter.associatedUser = this.game.playerNumber;
     logWriter.logPrefix = "${game.gameID}/log";
+
+    watchUpdateCb = logWriter.onChange;
   }
 
   void handleSyncUpdate(String key, String cmd) {
@@ -45,9 +47,4 @@
     assert(false);
     return current;
   }
-
-  @override
-  void close() {
-    logWriter.close();
-  }
 }
diff --git a/lib/logic/proto/proto_log.part.dart b/lib/logic/proto/proto_log.part.dart
index 1450f6e..5ff7c70 100644
--- a/lib/logic/proto/proto_log.part.dart
+++ b/lib/logic/proto/proto_log.part.dart
@@ -16,7 +16,4 @@
     assert(false); // This game can't have conflicts.
     return current;
   }
-
-  @override
-  void close() {}
 }
diff --git a/lib/logic/solitaire/solitaire_log.part.dart b/lib/logic/solitaire/solitaire_log.part.dart
index 7a1d3ce..579c687 100644
--- a/lib/logic/solitaire/solitaire_log.part.dart
+++ b/lib/logic/solitaire/solitaire_log.part.dart
@@ -19,6 +19,8 @@
     logWriter = new LogWriter(handleSyncUpdate, [0]);
     logWriter.associatedUser = this.game.playerNumber;
     logWriter.logPrefix = "${game.gameID}/log";
+
+    watchUpdateCb = logWriter.onChange;
   }
 
   void handleSyncUpdate(String key, String cmd) {
@@ -45,9 +47,4 @@
     assert(false);
     return current;
   }
-
-  @override
-  void close() {
-    logWriter.close();
-  }
 }
diff --git a/lib/src/mocks/log_writer.dart b/lib/src/mocks/log_writer.dart
index 19e6d58..bc8cc97 100644
--- a/lib/src/mocks/log_writer.dart
+++ b/lib/src/mocks/log_writer.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+import 'dart:async';
 import 'dart:convert' show JSON;
 
 enum SimulLevel { TURN_BASED, INDEPENDENT, DEPENDENT }
@@ -26,6 +27,8 @@
 
   Map<String, String> _data = new Map<String, String>();
 
+  Future onChange(String rowKey, String value, bool duringScan) async {}
+
   void write(SimulLevel s, String value) {
     assert(!inProposalMode);
 
@@ -120,6 +123,4 @@
     }
     return true;
   }
-
-  void close() {}
 }
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index b767029..d3509cf 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -14,13 +14,15 @@
   final util.keyValueCallback updateGamesCallback;
   final util.keyValueCallback updatePlayerFoundCallback;
   final util.keyValueCallback updateGameStatusCallback;
+  final util.asyncKeyValueCallback updateGameLogCallback;
 
   SettingsManager(
       settings_client.AppSettings _,
       this.updateCallback,
       this.updateGamesCallback,
       this.updatePlayerFoundCallback,
-      this.updateGameStatusCallback);
+      this.updateGameStatusCallback,
+      this.updateGameLogCallback);
 
   Map<String, String> _data = new Map<String, String>();
 
@@ -89,4 +91,6 @@
   Future<String> getGameSyncgroup(int gameID) async {
     return new Future(() => null);
   }
+
+  void quitGame() {}
 }
diff --git a/lib/src/mocks/util.dart b/lib/src/mocks/util.dart
index 0413ea1..0096922 100644
--- a/lib/src/mocks/util.dart
+++ b/lib/src/mocks/util.dart
@@ -2,7 +2,10 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+import 'dart:async';
+
 typedef void keyValueCallback(String key, String value);
+typedef Future asyncKeyValueCallback(String key, String value, bool duringScan);
 
 String gameIDFromGameKey(String gameKey) {
   return null;
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index 21eecc9..16a6dfc 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -43,10 +43,6 @@
   // Affects read/write/watch locations of the log writer.
   String logPrefix = ''; // This is usually set to <game_id>/log
 
-  // An internal StreamSubscription that should be canceled once we are done
-  // watching on this log.
-  StreamSubscription<WatchChange> _watchSubscription;
-
   // When a proposal is made (or received), this flag is set to true.
   // Once a consensus has been reached, this is set to false again.
   bool inProposalMode = false;
@@ -119,16 +115,9 @@
 
     SyncbaseDatabase db = await _cc.createDatabase();
     tb = await _cc.createTable(db, tbName);
-
-    // Start to watch the stream.
-    this._watchSubscription = await _cc.watchEverything(
-        db, tbName, this.logPrefix, _onChange,
-        sorter: (WatchChange a, WatchChange b) {
-      return a.rowKey.compareTo(b.rowKey);
-    });
   }
 
-  Future _onChange(String rowKey, String value, bool duringScan) async {
+  Future onChange(String rowKey, String value, bool duringScan) async {
     String key = rowKey.replaceFirst("${this.logPrefix}/", "");
     String timeStr = key.split("-")[0];
     DateTime keyTime = _parseTime(timeStr);
@@ -148,13 +137,6 @@
     }
   }
 
-  void close() {
-    if (this._watchSubscription != null) {
-      this._watchSubscription.cancel();
-      this._watchSubscription = null;
-    }
-  }
-
   Future write(SimulLevel s, String value) async {
     util.log('LogWriter.write start');
     await _prepareLog();
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index b6c8e31..b6e2acf 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -32,17 +32,22 @@
   final util.keyValueCallback updateGamesCallback;
   final util.keyValueCallback updatePlayerFoundCallback;
   final util.keyValueCallback updateGameStatusCallback;
+  final util.asyncKeyValueCallback updateGameLogCallback;
   final CroupierClient _cc;
   sc.SyncbaseTable tb;
 
   static const String _discoveryGameAdKey = "discovery-game-ad";
 
+  // The game subscription. Cancel when done listening.
+  StreamSubscription<sc.WatchChange> _gameSubscription;
+
   SettingsManager(
       settings_client.AppSettings appSettings,
       this.updateSettingsCallback,
       this.updateGamesCallback,
       this.updatePlayerFoundCallback,
-      this.updateGameStatusCallback)
+      this.updateGameStatusCallback,
+      this.updateGameLogCallback)
       : _cc = new CroupierClient(appSettings);
 
   Future _prepareSettingsTable() async {
@@ -143,6 +148,10 @@
       if (this.updateGameStatusCallback != null) {
         this.updateGameStatusCallback(key, value);
       }
+    } else if (key.indexOf("/log") != -1) {
+      if (this.updateGameLogCallback != null) {
+        await this.updateGameLogCallback(key, value, duringScan);
+      }
     }
   }
 
@@ -152,9 +161,13 @@
     sc.SyncbaseDatabase db = await _cc.createDatabase();
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
-    // Watch for the players in the game.
-    await _cc.watchEverything(
-        db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange);
+    // Watch all the data in the game.
+    assert(_gameSubscription == null);
+    _gameSubscription = await _cc.watchEverything(
+        db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange,
+        sorter: (sc.WatchChange a, sc.WatchChange b) {
+      return a.rowKey.compareTo(b.rowKey);
+    });
 
     print("Now writing to some rows of ${gameID}");
     // Start up the table and write yourself as player 0.
@@ -179,6 +192,13 @@
     return gsd;
   }
 
+  void quitGame() {
+    if (_gameSubscription != null) {
+      _gameSubscription.cancel();
+      _gameSubscription = null;
+    }
+  }
+
   Future joinGameSyncgroup(String sgName, int gameID) async {
     print("Now joining game syncgroup at ${sgName} and ${gameID}");
 
@@ -186,7 +206,7 @@
     sc.SyncbaseTable gameTable = await _cc.createTable(db, util.tableNameGames);
 
     // Watch for the players in the game.
-    await _cc.watchEverything(
+    _gameSubscription = await _cc.watchEverything(
         db, util.tableNameGames, util.syncgamePrefix(gameID), _onGameChange);
 
     await _cc.joinSyncgroup(sgName);