Bug 656184 — Add is-quiescent property
authorPhilip Withnall <philip@tecnocode.co.uk>
Fri, 2 Sep 2011 20:15:35 +0000 (21:15 +0100)
committerPhilip Withnall <philip@tecnocode.co.uk>
Fri, 2 Sep 2011 20:32:54 +0000 (21:32 +0100)
This allows clients to determine when the IndividualAggregator has reached
a quiescent state — it's received and linked all of the personas it should
do at startup, and from that point onwards will only relink personas in
response to (for example), persona stores dynamically being added and removed
at runtime.

Closes: bgo#656184

15 files changed:
NEWS
backends/eds/eds-backend.vala
backends/eds/lib/edsf-persona-store.vala
backends/key-file/kf-backend.vala
backends/key-file/kf-persona-store.vala
backends/libsocialweb/lib/swf-persona-store.vala
backends/libsocialweb/sw-backend.vala
backends/telepathy/lib/tpf-persona-store.vala
backends/telepathy/tp-backend.vala
backends/tracker/lib/trf-persona-store.vala
backends/tracker/tr-backend.vala
folks/backend-store.vala
folks/backend.vala
folks/individual-aggregator.vala
folks/persona-store.vala

diff --git a/NEWS b/NEWS
index 0889982..b75bb91 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,7 @@ Bugs fixed:
   persona
 * Bug 657635 — Linking personas from different (e-d-s) stores is not working
 * Bug 657510 — Add asynchronous property setter methods
+* Bug 656184 — Add is-quiescent property
 
 API changes:
 * Add PersonaStore:always-writeable-properties property
@@ -14,6 +15,8 @@ API changes:
 * Add IndividualAggregator.ensure_individual_property_writeable()
 * Add Folks.PropertyError
 * Add *Details.change_*() virtual methods
+* Add IndividualAggregator:is-quiescent, Backend:is-quiescent and
+  PersonaStore:is-quiescent
 
 Overview of changes from libfolks 0.6.0 to libfolks 0.6.1
 =========================================================
index b5fd81a..e83f55f 100644 (file)
@@ -37,6 +37,7 @@ public class Folks.Backends.Eds.Backend : Folks.Backend
   private static const string _use_address_books =
       "FOLKS_BACKEND_EDS_USE_ADDRESS_BOOKS";
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private HashMap<string, PersonaStore> _persona_stores;
   private Map<string, PersonaStore> _persona_stores_ro;
   private E.SourceList _ab_sources;
@@ -76,6 +77,18 @@ public class Folks.Backends.Eds.Backend : Folks.Backend
     }
 
   /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See {@link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    */
   public override async void prepare () throws GLib.Error
@@ -93,6 +106,9 @@ public class Folks.Backends.Eds.Backend : Folks.Backend
 
               this._is_prepared = true;
               this.notify_property ("is-prepared");
+
+              this._is_quiescent = true;
+              this.notify_property ("is-quiescent");
             }
         }
     }
@@ -114,6 +130,9 @@ public class Folks.Backends.Eds.Backend : Folks.Backend
               this._ab_sources.changed.disconnect (this._ab_source_list_changed_cb);
               this._ab_sources = null;
 
+              this._is_quiescent = false;
+              this.notify_property ("is-quiescent");
+
               this._is_prepared = false;
               this.notify_property ("is-prepared");
             }
index 83e0c29..3ff8aee 100644 (file)
@@ -37,6 +37,7 @@ public class Edsf.PersonaStore : Folks.PersonaStore
   private HashMap<string, Persona> _personas;
   private Map<string, Persona> _personas_ro;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private E.BookClient _addressbook;
   private E.BookClientView _ebookview;
   private string _addressbook_uri = null;
@@ -166,6 +167,18 @@ public class Edsf.PersonaStore : Folks.PersonaStore
       get { return this._always_writeable_properties; }
     }
 
+  /*
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See {@link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
   /**
    * The {@link Persona}s exposed by this PersonaStore.
    *
@@ -1268,6 +1281,14 @@ public class Edsf.PersonaStore : Folks.PersonaStore
         {
           this._emit_personas_changed (added_personas, null);
         }
+
+      /* If this is the first contacts-added notification, assume we've reached
+       * a quiescent state. */
+      if (this._is_quiescent == false)
+        {
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
     }
 
   private void _contacts_changed_cb (GLib.List<E.Contact> contacts)
index 53b8191..8569a63 100644 (file)
@@ -35,6 +35,7 @@ extern const string BACKEND_NAME;
 public class Folks.Backends.Kf.Backend : Folks.Backend
 {
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private HashMap<string, PersonaStore> _persona_stores;
   private Map<string, PersonaStore> _persona_stores_ro;
 
@@ -51,6 +52,18 @@ public class Folks.Backends.Kf.Backend : Folks.Backend
     }
 
   /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See {@link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    */
   public override string name { get { return BACKEND_NAME; } }
@@ -113,6 +126,9 @@ public class Folks.Backends.Kf.Backend : Folks.Backend
 
               this._is_prepared = true;
               this.notify_property ("is-prepared");
+
+              this._is_quiescent = true;
+              this.notify_property ("is-quiescent");
             }
         }
     }
@@ -130,6 +146,9 @@ public class Folks.Backends.Kf.Backend : Folks.Backend
       this._persona_stores.clear ();
       this.notify_property ("persona-stores");
 
+      this._is_quiescent = false;
+      this.notify_property ("is-quiescent");
+
       this._is_prepared = false;
       this.notify_property ("is-prepared");
     }
index d4d075b..956d306 100644 (file)
@@ -38,6 +38,7 @@ public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
   private GLib.KeyFile _key_file;
   private unowned Cancellable _save_key_file_cancellable = null;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
 
   private const string[] _always_writeable_properties =
     {
@@ -112,6 +113,18 @@ public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
     }
 
   /**
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See {@link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    *
    * @since UNRELEASED
@@ -265,6 +278,10 @@ public class Folks.Backends.Kf.PersonaStore : Folks.PersonaStore
 
               this._is_prepared = true;
               this.notify_property ("is-prepared");
+
+              /* We've finished loading all the personas we know about */
+              this._is_quiescent = true;
+              this.notify_property ("is-quiescent");
             }
         }
     }
index 631b783..89fd6d2 100644 (file)
@@ -37,6 +37,7 @@ public class Swf.PersonaStore : Folks.PersonaStore
   private HashMap<string, Persona> _personas;
   private Map<string, Persona> _personas_ro;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private ClientService _service;
   private ClientContactView _contact_view;
 
@@ -124,6 +125,19 @@ public class Swf.PersonaStore : Folks.PersonaStore
     {
       get { return this._always_writeable_properties; }
     }
+
+  /*
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See {@link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
   /**
    * The {@link Persona}s exposed by this PersonaStore.
    *
@@ -250,6 +264,15 @@ public class Swf.PersonaStore : Folks.PersonaStore
         {
           this._emit_personas_changed (added_personas, null);
         }
+
+      /* If this is the first contacts-added notification, assume we've reached
+       * a quiescent state. We can't do any better, since libsocialweb doesn't
+       * expose an is-quiescent property (or similar). */
+      if (this._is_quiescent == false)
+        {
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
     }
 
   private void contacts_changed_cb (GLib.List<unowned Contact> contacts)
index 120bac5..f598e23 100644 (file)
@@ -34,6 +34,7 @@ extern const string BACKEND_NAME;
 public class Folks.Backends.Sw.Backend : Folks.Backend
 {
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private Client _client;
   private HashMap<string, PersonaStore> _persona_stores;
   private Map<string, PersonaStore> _persona_stores_ro;
@@ -72,6 +73,18 @@ public class Folks.Backends.Sw.Backend : Folks.Backend
     }
 
   /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See {@link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    */
   public override async void prepare () throws GLib.Error
@@ -88,6 +101,9 @@ public class Folks.Backends.Sw.Backend : Folks.Backend
 
                   this._is_prepared = true;
                   this.notify_property ("is-prepared");
+
+                  this._is_quiescent = true;
+                  this.notify_property ("is-quiescent");
                 });
             }
         }
@@ -109,6 +125,9 @@ public class Folks.Backends.Sw.Backend : Folks.Backend
       this._persona_stores.clear ();
       this.notify_property ("persona-stores");
 
+      this._is_quiescent = false;
+      this.notify_property ("is-quiescent");
+
       this._is_prepared = false;
       this.notify_property ("is-prepared");
     }
index 37532c8..8e3c32f 100644 (file)
@@ -92,6 +92,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
   private MaybeBool _can_group_personas = MaybeBool.UNSET;
   private MaybeBool _can_remove_personas = MaybeBool.UNSET;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
+  private bool _got_stored_channel_members = false;
+  private bool _got_self_handle = false;
   private Debug _debug;
   private PersonaStoreCache _cache;
   private Cancellable? _load_cache_cancellable = null;
@@ -185,6 +188,29 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       get { return this._always_writeable_properties; }
     }
 
+  /*
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See {@link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  private void _notify_if_is_quiescent ()
+    {
+      if (this._got_stored_channel_members == true &&
+          this._got_self_handle == true &&
+          this._is_quiescent == false)
+        {
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
+    }
+
   /**
    * The {@link Persona}s exposed by this PersonaStore.
    *
@@ -250,6 +276,8 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       debug.print_key_value_pairs (domain, level,
           "ID", this.id,
           "Prepared?", this._is_prepared ? "yes" : "no",
+          "Has stored contact members?", this._got_stored_channel_members ? "yes" : "no",
+          "Has self handle?", this._got_self_handle ? "yes" : "no",
           "Publish TpChannel", "%p".printf (this._publish),
           "Stored TpChannel", "%p".printf (this._stored),
           "Subscribe TpChannel", "%p".printf (this._subscribe),
@@ -563,6 +591,11 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                   /* If we're disconnected, advertise personas from the cache
                    * instead. */
                   yield this._load_cache ();
+
+                  /* We've reached a quiescent state. */
+                  this._got_self_handle = true;
+                  this._got_stored_channel_members = true;
+                  this._notify_if_is_quiescent ();
                 }
 
               try
@@ -762,6 +795,12 @@ public class Tpf.PersonaStore : Folks.PersonaStore
                 });
             });
 
+          /* If the persona store starts offline, we've reached a quiescent
+           * state. */
+          this._got_self_handle = true;
+          this._got_stored_channel_members = true;
+          this._notify_if_is_quiescent ();
+
           return;
         }
       else if (new_status != TelepathyGLib.ConnectionStatus.CONNECTED)
@@ -969,7 +1008,14 @@ public class Tpf.PersonaStore : Folks.PersonaStore
         this._ignore_by_handle (this._self_contact.handle, null, null, 0);
 
       if (c.self_handle == 0)
-        return;
+        {
+          /* We can only claim to have reached a quiescent state once we've
+           * got the stored contact list and the self handle. */
+          this._got_self_handle = true;
+          this._notify_if_is_quiescent ();
+
+          return;
+        }
 
       uint[] contact_handles = { c.self_handle };
 
@@ -1002,6 +1048,9 @@ public class Tpf.PersonaStore : Folks.PersonaStore
 
               this._self_contact = contact;
               this._emit_personas_changed (personas, null);
+
+              this._got_self_handle = true;
+              this._notify_if_is_quiescent ();
             },
           this);
     }
@@ -1185,7 +1234,18 @@ public class Tpf.PersonaStore : Folks.PersonaStore
       HashTable details)
     {
       if (added.length > 0)
-        this._channel_group_pend_incoming_adds.begin (channel, added, true);
+        {
+          this._channel_group_pend_incoming_adds.begin (channel, added, true,
+              (obj, res) =>
+            {
+              this._channel_group_pend_incoming_adds.end (res);
+
+              /* We can only claim to have reached a quiescent state once we've
+               * got the stored contact list and the self handle. */
+              this._got_stored_channel_members = true;
+              this._notify_if_is_quiescent ();
+            });
+        }
 
       for (var i = 0; i < removed.length; i++)
         {
index b67de00..7a90a42 100644 (file)
@@ -34,6 +34,7 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
 {
   private AccountManager _account_manager;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private HashMap<string, PersonaStore> _persona_stores;
   private Map<string, PersonaStore> _persona_stores_ro;
 
@@ -72,6 +73,18 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
     }
 
   /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See {@link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    */
   public override async void prepare () throws GLib.Error
@@ -100,6 +113,9 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
 
               this._is_prepared = true;
               this.notify_property ("is-prepared");
+
+              this._is_quiescent = true;
+              this.notify_property ("is-quiescent");
             }
         }
     }
@@ -123,6 +139,9 @@ public class Folks.Backends.Tp.Backend : Folks.Backend
       this._persona_stores.clear ();
       this.notify_property ("persona-stores");
 
+      this._is_quiescent = false;
+      this.notify_property ("is-quiescent");
+
       this._is_prepared = false;
       this.notify_property ("is-prepared");
     }
index 598b9a2..e6a2c46 100644 (file)
@@ -166,6 +166,7 @@ public class Trf.PersonaStore : Folks.PersonaStore
   private HashMap<string, Persona> _personas;
   private Map<string, Persona> _personas_ro;
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private static const int _default_timeout = 100;
   private Resources _resources_object;
   private Tracker.Sparql.Connection _connection;
@@ -383,6 +384,18 @@ public class Trf.PersonaStore : Folks.PersonaStore
       get { return this._always_writeable_properties; }
     }
 
+  /*
+   * Whether this PersonaStore has reached a quiescent state.
+   *
+   * See {@link Folks.PersonaStore.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
   /**
    * The {@link Persona}s exposed by this PersonaStore.
    *
@@ -1071,6 +1084,11 @@ public class Trf.PersonaStore : Folks.PersonaStore
 
                   this._is_prepared = true;
                   this.notify_property ("is-prepared");
+
+                  /* By this time (due to having done the INITIAL_QUERY above)
+                   * we have already reached a quiescent state. */
+                  this._is_quiescent = true;
+                  this.notify_property ("is-quiescent");
                 }
               catch (GLib.IOError e1)
                 {
index f67b0cd..34e4dad 100644 (file)
@@ -33,6 +33,7 @@ extern const string BACKEND_NAME;
 public class Folks.Backends.Tr.Backend : Folks.Backend
 {
   private bool _is_prepared = false;
+  private bool _is_quiescent = false;
   private HashMap<string, PersonaStore> _persona_stores;
   private Map<string, PersonaStore> _persona_stores_ro;
 
@@ -69,6 +70,18 @@ public class Folks.Backends.Tr.Backend : Folks.Backend
     }
 
   /**
+   * Whether this Backend has reached a quiescent state.
+   *
+   * See {@link Folks.Backend.is_quiescent}.
+   *
+   * @since UNRELEASED
+   */
+  public override bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * {@inheritDoc}
    *
    */
@@ -79,8 +92,12 @@ public class Folks.Backends.Tr.Backend : Folks.Backend
           if (!this._is_prepared)
             {
               this._add_default_persona_store ();
+
               this._is_prepared = true;
               this.notify_property ("is-prepared");
+
+              this._is_quiescent = true;
+              this.notify_property ("is-quiescent");
             }
         }
     }
@@ -98,6 +115,9 @@ public class Folks.Backends.Tr.Backend : Folks.Backend
       this._persona_stores.clear ();
       this.notify_property ("persona-stores");
 
+      this._is_quiescent = false;
+      this.notify_property ("is-quiescent");
+
       this._is_prepared = false;
       this.notify_property ("is-prepared");
     }
index 89d9821..4a55d83 100644 (file)
@@ -186,7 +186,8 @@ public class Folks.BackendStore : Object {
           debug.print_key_value_pairs (domain, level,
               "Ref. count", this.ref_count.to_string (),
               "Name", backend.name,
-              "Prepared?", backend.is_prepared ? "yes" : "no"
+              "Prepared?", backend.is_prepared ? "yes" : "no",
+              "Quiescent?", backend.is_quiescent ? "yes" : "no"
           );
           debug.print_line (domain, level, "%u PersonaStores:",
               backend.persona_stores.size);
@@ -219,6 +220,7 @@ public class Folks.BackendStore : Object {
                   "ID", persona_store.id,
                   "Prepared?", persona_store.is_prepared ? "yes" : "no",
                   "Writeable?", persona_store.is_writeable ? "yes" : "no",
+                  "Quiescent?", persona_store.is_quiescent ? "yes" : "no",
                   "Trust level", trust_level,
                   "Persona count", persona_store.personas.size.to_string ()
               );
index db8d222..8e728fa 100644 (file)
@@ -44,6 +44,21 @@ public abstract class Folks.Backend : Object
   public abstract bool is_prepared { get; default = false; }
 
   /**
+   * Whether the backend has reached a quiescent state. This will happen at some
+   * point after {@link Backend.prepare} has successfully completed for the
+   * backend. A backend is in a quiescent state when all the
+   * {@link PersonaStore}s that it originally knows about have been loaded.
+   *
+   * It's guaranteed that this property's value will only ever change after
+   * {@link Backend.is_prepared} has changed to `true`.
+   *
+   * When {@link Backend.unprepare} is called, this will be reset to `false`.
+   *
+   * @since UNRELEASED
+   */
+  public abstract bool is_quiescent { get; default = false; }
+
+  /**
    * A unique name for the backend.
    *
    * This will be used to identify the backend, and should also be used as the
index b24ef5b..01efcab 100644 (file)
@@ -79,6 +79,18 @@ public class Folks.IndividualAggregator : Object
   private static const string _FOLKS_CONFIG_KEY =
     "/system/folks/backends/primary_store";
 
+  /* The number of persona stores and backends we're waiting to become
+   * quiescent. Once these both reach 0, we should be in a quiescent state.
+   * We have to count both of them so that we can handle the case where one
+   * backend becomes available, and its persona stores all become quiescent,
+   * long before any other backend becomes available. In this case, we want
+   * the aggregator to signal that it's reached a quiescent state only once
+   * all the other backends have also become available. */
+  private uint _non_quiescent_persona_store_count = 0;
+  /* Same for backends. */
+  private uint _non_quiescent_backend_count = 0;
+  private bool _is_quiescent = false;
+
   /**
    * Whether {@link IndividualAggregator.prepare} has successfully completed for
    * this aggregator.
@@ -91,6 +103,23 @@ public class Folks.IndividualAggregator : Object
     }
 
   /**
+   * Whether the aggregator has reached a quiescent state. This will happen at
+   * some point after {@link IndividualAggregator.prepare} has successfully
+   * completed for the aggregator. An aggregator is in a quiescent state when
+   * all the {@link PersonaStore}s listed by its backends have reached a
+   * quiescent state.
+   *
+   * It's guaranteed that this property's value will only ever change after
+   * {@link IndividualAggregator.is_prepared} has changed to `true`.
+   *
+   * @since UNRELEASED
+   */
+  public bool is_quiescent
+    {
+      get { return this._is_quiescent; }
+    }
+
+  /**
    * Our configured primary (writeable) store.
    *
    * Which one to use is decided (in order or precedence)
@@ -265,7 +294,12 @@ public class Folks.IndividualAggregator : Object
           "Ref. count", this.ref_count.to_string (),
           "Writeable store", "%p".printf (this._writeable_store),
           "Linking enabled?", this._linking_enabled ? "yes" : "no",
-          "Prepared?", this._is_prepared ? "yes" : "no"
+          "Prepared?", this._is_prepared ? "yes" : "no",
+          "Quiescent?", this._is_quiescent
+              ? "yes"
+              : "no (%u backends, %u persona stores left)".printf (
+                  this._non_quiescent_backend_count,
+                  this._non_quiescent_persona_store_count)
       );
 
       debug.print_line (domain, level,
@@ -463,6 +497,8 @@ public class Folks.IndividualAggregator : Object
               this._backend_persona_store_added_cb);
           backend.persona_store_removed.connect (
               this._backend_persona_store_removed_cb);
+          backend.notify["is-quiescent"].connect (
+              this._backend_is_quiescent_changed_cb);
 
           /* handle the stores that have already been signaled */
           foreach (var persona_store in backend.persona_stores.values)
@@ -475,6 +511,16 @@ public class Folks.IndividualAggregator : Object
   private void _backend_available_cb (BackendStore backend_store,
       Backend backend)
     {
+      /* Increase the number of non-quiescent backends we're waiting for.
+       * If we've already reached a quiescent state, this is ignored. If we
+       * haven't, this delays us reaching a quiescent state until the
+       * _backend_is_quiescent_changed_cb() callback is called for this
+       * backend. */
+      if (backend.is_quiescent == false)
+        {
+          this._non_quiescent_backend_count++;
+        }
+
       this._add_backend.begin (backend);
     }
 
@@ -507,6 +553,18 @@ public class Folks.IndividualAggregator : Object
       store.personas_changed.connect (this._personas_changed_cb);
       store.notify["is-writeable"].connect (this._is_writeable_changed_cb);
       store.notify["trust-level"].connect (this._trust_level_changed_cb);
+      store.notify["is-quiescent"].connect (
+          this._persona_store_is_quiescent_changed_cb);
+
+      /* Increase the number of non-quiescent persona stores we're waiting for.
+       * If we've already reached a quiescent state, this is ignored. If we
+       * haven't, this delays us reaching a quiescent state until the
+       * _persona_store_is_quiescent_changed_cb() callback is called for this
+       * store. */
+      if (store.is_quiescent == false)
+        {
+          this._non_quiescent_persona_store_count++;
+        }
 
       store.prepare.begin ((obj, result) =>
         {
@@ -528,9 +586,19 @@ public class Folks.IndividualAggregator : Object
       PersonaStore store)
     {
       store.personas_changed.disconnect (this._personas_changed_cb);
+      store.notify["is-quiescent"].disconnect (
+          this._persona_store_is_quiescent_changed_cb);
       store.notify["trust-level"].disconnect (this._trust_level_changed_cb);
       store.notify["is-writeable"].disconnect (this._is_writeable_changed_cb);
 
+      /* If we were still waiting on this persona store to reach a quiescent
+       * state, stop waiting. */
+      if (this._is_quiescent == false && store.is_quiescent == false)
+        {
+          this._non_quiescent_persona_store_count--;
+          this._notify_if_is_quiescent ();
+        }
+
       /* no need to remove this store's personas from all the individuals, since
        * they'll do that themselves (and emit their own 'removed' signal if
        * necessary) */
@@ -984,6 +1052,37 @@ public class Folks.IndividualAggregator : Object
         assert (store.trust_level != PersonaStoreTrust.FULL);
     }
 
+  private void _persona_store_is_quiescent_changed_cb (Object obj,
+      ParamSpec pspec)
+    {
+      /* Have we reached a quiescent state yet? */
+      if (this._non_quiescent_persona_store_count > 0)
+        {
+          this._non_quiescent_persona_store_count--;
+          this._notify_if_is_quiescent ();
+        }
+    }
+
+  private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
+    {
+      if (this._non_quiescent_backend_count > 0)
+        {
+          this._non_quiescent_backend_count--;
+          this._notify_if_is_quiescent ();
+        }
+    }
+
+  private void _notify_if_is_quiescent ()
+    {
+      if (this._non_quiescent_backend_count == 0 &&
+          this._non_quiescent_persona_store_count == 0 &&
+          this._is_quiescent == false)
+        {
+          this._is_quiescent = true;
+          this.notify_property ("is-quiescent");
+        }
+    }
+
   private void _individual_removed_cb (Individual i, Individual? replacement)
     {
       if (this.user == i)
index 4f70778..b330536 100644 (file)
@@ -463,6 +463,19 @@ public abstract class Folks.PersonaStore : Object
    */
   public abstract bool is_prepared { get; default = false; }
 
+  /**
+   * Whether the store has reached a quiescent state. This will happen at some
+   * point after {@link PersonaStore.prepare} has successfully completed for the
+   * store. A store is in a quiescent state when all the {@link Persona}s that
+   * it originally knows about have been loaded.
+   *
+   * It's guaranteed that this property's value will only ever change after
+   * {@link IndividualAggregator.is_prepared} has changed to `true`.
+   *
+   * @since UNRELEASED
+   */
+  public abstract bool is_quiescent { get; default = false; }
+
    /**
    * Whether the PersonaStore is writeable.
    *