[sessionmgr][refactor] Add barebones StoryModel*
... and associated tests. This includes implementations for:
* StoryModel table
* StoryModelMutation union
* StoryModelOwner
* StoryMutator
* StoryObserver
... and an interface for StoryModelStorage (what will ultimately
have a Ledger-backed implementation).
TEST=story_model_owner_unittest,apply_commands_unittest
MF-88 #comment [sessionmgr][refactor] Add barebones StoryModel*
Change-Id: I6daf15afad17a18e18a62efdd8e4d8cdb757c3f9
diff --git a/bin/sessionmgr/story/model/BUILD.gn b/bin/sessionmgr/story/model/BUILD.gn
new file mode 100644
index 0000000..c01950f
--- /dev/null
+++ b/bin/sessionmgr/story/model/BUILD.gn
@@ -0,0 +1,117 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//peridot/build/tests_package.gni")
+
+group("model") {
+ deps = [
+ ":story_observer",
+ ":story_mutator",
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ ]
+}
+
+tests_package("story_model_unittests") {
+ deps = [
+ ":apply_mutations_unittest",
+ ":story_model_owner_unittest",
+ ]
+}
+
+source_set("story_observer") {
+ sources = [
+ "story_observer.h",
+ ]
+
+ public_deps = [
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ ]
+}
+
+source_set("story_mutator") {
+ sources = [
+ "story_mutator.cc",
+ "story_mutator.h",
+ ]
+
+ public_deps = [
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ ]
+}
+
+source_set("story_model_storage") {
+ sources = [
+ "story_model_storage.h",
+ "story_model_storage.cc",
+ ]
+
+ public_deps = [
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ ]
+}
+
+source_set("apply_mutations") {
+ sources = [
+ "apply_mutations.cc",
+ "apply_mutations.h",
+ ]
+
+ public_deps = [
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ ]
+
+ deps = [
+ "//garnet/public/lib/fxl",
+ ]
+}
+
+executable("apply_mutations_unittest") {
+ testonly = true
+
+ sources = [
+ "apply_mutations_unittest.cc",
+ ]
+
+ deps = [
+ ":apply_mutations",
+ "//third_party/googletest:gtest",
+ "//third_party/googletest:gtest_main",
+ ]
+}
+
+source_set("story_model_owner") {
+ sources = [
+ "story_model_owner.cc",
+ "story_model_owner.h",
+ ]
+
+ public_deps = [
+ "//peridot/public/fidl/fuchsia.modular.storymodel",
+ "//garnet/public/lib/fxl",
+ "//garnet/public/lib/async_promise",
+ ]
+
+ deps = [
+ ":apply_mutations",
+ ":story_model_storage",
+ ":story_mutator",
+ ":story_observer",
+ ]
+}
+
+executable("story_model_owner_unittest") {
+ testonly = true
+
+ sources = [
+ "story_model_owner_unittest.cc",
+ ]
+
+ deps = [
+ ":story_model_owner",
+ ":story_model_storage",
+ "//garnet/public/lib/gtest",
+ "//third_party/googletest:gtest",
+ "//third_party/googletest:gtest_main",
+ ]
+}
diff --git a/bin/sessionmgr/story/model/README.md b/bin/sessionmgr/story/model/README.md
new file mode 100644
index 0000000..72b7cd6
--- /dev/null
+++ b/bin/sessionmgr/story/model/README.md
@@ -0,0 +1,103 @@
+### StoryModel
+
+The `StoryModel` FIDL table is used to represent the state of a story.
+`sessionmgr` keeps a separate `StoryModel` in memory for each running story,
+and also persists changes to it onto storage. Mutations to `StoryModel` are
+done using the `StoryMutator` interface and changes from these mutations
+are notified to registered observers (using the `StoryObserver`
+interface).
+
+This directory defines classes that define the control flow for mutating a
+`StoryModel` FIDL table.
+
+The following interfaces are defined:
+
+* `StoryMutator`: Allows clients to issue a series of `StoryModelMutation`
+ structs that describe mutations to a `StoryModel`.
+* `StoryObserver`: Used to ready `StoryModel` and observe changes to it.
+ Changes to `StoryModel` may happen through `StoryMutator` or, in the
+ case of a distributed storage implementation such as Ledger, from sync'ing
+ with peers.
+interface. Allows clients to read the current `StoryModel` state, and register
+for updates when the `StoryModel` changes.
+
+Both of the above are abstract base classes to aid in testing: clients that
+wish to mutate and/or observe a `StoryModel` will accept a `StoryMutator`
+or `StoryObserver` as a constructor argument. Making them abstract allows
+injection of test implementations that do not require the full machinery
+introduced by `StoryModelOwner`.
+
+#### `StoryModelOwner` class
+
+`StoryModelOwner` is has its own implementations of `StoryMutator` and
+`StoryObserver`, and coordinates the flow of data from
+`StoryMutators` through a `StoryModelStorage`, applies those changes
+to a `StoryModel` and flows this new model to `StoryObserver` instances.
+It acts as a factory for its own implementations of `StoryMutators` and
+`StoryObservers`.
+
+#### `StoryModelStorage` interface
+
+Supplied to `StoryModelOwner` at the time of its creation, a
+`StoryModelStorage` is responsible for consuming mutation commands and
+updating its persistent storage layer by applying a set of mutation commands,
+as well as notifying of mutations that have been applied. A request to mutate
+does not necessary result in those exact mutations being observed (in the case
+of conflict resolution), nor do observed mutations imply a request was made (in
+the case of a `StoryModel` backed by distributed storage).
+
+A "no-op" StoryModelStorage (one that does not result in any disk-backed
+or other durable storage) would immediately notify of any incoming mutations
+without applying them anywhere. In this case the `StoryModel` would be resident
+in memory only.
+
+#### Flow of Control
+
+Mutation commands flow from anything that has a `StoryMutator` through a
+`StoryModelOwner`, `StoryModelStorage` and are translated into new
+`StoryModels`. From that point forward, observers see the new `StoryModel`
+values.
+
+```
+[some system] -> StoryMutator -> StoryModelOwner | commands
+ | | "
+ StoryModelStorage | "
+ | | "
+[other system] <- StoryObserver <- StoryModelOwner $ model
+```
+
+### Example
+
+The constructor for your average System that both mutates and observes a
+`StoryModel` would look like:
+```
+class Foo : public System {
+ public:
+ Foo(std::unique_ptr<StoryMutator> mutator,
+ std::unique_ptr<StoryObserver> observer, ...);
+};
+```
+
+In production, we will create this by leveraging the `StoryModelOwner` to
+create those dependencies:
+
+```
+// assume StoryModelOwner has already been defined.
+auto foo_system = std::make_unique<Foo>(owner->NewMutator(), owner->NewObserver(), ...);
+```
+
+For testing, we leverage test versions of both:
+```
+auto test_mutator = std::make_unique<TestStoryMutator>();
+auto test_observer = std::make_unique<TestStoryObserver>();
+
+// Retain pointers to both |test_mutator| and |test_observer| so we can trigger
+// behavior and validate side-effects.
+auto mutator_ptr = test_mutator.get();
+auto observer_ptr = test_observer.get();
+auto foo_system = std::make_unique<Foo>(std::move(test_mutator), std::move(test_observer));
+
+observer_ptr->NotifyOfModel(new_model); // Push a new model to observers!
+// |foo_system| should have generated new mutations as a side-effect.
+EXPECT_TRUE(3, mutator_ptr->GetNumCommandsIssued());
+```
\ No newline at end of file
diff --git a/bin/sessionmgr/story/model/apply_mutations.cc b/bin/sessionmgr/story/model/apply_mutations.cc
new file mode 100644
index 0000000..fefa18e
--- /dev/null
+++ b/bin/sessionmgr/story/model/apply_mutations.cc
@@ -0,0 +1,42 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/fxl/logging.h>
+
+#include "peridot/bin/sessionmgr/story/model/apply_mutations.h"
+
+using fuchsia::modular::StoryVisibilityState;
+using fuchsia::modular::storymodel::StoryModel;
+using fuchsia::modular::storymodel::StoryModelMutation;
+
+namespace modular {
+
+namespace {
+
+void ApplySetVisibilityState(const StoryVisibilityState visibility_state, StoryModel* story_model) {
+ story_model->set_visibility_state(visibility_state);
+}
+
+} // namespace
+
+StoryModel ApplyMutations(const StoryModel& current_model,
+ const std::vector<StoryModelMutation>& commands) {
+ StoryModel new_model;
+ fidl::Clone(current_model, &new_model);
+
+ for (const auto& command : commands) {
+ switch (command.Which()) {
+ case StoryModelMutation::Tag::kSetVisibilityState:
+ ApplySetVisibilityState(command.set_visibility_state(), &new_model);
+ break;
+ default:
+ FXL_LOG(FATAL) << "Unsupported StoryModelMutation: "
+ << fidl::ToUnderlying(command.Which());
+ }
+ }
+
+ return new_model;
+}
+
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/apply_mutations.h b/bin/sessionmgr/story/model/apply_mutations.h
new file mode 100644
index 0000000..e8a77ca
--- /dev/null
+++ b/bin/sessionmgr/story/model/apply_mutations.h
@@ -0,0 +1,21 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PERIDOT_BIN_SESSIONMGR_STORY_MODEL_APPLY_MUTATIONS_H_
+#define PERIDOT_BIN_SESSIONMGR_STORY_MODEL_APPLY_MUTATIONS_H_
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+
+namespace modular {
+
+// Returns a new StoryModel which is the result of applying |commands| to
+// |current_model|.
+fuchsia::modular::storymodel::StoryModel ApplyMutations(
+ const fuchsia::modular::storymodel::StoryModel& current_model,
+ const std::vector<fuchsia::modular::storymodel::StoryModelMutation>
+ &commands);
+
+} // namespace modular
+
+#endif // PERIDOT_BIN_SESSIONMGR_STORY_MODEL_APPLY_MUTATIONS_H_
diff --git a/bin/sessionmgr/story/model/apply_mutations_unittest.cc b/bin/sessionmgr/story/model/apply_mutations_unittest.cc
new file mode 100644
index 0000000..3e708af
--- /dev/null
+++ b/bin/sessionmgr/story/model/apply_mutations_unittest.cc
@@ -0,0 +1,47 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+#include <lib/fit/bridge.h>
+#include <lib/fit/function.h>
+#include <lib/fit/single_threaded_executor.h>
+
+#include "gtest/gtest.h"
+#include "peridot/bin/sessionmgr/story/model/apply_mutations.h"
+
+using fuchsia::modular::StoryVisibilityState;
+using fuchsia::modular::storymodel::StoryModel;
+using fuchsia::modular::storymodel::StoryModelMutation;
+
+namespace modular {
+namespace {
+
+// Test a single StoryModelMutation.set_visibility_state command to change
+// StoryModel.visibility_state.
+TEST(ApplyMutationsTest, SingleMutation_set_visibility_state) {
+ StoryModel before;
+ *before.mutable_visibility_state() = StoryVisibilityState::DEFAULT;
+
+ std::vector<StoryModelMutation> commands(1);
+ commands[0].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
+ auto result = ApplyMutations(before, commands);
+ EXPECT_EQ(StoryVisibilityState::IMMERSIVE, *result.visibility_state());
+}
+
+// Test two StoryModelMutation.set_visibility_state commands to change
+// StoryModel.visibility_state to one value and back. Tests that multiple
+// commands in a list are applied in order.
+TEST(ApplyMutationsTest,
+ MultipleMutations_AppliedInOrder_set_visibility_state) {
+ StoryModel before;
+
+ std::vector<StoryModelMutation> commands(2);
+ commands[0].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
+ commands[1].set_set_visibility_state(StoryVisibilityState::DEFAULT);
+ auto result = ApplyMutations(before, commands);
+ EXPECT_EQ(StoryVisibilityState::DEFAULT, *result.visibility_state());
+}
+
+} // namespace
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/story_model_owner.cc b/bin/sessionmgr/story/model/story_model_owner.cc
new file mode 100644
index 0000000..6fe687d
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_model_owner.cc
@@ -0,0 +1,159 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "peridot/bin/sessionmgr/story/model/story_model_owner.h"
+
+#include <lib/fit/bridge.h>
+#include <lib/fit/defer.h>
+#include <lib/fxl/logging.h>
+#include "peridot/bin/sessionmgr/story/model/apply_mutations.h"
+#include "peridot/bin/sessionmgr/story/model/story_mutator.h"
+#include "peridot/bin/sessionmgr/story/model/story_model_storage.h"
+
+using fuchsia::modular::storymodel::StoryModel;
+using fuchsia::modular::storymodel::StoryModelMutation;
+using fuchsia::modular::storymodel::ModuleModel;
+
+namespace modular {
+
+namespace {
+// Sets default values for all fields of a new StoryModel.
+void InitializeModelDefaults(StoryModel* model) {
+ model->set_visibility_state(fuchsia::modular::StoryVisibilityState::DEFAULT);
+ model->set_modules(fidl::VectorPtr<ModuleModel>::New(0));
+}
+} // namespace
+
+// Delegates Execute() to the StoryModelOwner.
+class StoryModelOwner::Mutator : public StoryMutator {
+ public:
+ Mutator(fxl::WeakPtr<StoryModelOwner> weak_owner) : weak_owner_(weak_owner) {}
+ ~Mutator() override = default;
+
+ private:
+ // |StoryMutator|
+ fit::consumer<> ExecuteInternal(std::vector<StoryModelMutation> commands) override {
+ if (!weak_owner_) {
+ fit::bridge<> bridge;
+ bridge.completer.complete_error();
+ return std::move(bridge.consumer);
+ }
+ return weak_owner_->ExecuteCommands(std::move(commands));
+ }
+
+ fxl::WeakPtr<StoryModelOwner> weak_owner_;
+};
+
+// Manages the lifecycle of multiple listener callbacks. When Observer dies,
+// all callbacks registered with RegisterListener() are unregistered from the
+// backing StoryModelOwner.
+class StoryModelOwner::Observer : public StoryObserver {
+ public:
+ Observer(fxl::WeakPtr<StoryModelOwner> weak_owner)
+ : weak_owner_(weak_owner) {}
+ ~Observer() {
+ // If our owner is gone, all of the listener functions have already been
+ // cleaned up. We need to cancel all the deferred actions since they
+ // capture and make a call on our owner.
+ if (!weak_owner_) {
+ for (auto& i : deferred_cleanup_) {
+ i.cancel();
+ }
+ }
+ }
+
+ private:
+ void RegisterListener(
+ fit::function<void(const StoryModel&)> listener) override {
+ if (!weak_owner_) {
+ return;
+ // |listener| is destroyed.
+ }
+
+ deferred_cleanup_.push_back(
+ weak_owner_->RegisterListener(std::move(listener)));
+ }
+
+ const StoryModel& model() override {
+ FXL_CHECK(weak_owner_);
+ return weak_owner_->model_;
+ }
+
+ fxl::WeakPtr<StoryModelOwner> weak_owner_;
+ // When we are destroyed, we want to clean up any listeners we've added to
+ // |shared_state_->owner|.
+ std::vector<fit::deferred_action<fit::function<void()>>> deferred_cleanup_;
+};
+
+StoryModelOwner::StoryModelOwner(
+ fit::executor* executor,
+ std::unique_ptr<StoryModelStorage> model_storage)
+ : model_storage_(std::move(model_storage)),
+ weak_ptr_factory_(this),
+ executor_(executor) {
+ FXL_CHECK(model_storage_ != nullptr);
+ InitializeModelDefaults(&model_);
+ model_storage_->SetObserveCallback(
+ [this](std::vector<StoryModelMutation> commands) {
+ HandleObservedMutations(std::move(commands));
+ });
+}
+
+StoryModelOwner::~StoryModelOwner() = default;
+
+std::unique_ptr<StoryMutator> StoryModelOwner::NewMutator() {
+ return std::make_unique<Mutator>(weak_ptr_factory_.GetWeakPtr());
+}
+
+std::unique_ptr<StoryObserver> StoryModelOwner::NewObserver() {
+ return std::make_unique<Observer>(weak_ptr_factory_.GetWeakPtr());
+}
+
+fit::deferred_action<fit::function<void()>> StoryModelOwner::RegisterListener(
+ fit::function<void(const StoryModel&)> listener) {
+ auto it = listeners_.insert(listeners_.end(), std::move(listener));
+ return fit::defer(
+ fit::function<void()>([this, it] { listeners_.erase(it); }));
+}
+
+fit::consumer<> StoryModelOwner::ExecuteCommands(
+ std::vector<StoryModelMutation> commands) {
+ // fit::bridge allows this function to return (and eventually complete) a
+ // promise that is owned by the caller and still schedule a promise as a task
+ // to execute the model update locally.
+ //
+ // If the caller chooses to ignore the result, our local promise will still be
+ // scheduled and executed.
+ fit::bridge<> bridge;
+ auto promise = model_storage_->Execute(std::move(commands))
+ .then([completer = std::move(bridge.completer)](
+ fit::result<>& result) mutable {
+ if (result.is_ok()) {
+ completer.complete_ok();
+ } else {
+ completer.complete_error();
+ }
+ });
+
+ executor_->schedule_task(std::move(promise));
+ return std::move(bridge.consumer);
+}
+
+void StoryModelOwner::HandleObservedMutations(
+ std::vector<StoryModelMutation> commands) {
+ // This is not thread-safe. We rely on the fact that
+ // HandleObservedMutations() will only be called on a single thread.
+ StoryModel old_model;
+ FXL_CHECK(fidl::Clone(model_, &old_model) == ZX_OK);
+ model_ = ApplyMutations(std::move(old_model), std::move(commands));
+
+ executor_->schedule_task(fit::make_promise([this] {
+ for (auto& listener : listeners_) {
+ listener(model_);
+ }
+ return fit::ok();
+ }).wrap_with(scope_));
+}
+
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/story_model_owner.h b/bin/sessionmgr/story/model/story_model_owner.h
new file mode 100644
index 0000000..2286a4b
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_model_owner.h
@@ -0,0 +1,115 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_OWNER_H_
+#define PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_OWNER_H_
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+#include <lib/async_promise/executor.h>
+#include <lib/fit/defer.h>
+#include <lib/fit/scope.h>
+#include <lib/fxl/macros.h>
+#include <lib/fxl/memory/weak_ptr.h>
+
+#include <list>
+#include <memory>
+
+#include "peridot/bin/sessionmgr/story/model/story_mutator.h"
+#include "peridot/bin/sessionmgr/story/model/story_observer.h"
+
+namespace modular {
+
+class StoryModelStorage;
+
+// Owns a single instance of StoryModel and manages the flow of control from a
+// stream of mutations to observers.
+//
+// Clients do not depend on or have any direct knowledge of StoryModelOwner.
+// Rather, they depend on either or both a StoryObserver and
+// StoryMutator, depending on if they need to observe or mutate the model.
+//
+// See README.md.
+//
+// This class is not thread-safe.
+class StoryModelOwner {
+ public:
+ // Uses |executor| to schedule internal mutation tasks. Delegates mutation
+ // commands to and reacts to observation of applied mutations from
+ // |model_storage|.
+ explicit StoryModelOwner(fit::executor* executor,
+ std::unique_ptr<StoryModelStorage> model_storage);
+ ~StoryModelOwner();
+
+ // Returns a mutator object that can be provided to a System that requires
+ // the ability to issue commands to mutate the model. This can be provided to
+ // Systems as a constructor argument.
+ //
+ // The returned StoryMutator may outlive |this|, but will return an
+ // error for all attempts to mutate the model.
+ std::unique_ptr<StoryMutator> NewMutator();
+
+ // Returns an object that can be used to register observer callbacks to the
+ // be notified of the model's current state when changes are made. This
+ // should be provided to Systems as a constructor argument.
+ //
+ // The returned StoryObserver may outlive |this|. See the documentation
+ // for StoryObserver for caveats.
+ std::unique_ptr<StoryObserver> NewObserver();
+
+ private:
+ class Mutator;
+ class Observer;
+
+ // Registers |listener| to be called whenever mutation commands are applied
+ // to |model_|. Returns a deferred action that will deregister |listener|
+ // when it goes out of scope.
+ //
+ // Called by instances of Observer.
+ fit::deferred_action<fit::function<void()>> RegisterListener(
+ fit::function<void(const fuchsia::modular::storymodel::StoryModel&)>
+ listener);
+
+ // Calls |model_storage_| to execute |commands|.
+ //
+ // Called by instances of Mutator.
+ fit::consumer<> ExecuteCommands(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation> commands);
+
+ // Applies |commands| to |model_| and notifies all |listeners_| with the
+ // updated StoryModel.
+ //
+ // Called indirectly by |model_storage_| through a callback.
+ void HandleObservedMutations(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation> commands);
+
+ std::unique_ptr<StoryModelStorage> model_storage_;
+
+ // The most recent StoryModel value. Accessed by StoryObservers at any
+ // time. Updated by HandleObservedMutations().
+ fuchsia::modular::storymodel::StoryModel model_;
+
+ // Used to signal to instances of StoryMutator/Observer when |this| is
+ // destroyed.
+ fxl::WeakPtrFactory<StoryModelOwner> weak_ptr_factory_;
+
+ // A list<> so that we can get stable iterators for cleanup purposes. See
+ // RegisterListener().
+ std::list<
+ fit::function<void(const fuchsia::modular::storymodel::StoryModel&)>>
+ listeners_;
+
+ fit::executor* executor_; // Not owned.
+
+ // Since we schedule our fit::promises for execution on |executor_|, which can
+ // outlive |this|, we use this to wrap our fit::promises (using
+ // fit::promise.wrap_with(scope_)) such that when |this| is destroyed, all
+ // pending promises are abandoned.
+ fit::scope scope_;
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(StoryModelOwner);
+};
+
+} // namespace modular
+
+#endif // PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_OWNER_H_
diff --git a/bin/sessionmgr/story/model/story_model_owner_unittest.cc b/bin/sessionmgr/story/model/story_model_owner_unittest.cc
new file mode 100644
index 0000000..c71b6da
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_model_owner_unittest.cc
@@ -0,0 +1,247 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <lib/fit/bridge.h>
+#include <lib/fit/function.h>
+#include <lib/fit/single_threaded_executor.h>
+
+#include "lib/gtest/test_loop_fixture.h"
+#include "peridot/bin/sessionmgr/story/model/story_model_owner.h"
+#include "peridot/bin/sessionmgr/story/model/story_model_storage.h"
+
+using fuchsia::modular::StoryVisibilityState;
+using fuchsia::modular::storymodel::StoryModel;
+using fuchsia::modular::storymodel::StoryModelMutation;
+
+namespace modular {
+namespace {
+
+// This persistence system acts as a mock for calls to Execute(), and promotes
+// Observe() from protected to public so that we can call it directly from the
+// test body.
+class TestPersistenceSystem : public StoryModelStorage {
+ public:
+ struct ExecuteCall {
+ std::vector<StoryModelMutation> commands;
+
+ // Call this after a call to Execute() to complete the returned promise.
+ fit::completer<> completer;
+ };
+ std::vector<ExecuteCall> calls;
+
+ fit::promise<> Execute(std::vector<StoryModelMutation> commands) override {
+ fit::bridge<> bridge;
+
+ // Store the arguments we got.
+ ExecuteCall call{.commands = std::move(commands),
+ .completer = std::move(bridge.completer)};
+ calls.push_back(std::move(call));
+
+ return bridge.consumer.promise();
+ }
+
+ using StoryModelStorage::Observe;
+};
+
+class StoryModelOwnerTest : public ::gtest::TestLoopFixture {
+ public:
+ StoryModelOwnerTest() : TestLoopFixture() { ResetExecutor(); }
+
+ std::unique_ptr<StoryModelOwner> Create() {
+ auto model_storage = std::make_unique<TestPersistenceSystem>();
+ model_storage_ = model_storage.get();
+
+ auto owner = std::make_unique<StoryModelOwner>(executor_.get(),
+ std::move(model_storage));
+ return owner;
+ }
+
+ void ResetExecutor() { executor_.reset(new async::Executor(dispatcher())); }
+
+ TestPersistenceSystem* model_storage() { return model_storage_; }
+
+ private:
+ std::unique_ptr<async::Executor> executor_;
+ TestPersistenceSystem* model_storage_;
+};
+
+TEST_F(StoryModelOwnerTest, SuccessfulMutate) {
+ // Show that a single mutation flows through the StoryModelOwner to
+ // StoryModelStorage, and then applies the resulting commands.
+ auto owner = Create();
+
+ auto mutator = owner->NewMutator();
+ bool done{false};
+ auto result_task =
+ mutator->set_visibility_state(StoryVisibilityState::IMMERSIVE)
+ .promise()
+ .and_then([&] { done = true; })
+ .or_else([] { FAIL(); });
+ RunLoopUntilIdle();
+
+ // The persistence system should have been called.
+ ASSERT_EQ(1lu, model_storage()->calls.size());
+ ASSERT_EQ(1lu, model_storage()->calls[0].commands.size());
+ EXPECT_TRUE(model_storage()->calls[0].commands[0].is_set_visibility_state());
+ EXPECT_EQ(StoryVisibilityState::IMMERSIVE, model_storage()
+ ->calls[0]
+ .commands[0]
+ .set_visibility_state());
+
+ // Complete the pending persistence call.
+ model_storage()->calls[0].completer.complete_ok();
+ RunLoopUntilIdle();
+ fit::run_single_threaded(std::move(result_task));
+ EXPECT_TRUE(done);
+
+ // The existing model hasn't changed, because the StoryModelStorage
+ // has not heard back from its storage that the mutation occurred.
+ auto observer = owner->NewObserver();
+ EXPECT_EQ(StoryVisibilityState::DEFAULT, *observer->model().visibility_state());
+
+ // Now dispatch mutations from the persistence system, and we should observe
+ // that ApplyMutations() is invoked.
+ model_storage()->Observe(
+ std::move(model_storage()->calls[0].commands));
+
+ // And the new model value that ApplyMutations returned should be reflected in
+ // the owner.
+ EXPECT_EQ(StoryVisibilityState::IMMERSIVE,
+ *observer->model().visibility_state());
+}
+
+TEST_F(StoryModelOwnerTest, FailedMutate) {
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ bool task_executed{false};
+ bool saw_error{false};
+ auto result_task =
+ mutator->set_visibility_state(StoryVisibilityState::IMMERSIVE)
+ .promise()
+ .and_then([&] { task_executed = true; })
+ .or_else([&] { saw_error = true; });
+ RunLoopUntilIdle();
+ model_storage()->calls[0].completer.complete_error();
+ RunLoopUntilIdle();
+ fit::run_single_threaded(std::move(result_task));
+ EXPECT_FALSE(task_executed);
+ EXPECT_TRUE(saw_error);
+}
+
+TEST_F(StoryModelOwnerTest, AbandonedMutate) {
+ // If for some reason the underlying mutation is abandoned, we should observe
+ // an error.
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ bool task_executed{false};
+ bool saw_error{false};
+ auto result_task =
+ mutator->set_visibility_state(StoryVisibilityState::IMMERSIVE)
+ .promise_or(fit::error()) // turn abandonment into an error.
+ .and_then([&] { task_executed = true; })
+ .or_else([&] { saw_error = true; });
+ RunLoopUntilIdle();
+ // Clearing the list of calls will destroy the completer, which has the
+ // side-effect of abandoning the returned task.
+ model_storage()->calls.clear();
+ RunLoopUntilIdle();
+ fit::run_single_threaded(std::move(result_task));
+ EXPECT_FALSE(task_executed);
+ EXPECT_TRUE(saw_error);
+}
+
+TEST_F(StoryModelOwnerTest, MutatorLifecycle_OwnerDestroyed) {
+ // When the StoryModelOwner is destroyed but someone is still holding onto a
+ // StoryMutator that mutator should return an error on Execute().
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ owner.reset();
+ bool task_executed{false};
+ bool saw_error{false};
+ auto result_task =
+ mutator->set_visibility_state(StoryVisibilityState::IMMERSIVE)
+ .promise()
+ .and_then([&] { task_executed = true; })
+ .or_else([&] { saw_error = true; });
+ RunLoopUntilIdle();
+ fit::run_single_threaded(std::move(result_task));
+ EXPECT_FALSE(task_executed);
+ EXPECT_TRUE(saw_error);
+}
+
+TEST_F(StoryModelOwnerTest, ObserversAreNotified) {
+ // One can create an observer and learn of the new state.
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ auto observer = owner->NewObserver();
+
+ bool got_update1{false};
+ observer->RegisterListener([&](const StoryModel& model) {
+ got_update1 = true;
+ EXPECT_EQ(StoryVisibilityState::IMMERSIVE, *model.visibility_state());
+ });
+
+ // Another listener should also get the update!
+ bool got_update2{false};
+ observer->RegisterListener(
+ [&](const StoryModel& model) { got_update2 = true; });
+
+ // Also on another observer.
+ auto observer2 = owner->NewObserver();
+ bool got_update3{false};
+ observer2->RegisterListener(
+ [&](const StoryModel& model) { got_update3 = true; });
+
+ std::vector<StoryModelMutation> commands;
+ commands.resize(1);
+ commands[0].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
+ model_storage()->Observe(std::move(commands));
+ RunLoopUntilIdle();
+ EXPECT_TRUE(got_update1);
+ EXPECT_TRUE(got_update2);
+ EXPECT_TRUE(got_update3);
+}
+
+TEST_F(StoryModelOwnerTest, ObserversLifecycle_ClientDestroyed) {
+ // When the client destroys its observer object, it no longer receives
+ // updates.
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ auto observer = owner->NewObserver();
+
+ bool got_update{false};
+ observer->RegisterListener(
+ [&](const StoryModel& model) { got_update = true; });
+
+ std::vector<StoryModelMutation> commands;
+ commands.resize(1);
+ commands[0].set_set_visibility_state(StoryVisibilityState::IMMERSIVE);
+ model_storage()->Observe(std::move(commands));
+
+ observer.reset();
+ RunLoopUntilIdle();
+ EXPECT_FALSE(got_update);
+}
+
+TEST_F(StoryModelOwnerTest, ObserversLifecycle_OwnerDestroyed) {
+ // When the StoryModelOwner is destroyed, clients can learn of the fact by
+ // using a fit::defer on the listener callback.
+ auto owner = Create();
+ auto mutator = owner->NewMutator();
+ auto observer = owner->NewObserver();
+
+ bool destroyed{false};
+ observer->RegisterListener([defer = fit::defer([&] { destroyed = true; })](
+ const StoryModel& model) {});
+
+ owner.reset();
+ EXPECT_TRUE(destroyed);
+
+ // Explicitly destroy |observer| to ensure that its cleanup isn't affected by
+ // |owner| being destroyed.
+ observer.reset();
+}
+
+} // namespace
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/story_model_storage.cc b/bin/sessionmgr/story/model/story_model_storage.cc
new file mode 100644
index 0000000..b37a571
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_model_storage.cc
@@ -0,0 +1,27 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "peridot/bin/sessionmgr/story/model/story_model_storage.h"
+
+#include <lib/fxl/logging.h>
+
+namespace modular {
+
+StoryModelStorage::StoryModelStorage() = default;
+StoryModelStorage::~StoryModelStorage() = default;
+
+void StoryModelStorage::SetObserveCallback(
+ fit::function<
+ void(std::vector<fuchsia::modular::storymodel::StoryModelMutation>)>
+ callback) {
+ observe_callback_ = std::move(callback);
+}
+
+void StoryModelStorage::Observe(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation> commands) {
+ FXL_DCHECK(observe_callback_);
+ observe_callback_(std::move(commands));
+}
+
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/story_model_storage.h b/bin/sessionmgr/story/model/story_model_storage.h
new file mode 100644
index 0000000..f4ce297
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_model_storage.h
@@ -0,0 +1,60 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_STORAGE_H_
+#define PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_STORAGE_H_
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+#include <lib/async_promise/executor.h>
+#include <lib/fit/function.h>
+#include <lib/fxl/macros.h>
+
+#include <list>
+#include <memory>
+
+namespace modular {
+
+// The purpose of a StoryModelStorage is to apply StoryModelMutations to a
+// persistent storage layer and flow observed changes from the storage layer to
+// Observe().
+//
+// Due to the nature of some storage systems (such as Ledger), a call to
+// Mutate() doesn't necessarily result in an equivalent Observe().
+//
+// A trivial example: two peers, A and B, both call Mutate() with conflicting
+// instructions. Peer A learns of peer B's mutation before calling Observe(),
+// and conflict resolution results in no change to the model. In this case,
+// Observe() will not be called.
+class StoryModelStorage {
+ public:
+ StoryModelStorage();
+ virtual ~StoryModelStorage();
+
+ // Registers a callback that is called when Observe() is called by
+ // implementers.
+ void SetObserveCallback(
+ fit::function<
+ void(std::vector<fuchsia::modular::storymodel::StoryModelMutation>)>
+ callback);
+
+ virtual fit::promise<> Execute(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation>
+ commands) = 0;
+
+ protected:
+ // Calls to Observe() must always be made from the same thread: ordering
+ // of the observed mutations matters.
+ void Observe(std::vector<fuchsia::modular::storymodel::StoryModelMutation>);
+
+ private:
+ fit::function<void(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation>)>
+ observe_callback_;
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(StoryModelStorage);
+};
+
+} // namespace modular
+
+#endif // PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MODEL_STORAGE_H_
diff --git a/bin/sessionmgr/story/model/story_mutator.cc b/bin/sessionmgr/story/model/story_mutator.cc
new file mode 100644
index 0000000..f89c9ca
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_mutator.cc
@@ -0,0 +1,23 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <vector>
+
+#include "peridot/bin/sessionmgr/story/model/story_mutator.h"
+
+using fuchsia::modular::storymodel::StoryModelMutation;
+
+namespace modular {
+
+StoryMutator::StoryMutator() = default;
+StoryMutator::~StoryMutator() = default;
+
+fit::consumer<> StoryMutator::set_visibility_state(
+ fuchsia::modular::StoryVisibilityState state) {
+ std::vector<StoryModelMutation> commands(1);
+ commands[0].set_set_visibility_state(state);
+ return ExecuteInternal(std::move(commands));
+}
+
+} // namespace modular
diff --git a/bin/sessionmgr/story/model/story_mutator.h b/bin/sessionmgr/story/model/story_mutator.h
new file mode 100644
index 0000000..c0f24a6
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_mutator.h
@@ -0,0 +1,57 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MUTATOR_H_
+#define PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MUTATOR_H_
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+#include <lib/fit/bridge.h>
+#include <lib/fxl/macros.h>
+
+namespace modular {
+
+// Implemented by and created by the StoryModelOwner.
+//
+// An instance of StoryMutator is provided to all clients that wish to perform
+// mutations on a StoryModel. It is responsible for consuming mutation commands
+// (exposed as methods publicly and converted to StoryModelMutation internally)
+// and dispatching them be applied to a shared StoryModel instance.
+//
+// This is an interface in order to aid in testing clients that depend on
+// StoryMutator.
+class StoryMutator {
+ public:
+ StoryMutator();
+ virtual ~StoryMutator();
+
+ // The following mutators issue a single mutation instruction to
+ // change the StoryModel.
+ //
+ // The returned fit::consumer<> will eventually be completed with the result
+ // of the mutation operation, HOWEVER success does not guarantee that
+ // observers of the StoryModel will see those same changes reflected, and thus
+ // clients should NOT perform side-effects under that assumption.
+ //
+ // It IS safe to perform side-effects once mutations have been observed
+ // through StoryObserver.
+ //
+ // A failure guarantees that the mutation was not applied and it is safe to
+ // retry.
+
+ // Sets the value of |StoryModel.visibility_state|.
+ fit::consumer<> set_visibility_state(
+ fuchsia::modular::StoryVisibilityState state);
+
+ private:
+ // Executes |commands| in order and in a single transaction.
+ virtual fit::consumer<> ExecuteInternal(
+ std::vector<fuchsia::modular::storymodel::StoryModelMutation>
+ commands) = 0;
+
+ FXL_DISALLOW_COPY_AND_ASSIGN(StoryMutator);
+};
+
+} // namespace modular
+
+#endif // PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_MUTATOR_H_
diff --git a/bin/sessionmgr/story/model/story_observer.h b/bin/sessionmgr/story/model/story_observer.h
new file mode 100644
index 0000000..f1eb211
--- /dev/null
+++ b/bin/sessionmgr/story/model/story_observer.h
@@ -0,0 +1,50 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_OBSERVER_H_
+#define PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_OBSERVER_H_
+
+#include <fuchsia/modular/storymodel/cpp/fidl.h>
+#include <lib/fxl/macros.h>
+
+namespace modular {
+
+// Implemented bv StoryModelOwner. A client of StoryObserver can query the
+// current state of a StoryModel or register a callback to be notified of
+// changes.
+class StoryObserver {
+ public:
+ StoryObserver() = default;
+ virtual ~StoryObserver() = default;
+
+ // Registers |listener| as a callback for StoryModel updates. If |this| is
+ // destroyed, or the StoryModelOwner that created |this| is destroyed, all
+ // registered listeners will also be destroyed.
+ //
+ // Clients may learn that the StoryModelOwner is destroyed by deferring
+ // a callback in the capture list of |listener|. Example:
+ //
+ // auto on_abandoned = [] { /* do some cleanup */ };
+ // observer.RegisterListener(
+ // [on_destroy = fit::defer(on_abandoned)]
+ // (const StoryModel& model) {
+ // /* do something with the update */
+ // });
+ //
+ // Note that the received StoryModel is only valid for the lifetime
+ // of the call to |listener|.
+ virtual void RegisterListener(
+ fit::function<void(const fuchsia::modular::storymodel::StoryModel& model)>
+ listener) = 0;
+
+ // Returns the current state of the StoryModel.
+ virtual const fuchsia::modular::storymodel::StoryModel& model() = 0;
+
+ private:
+ FXL_DISALLOW_COPY_AND_ASSIGN(StoryObserver);
+};
+
+} // namespace modular
+
+#endif // PERIDOT_BIN_SESSIONMGR_STORY_MODEL_STORY_OBSERVER_H_
diff --git a/bin/sessionmgr/story_runner/README.md b/bin/sessionmgr/story_runner/README.md
index eb75d22..e0fc842 100644
--- a/bin/sessionmgr/story_runner/README.md
+++ b/bin/sessionmgr/story_runner/README.md
@@ -1,39 +1,8 @@
-This directory contains implementations of services used by the current story
-runner implementation. They will be revised.
+NOTE: The contents of this directory will slowly migrate into:
-What happens so far:
+../story/model
+../story/system
+../story/fidl
-1. The story runner app exposes the StoryRunner service that will start a story.
- The running story is represented as a Story.
-
-2. The Story service allows to request to run an application. Invoking an
- application through the Story service causes (2.1) the specified application
- to be run, (2.2) the Module service of that application to be requested, and
- (2.3.) Initialize() method of the Module service to be invoked with a handle
- of the Story service instance as a parameter (among others). Thus, the module
- so started is able to start more modules, all in the scope of the same
- Story.
-
-3. The Story service also exposes a factory for instances of the fuchsia::modular::Link service. A
- fuchsia::modular::Link instance exposes an API to store and retrieve values, and to register
- callbacks to notify when values stored in the instance change. A fuchsia::modular::Link
- instance is shared between each pair of requesting and requested modules in
- the story.
-
-4. The Module and fuchsia::modular::Link instances in the story are recorded in the Ledger by the
- story runner.
-
-5. An existing story can be restarted.
-
-What is still missing:
-
-6. The fuchsia::modular::Link instance holds data according to a Schema.
-
-Miscellaneous observations on these interfaces:
-
-There is a trade off between Interface request arguments and Interface return
-values. Interface requests are less verbose because they are synchronous.
-Interface requests can be sent to handles and the handles be passed on
-immediately. However, if the receiving side is to make calls on the bound
-implementation and delegate implementation to a further service, then this is
-possible only for a returned interface.
+during the process of a refactor. Please avoid adding new contents
+here.
diff --git a/packages/tests/modular_unittests b/packages/tests/modular_unittests
index 0d05e4c..1725f22 100644
--- a/packages/tests/modular_unittests
+++ b/packages/tests/modular_unittests
@@ -4,6 +4,7 @@
"//peridot/bin/agents/clipboard:clipboard_unittests",
"//peridot/bin/basemgr:basemgr_unittests",
"//peridot/bin/sessionmgr/entity_provider_runner:entity_provider_runner_unittests",
+ "//peridot/bin/sessionmgr/story/model:story_model_unittests",
"//peridot/bin/sessionmgr/puppet_master:puppet_master_unittests",
"//peridot/bin/sessionmgr/story_runner:story_runner_unittests",
"//peridot/bin/sessionmgr/storage:storage_unittests",
diff --git a/public/fidl/fuchsia.modular.internal/BUILD.gn b/public/fidl/fuchsia.modular.internal/BUILD.gn
index c91ae2d..06a0781 100644
--- a/public/fidl/fuchsia.modular.internal/BUILD.gn
+++ b/public/fidl/fuchsia.modular.internal/BUILD.gn
@@ -8,8 +8,8 @@
cpp_legacy_callbacks = true
sources = [
- "story_data.fidl",
"sessionmgr.fidl",
+ "story_data.fidl",
]
deps = [
"//garnet/public/fidl/fuchsia.auth",
diff --git a/public/fidl/fuchsia.modular.internal/story_data.fidl b/public/fidl/fuchsia.modular.internal/story_data.fidl
index b429de2..ed1f50f 100644
--- a/public/fidl/fuchsia.modular.internal/story_data.fidl
+++ b/public/fidl/fuchsia.modular.internal/story_data.fidl
@@ -7,11 +7,13 @@
using fuchsia.ledger;
using fuchsia.modular;
+// Metadata and summary information about a single story. Does not contain the
+// data necessary to run a story: see story_model.fidl for that.
struct StoryData {
// Metadata available to the SessionShell.
fuchsia.modular.StoryInfo story_info;
- // An optiona client-supplied name for this story.
+ // An optional client-supplied name for this story.
string? story_name;
// Story metadata and configuration.
diff --git a/public/fidl/fuchsia.modular.storymodel/BUILD.gn b/public/fidl/fuchsia.modular.storymodel/BUILD.gn
new file mode 100644
index 0000000..0cbb358
--- /dev/null
+++ b/public/fidl/fuchsia.modular.storymodel/BUILD.gn
@@ -0,0 +1,16 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//build/fidl/fidl.gni")
+
+fidl("fuchsia.modular.storymodel") {
+ sources = [
+ "constants.fidl",
+ "story_model.fidl",
+ "story_model_mutation.fidl",
+ ]
+ deps = [
+ "//peridot/public/fidl/fuchsia.modular",
+ ]
+}
diff --git a/public/fidl/fuchsia.modular.storymodel/constants.fidl b/public/fidl/fuchsia.modular.storymodel/constants.fidl
new file mode 100644
index 0000000..5630ec1
--- /dev/null
+++ b/public/fidl/fuchsia.modular.storymodel/constants.fidl
@@ -0,0 +1,10 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library fuchsia.modular.storymodel;
+
+const uint32 MAX_STORY_NAME_LENGTH = 1024;
+
+const uint32 MAX_MODULES_PER_STORY = 128;
+const uint32 MAX_MODULE_NAME_LENGTH = 1024;
diff --git a/public/fidl/fuchsia.modular.storymodel/story_model.fidl b/public/fidl/fuchsia.modular.storymodel/story_model.fidl
new file mode 100644
index 0000000..8bf437d
--- /dev/null
+++ b/public/fidl/fuchsia.modular.storymodel/story_model.fidl
@@ -0,0 +1,39 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library fuchsia.modular.storymodel;
+
+using fuchsia.modular;
+
+// IMPORTANT: StoryModel must only contain field types that are cloneable.
+//
+// The `StoryModel` FIDL table is used to represent the state of a story.
+// `sessionmgr` keeps a separate `StoryModel` in memory for each running story,
+// and also persists changes to it onto storage.
+table StoryModel {
+ // The name of the story, set at story create time.
+ //
+ // Always set. Immutable.
+ 1: string:MAX_STORY_NAME_LENGTH name;
+
+ // An enum describing how the story should be displayed, when focused,
+ // in the StoryShell.
+ //
+ // Always set. Defaults to StoryVisibilityState::DEFAULT.
+ 2: fuchsia.modular.StoryVisibilityState visibility_state;
+
+ // A list of modules present in the story.
+ //
+ // Always set. Defaults to an empty list.
+ 3: vector<ModuleModel>:MAX_MODULES_PER_STORY modules;
+};
+
+table ModuleModel {
+ // The name of the module, set by the client that requested creation
+ // of the module. The name uniquely identifies this module within
+ // the story.
+ //
+ // Always set. Immutable.
+ 1: string:MAX_MODULE_NAME_LENGTH name;
+};
\ No newline at end of file
diff --git a/public/fidl/fuchsia.modular.storymodel/story_model_mutation.fidl b/public/fidl/fuchsia.modular.storymodel/story_model_mutation.fidl
new file mode 100644
index 0000000..05d6f15
--- /dev/null
+++ b/public/fidl/fuchsia.modular.storymodel/story_model_mutation.fidl
@@ -0,0 +1,19 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+library fuchsia.modular.storymodel;
+
+using fuchsia.modular;
+
+// The `StoryModelMutation` union represents the set of all possible low-level mutations to data
+// for a single story. A vector of mutations represent mutations that are to be applied to the
+// model in a single transaction.
+//
+// This structured is used internally in `sessionmgr` and is not exposed to any clients outside
+// that process. Clients will typically construct these indirectly using convenience methods on the
+// `StoryMutator` class.
+union StoryModelMutation {
+ // Sets the value of |StoryModel.visibility_state|.
+ fuchsia.modular.StoryVisibilityState set_visibility_state;
+};
diff --git a/public/fidl/fuchsia.modular/story/story_state.fidl b/public/fidl/fuchsia.modular/story/story_state.fidl
index eb877f7..797435a 100644
--- a/public/fidl/fuchsia.modular/story/story_state.fidl
+++ b/public/fidl/fuchsia.modular/story/story_state.fidl
@@ -13,7 +13,6 @@
// STOPPED -> RUNNING
// RUNNING -> STOPPING
// STOPPING -> STOPPED
-//
enum StoryState {
// Story was started using StoryController.Start().
RUNNING = 1;
diff --git a/public/lib/fostr/fidl/fuchsia.modular.storymodel/BUILD.gn b/public/lib/fostr/fidl/fuchsia.modular.storymodel/BUILD.gn
new file mode 100644
index 0000000..de4b509
--- /dev/null
+++ b/public/lib/fostr/fidl/fuchsia.modular.storymodel/BUILD.gn
@@ -0,0 +1,13 @@
+# Copyright 2018 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//garnet/public/build/fostr/fostr_fidl.gni")
+
+fostr_fidl("fuchsia.modular.storymodel") {
+ fidl_target = "//peridot/public/fidl/fuchsia.modular.storymodel"
+
+ deps = [
+ "//peridot/public/lib/fostr/fidl/fuchsia.modular",
+ ]
+}