Workspaces

Content is stored in a Workspace

Neos 9

This page is for Neos 9.0 with the new Event Sourced Content Repository.

The Workspace is a named part of the address where a Node is stored. Each Workspace consist of a Content Graph.

#Hierarchy

Workspaces form a hierarchical structure, where Workspaces may have a single base Workspace and an arbitrary number of sub Workspaces based upon it. Nodes without a base Workspace are called root Workspaces. Workspaces are a copy of their base Workspace and its contents. A Workspaces full nested hierarchy (directly or indirectly based) are the dependant Workspaces.

#Changes

The Content Repository allows modifying a Node or its relations. These changes can be made to the root Workspace or be separately staged by using another workspace.

A change to the Content Graph inside a sub Workspace is only made to its copy leaving the base workspace untouched.

Existing dependant workspaces with their own copy will not be modified. Whereas new dependant workspaces will copy the modified Content Graph.

#Publishing and Discarding Changes

Changes in a sub Workspace can be published or discarded. Publishing a workspace reapplies the changes to the base Workspace, modifying the base Workspace copy. Discarding drops the staged changes.

#Change Propagation

In the Event-Sourced Content Repository, workspaces change propagation is made at write time instead of read time, embracing eventual consistency:

Live Workspace
Live Work...
User Workspace
User Work...
Change done in Live Workspace
Change done in Live...
Change is shown in the user workspace
Change is shown in the...
Eventual Consistency ...
Eventual Consisten...
change propagation
change pr...
Text is not SVG - cannot display

This means that in practice, it can take some time until a change from the live workspace is propagated upwards to its depending workspaces, so that the user sees this change in the user workspace.

Why do we accept this eventual consistency here?

  • A point where we can allow eventual consistency is a great point for scaling out. This will allow us to scale to more users and changes, when we allow for some slack time until a change is visible to everybody.
  • Workspaces at read time are complex and the write time allows for better encapsulation, with less side-effects to other code-parts.
  • Making changes and differences between workspaces more explicit allows to detect optional soft conflicts where a property is edited from two users.
  • It makes the publishing process (e.g. changing something in the live workspace) independent of the number of dependent workspaces.

#Propagating base workspace changes through rebase

Now, if the base workspace changes, how can we push these changes upwards to the dependant workspaces? We wanted to find a technique which aligns well with out overall event-sourced ideas – and we got inspired by Git rebase.

The core idea is the following: We will "re-do" all the changes the user did (in a very fast manner), based on the new state of the base workspace.

How is this implemented in practice? In a nutshell, like the following:

  1. We record all commands which have been part of the original user workspace.
    This is done by adding the serialized commands to the event metadata of the first event which resulted from this command.
  2. Re-apply all commands in simulation
  3. Fork new content stream (based on current version of base content stream)
  4. Apply simulated events on the fork
  5. Switch active content stream for Workspace
  6. Mark dependent workspaces outdated
Re-Applying Commands can fail!

As commands contain (soft) constraint checks to ensure their prerequisites are true, the commands might not execute successfully, if something on the base has changed. As an example, if you modify content in a node; and in the new base workspace, the node (or one of its parents) has been hard-removed, the modification cannot be applied.

Command Handlers must be deterministic

Because we re-apply commands, the command handlers (which take the command and create events from them) must be deterministic: The command must encapsulate all information to ensure the same events are generated from it.

As an example, when creating new nodes with auto-created child nodes, the nested node aggregate identifiers must be part of the command. In case the end-user did not supply them, we generate them and store them in the command before persisting it.

#Workspaces in Neos

Old Content Repository: Shine-Through at Read Time

Before Neos 9, we've workspaces were solved via layering/shine though at read time when accessing the CR.

While the Content Repository only differentiates be tween root- and sub Workspace, Neos introduces further features on top.

In Neos the root Workspace is named "live" and provides the frontend content for the website. User have a copy of the "live" content in form of their own user workspace where their changes are staged. Additionally review Workspaces can be placed between "live" and the user to allow dedicated reviewing.

A user will upon first opening of the Neos backend be assigned to a new personal workspace. The assignment is managed by Neos in a separate model, from user id to generated workspace name.

The user workspace is currently the only workspace you directly edit or view through the Neos Ui. All changes done are made to this workspace, and then, on publishing, pushed to the base workspace (i.e. live).

You can create intermediate Review Workspaces which sit between your user workspace and the live workspace, in order to collaborate on changes before the they go live.

We can assume that changes in a workspace are a temporary thing (albeit they might exist for multiple weeks or months). Furthermore, most changes created by users will end up in the live workspace eventually (as you want to make them visible to the frontend).

Live Workspace
Live Workspace
Review-Workspace
Review-Workspace
User-A-Workspace
User-A-Workspace
User-B-Workspace
User-B-Workspace
User A
User...
User B
User...
Text is not SVG - cannot display

#Technical - Content Streams

A Content Stream is the "technical location" where nodes are stored - where the copy of the content graph is made. Every Workspace has one assigned Content Stream, which is the one used for writing and reading.

The event store is append-only, which means that changes made to a Workspace would always be part of that. To allow removing events from the Workspace we use multiple Content Streams and switch the active pointer of the Workspace.

Workspaces are the stable named entry-points to the content, while the content streams are often more temporary while they are rebased or published.

The Root Workspace Content Stream is never switched after creation of the root workspace. For testing we often denote this by saying "the live content stream".

A content stream has no knowledge of workspaces pointing to it, but a workspace is always connected to one content stream, and no Content Stream should be unused.

#Technical - Publishing a Workspace

  • We simulate the command handling in memory and apply the to-be-published commands. As usual, this might fail (in case of soft constraint errors).
  • When this has worked out, we can take the emitted events from the simulation and commit them onto the base content stream. Then the base content stream is forked as new sub Workspace content stream.

#Technical - Partial Publishing

Now we have enough context to discuss how to publish only certain changes of our Content Stream.

Important: As an end-user, you do not want to specify the set of commands you want to apply; but instead you want to specify the set of nodes which you want to publish.

The general idea is as follows: We want to order all commands in a way that the to-be-published commands come first – then we only publish this part to the base workspace.

  • We split the commands from our content stream into two lists – the ones which are relevant to the nodes which we want to publish, and the remainder.
    Each command from the NodeAggregate bounded context will be filtered by node aggregate id whether it interacts with a given node.
  • Then, we simulate the command handling in memory and apply only the to-be-published commands. As usual, this might fail (in case of soft constraint errors).
  • As second part the simulation applies the not-to-be-published commands. As usual, this might fail (in case of soft constraint errors).
  • When this has all worked out, we can take the emitted events from the first simulation and commit them onto the base content stream. Then the base content stream is forked and we apply the remaining emitted events from the simulation. This forms the new sub Workspace content stream.