Skip to content

Commit

Permalink
Merge pull request #2184 from ably/DTP-732-Create-Quickstart-page
Browse files Browse the repository at this point in the history
[DTP-732] - Create LiveSync Quickstart page
  • Loading branch information
GregHolmes committed Jun 10, 2024
2 parents 410604c + 0bfea92 commit bf74596
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 3 deletions.
2 changes: 1 addition & 1 deletion content/livesync/outbox-nodes-tables.textile
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ $$ LANGUAGE plpgsql;
Create a trigger in the same schema as your outbox table with the same name as the outbox table with a suffix of @_trigger@ that invokes the function when new data is inserted into the outbox table. For example:

```[text]
CREATE TRIGGER your_outbox_table_schema.your_outbox_table_name_trigger
CREATE TRIGGER your_outbox_table_name_trigger
AFTER INSERT ON your_outbox_table_schema.your_outbox_table_name
FOR EACH STATEMENT EXECUTE PROCEDURE
your_outbox_table_schema.your_outbox_table_name_notify();
Expand Down
236 changes: 235 additions & 1 deletion content/livesync/quickstart.textile
Original file line number Diff line number Diff line change
@@ -1,7 +1,241 @@
---
title: Quickstart
meta_description: "."
meta_description: "A quickstart guide to learn the basics of integrating the Ably LiveSync product into your application."
product: livesync
languages:
- javascript
---

This quickstart will provide you with a set of instructions to get an example running locally on your machine. Following this, we'll cover each component of LiveSync found within the code of the example.

h3(#step-0). Clone the repository

Clone the "live comments repository":https://github.com/ably-labs/live-comments :

```[sh]
git clone https://github.com/ably-labs/live-comments.git
```

h3(#step-1). Create an Ably account

"Sign up":https://ably.com/signup for a free account to get your own API key. Ensure that your API key includes the @subscribe@ and @publish@ "capabilities":/auth/capabilities.

h3(#step-2). Set up a database

In general, LiveSync works with your own Postgres database, but for the purposes of the quickstart, this step will take you through setting up an example database with "Neon":https://neon.tech which is a serverless platform for Postgres databases.

In Neon, create a new database, and make a note of the @Connection string@ in the Neon dashboard. This is the database URL your project's backend will connect to, as well as the Database Connector.

h3(#step-3). Add the database credentials to env file

Set up environment variables by copying them from the template:

```[sh]
cp env.example env.local
```

Update your @env.local@ file with the following:

```
POSTGRES_URL="YOUR_FULL_POSTGRES_URL"
POSTGRES_URL_NON_POOLING="YOUR_FULL_POSTGRES_URL"
SESSION_SECRET=somesecret
NEXT_PUBLIC_ABLY_API_KEY=YOUR_ABLY_API_KEY
```

Export the environment variables in your shell session:

```[sh]
export $(grep -s -v "^#" env.local | xargs)
```

h3(#step-4). Configure the Postgres Integration on Ably Dashboard

The Database Connector watches changes in your database table using the transactional "outbox pattern":https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html and broadcasts those updates over Ably Channels. There are two ways to host the Database Connector. You can use either the Database Connector "hosted with Ably":/livesync/database-connector#hosted-with-ably or the same Database Connector but "hosted by yourself":/livesync/database-connector#self-hosted. For this quickstart we'll use the Ably hosted Database Connector. In the "Ably dashboard":https://ably.com/dashboard, choose the project you wish to use for this quickstart and open the @Integrations@ tab. Update the following options available to you:

|_. Option |_. What to add |_. Example |
| URL | Your Neon Postgres database full URL | postgresql://pguser:pgpass@pghost/pgdb?sslmode=require |
| Outbox table schema | Schema for the "outbox table":/livesync/outbox-nodes-tables#schema. Use "public" for this quickstart. | public |
| Outbox table name | Name for the "outbox table":/livesync/outbox-nodes-tables#outbox-table. In this quickstart it is "outbox" | outbox |
| Nodes table schema | Schema for the "nodes table":/livesync/outbox-nodes-tables#schema. Use "public" for this quickstart. | public |
| Nodes table name | Name for the "nodes table":/livesync/outbox-nodes-tables#nodes-table. In this quickstart it is "nodes" | nodes |
| SSL mode | Determines the level of protection provided by the SSL connection. Use the default value (@prefer@) for this quickstart. | required |
| SSL root certificate | Leave this empty for the quickstart. | |
| Primary site | The primary data center in which to run the integration rule. | eu-central-1-A |

h3(#step-5). Install the dependencies

Install the required dependencies for the web application, including your Models SDK. Then run the @pnpm run db@ command to run the migrations, creating the database tables and populate them with some demo data:

```[sh]
pnpm install
pnpm run db
```

h3(#step-6). Run local web server

Run the web application locally by running the following command:

```[sh]
pnpm run dev
```

h3(#step-7). Try it out!

Open the app "localhost:3000":http://localhost:3000 in at least two browser tabs.

Navigate to a post in both tabs, then in one of the tabs try adding, editing, and removing comments, each update will be optimistically updated immediately for the tab making the change. Once the change is confirmed from the backend, all other tabs will be updated in realtime.

h2(#breakdown). Understanding the implementation

Each component used within this section is based on the "livecomments application":https://github.com/ably-labs/live-comments that you now have running locally.

h3(#project-structure). Project structure

Before breaking down LiveSync components within the example, the main files within the project are broken down below:

- @app/api/@ - The backend API routes called by the frontend to read the current post, create, read, update, or delete the comments on the post.
- @components@ - Key components for rendering the application, for example, a @new-comment@ component rendering a form with the comment fields and handling the submission of the form.
- @lib/models/hook.ts@ - The definition of the model specified by the post id passed as an argument, syncronising the frontend model with the database and defining the merge function which is used to update an existing model when the database connector publishes a change from the database.
- @lib/models/mutations.ts@ - Each API call to add, edit, and delete comments. Also contains the merge function called in @hook.ts@ which reads the request and calls the relevant action based on the data (ie, add, edit, or delete the comment).
- @lib/prisma/api.ts@ - Database actions to read, list, add, edit, or delete comments. This file also contains the functionality to add the change to the outbox table.
- @prisma/migrations/@ - Creating the @nodes@, @outbox@, @user@, @post@, and @comment@ tables.
- @prisma/seed.ts@ - Populates the database tables with seeded data for testing with.

h3(#model). The frontend model

The @model@ is a data model representation of a specific part of your frontend application. In the @livecomments@ example, the "model":https://github.com/ably-labs/live-comments/blob/main/lib/models/hook.ts#L40-L46 is a specific post, defined by its unique post ID, @post:${id}@.

```[javascript]
const model: ModelType = modelsClient().models.get({
channelName: `post:${id}`,
sync: async () => getPost(id),
merge,
});

setModel(model);
```

h4(#sync). The sync function

The sync function is used by the Models SDK to fetch the latest data from your backend to hydrate the application initially upon load. There are two objects returned within the response, these are the:

- "@sequenceId@":/livesync/models#sequence-id, which is used to identify the point in the stream of change events that corresponds to the current version of the database's state.
- the data relevant to the expected data model, in this instance it's the current state of the post defined by its @id@. In this example, the sync function called is "@getPost@":https://github.com/ably-labs/live-comments/lib/models/hook.ts#L11-L29, which is found in the @/lib/models/hook.ts@ file of the repository you cloned.

```[javascript]
export async function getPost(id: number) {
const response = await fetch(`/api/posts/${id}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
throw new Error(
`GET /api/posts/:id: ${response.status} ${JSON.stringify(
await response.json()
)}`
);
}
const { sequenceId, data } = (await response.json()) as {
sequenceId: string;
data: PostType;
};

return { sequenceId, data };
}
```

h4(#merge). The merge function

When a message is received on the channel, the merge function is called to merge that new data model state into the existing model state. The merge function should return the updated model state with the new comment merged in. In this example, the "merge":https://github.com/ably-labs/live-comments/lib/models/mutations.ts#L40-L71 function uses the event name to determine whether the comment sent is a new, an updated, or a deleted comment.

```[javascript]
export function merge(existingState: PostType, event: OptimisticEvent | ConfirmedEvent): PostType {
const state = cloneDeep(existingState);

switch (event.name) {
case 'addComment':
const newComment = event.data! as Comment;
state.comments.push(newComment);
break;
case 'editComment':
const editComment = event.data! as Comment;
const editIdx = state.comments.findIndex((c) => c.id === editComment.id);
state.comments[editIdx] = editComment;
break;
case 'deleteComment':
const { id } = event.data! as { id: number };
const deleteIdx = state.comments.findIndex((c) => c.id === id);
state.comments.splice(deleteIdx, 1);
break;
default:
console.error('unknown event', event);
}

state.comments.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());

return state;
}
```

h4(#subscribe). The subscribe function

In the Models SDK, you subscribe to a model to receive its updated state whenever changes occur. Incoming messages on a channel trigger the merge function which integrates the message content into the existing state. With the current example, the "subscribe function":https://github.com/ably-labs/live-comments/blob/main/lib/models/hook.ts#L61-L66 updates the current state of the post with the new state.

```[javascript]
const subscribe = (err: Error | null, data?: PostType | undefined) => {
if (err) return console.error(err);
setPostData(data);
};
```

h3(#backend). The backend

There are two parts of the backend that are key for the LiveSync integration in the livecomments demo. These two are the @sync@ function and the @outbox entry@.

h4(#sync-function). The sync function

The sync function in the backend is an endpoint of a @GET@ request, which returns the "@sequenceId@":/livesync/models#sequence-id, used to identify the point in the stream of change events that corresponds to the current version of the database's state. This "endpoint":https://github.com/ably-labs/live-comments/app/api/posts/%5Bid%5D/route.ts#L5-L18 also returns the current state of the data relevant to the expected data model, in this instance it's the current state of the post defined by its @id@.


```[javascript]
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
try {
let id: number;
try {
id = Number(params.id);
} catch (error) {
return NextResponse.json({ message: 'failed to read :id url parameter', error }, { status: 400 });
}
const [data, sequenceID] = await getPost(id);
return NextResponse.json({ sequenceID, data });
} catch (error) {
return NextResponse.json({ message: 'failed to get post', error }, { status: 500 });
}
}
```

h4(#outbox-entry). outbox entry

The outbox entry is an entry of an update to one of the models within the database. In "this example":https://github.com/ably-labs/live-comments/lib/prisma/api.ts#L129-L139 it's a record of a comment being added to, updated with, or deleted from a post. When a post is added, updated, or deleted a record of this is also added to the outbox table so that the ADBC can pick this up and publish the update to the specified Pub/Sub channel.

```[javascript]
export async function withOutboxWrite(
op: (tx: TxClient, ...args: any[]) => Promise<Prisma.outboxCreateInput>,
...args: any[]
) {
return await prisma.$transaction(async (tx) => {
const { mutation_id, channel, name, data, headers } = await op(tx, ...args);
await tx.outbox.create({
data: { mutation_id, channel, name, data, headers },
});
});
}
```

h2(#next-steps). Next steps

This quickstart introduced the basic concepts of LiveSync and provided a quick demo to illustrate how LiveSync works. The next steps are to:

* Read more about "LiveSync Models":/livesync/models.
* Find out more about the "Database Connector":/livesync/database-connector.
4 changes: 3 additions & 1 deletion data/yaml/page-furniture/liveSyncSidebar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ items:
link: ''
items:
- label: 'How LiveSync works'
link: '/livesync/'
link: /livesync/
- label: 'Quickstart'
link: /livesync/quickstart
- label: 'Frontend data models'
link: '/livesync/models'
- label: 'Database Connector'
Expand Down

0 comments on commit bf74596

Please sign in to comment.