/
how-to-modify-liveblocks-storage-from-the-server.mdx
195 lines (155 loc) · 5.61 KB
/
how-to-modify-liveblocks-storage-from-the-server.mdx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
---
meta:
title: "How to modify Liveblocks Storage from the server"
description:
"Learn about the different methods you can use to modify Liveblocks Storage
within Node.js"
---
In real-time applications, Liveblocks Storage is generally modified from the
browser with [`useMutation`](/docs/api-reference/liveblocks-react#useMutation)
or through
[conflict-free data methods](/docs/api-reference/liveblocks-client#Storage).
However, sometimes it can be useful to modify your real-time storage from
server-side Node.js too.
## What we’re building
In this guide, we’ll be building a function that allows you to easily modify
storage from the server. We’ll do this by running `@liveblocks/client`
server-side using Node.js polyfills, and by signing in with a service account
`userId`.
<Banner title="This solution has caveats" type="warning">
When using this solution, the service account user will appear in presence, and
you must account for this in your front end (e.g. filtering out the service
account when displaying live avatars). We are investigating other solutions, and
hope to provide a superior API in future.
</Banner>
```ts
await modifyStorage("my-room-name", (root) => {
root.get("list").push("item3");
});
```
## Set up Liveblocks server config
<Banner title="Liveblocks already set up">
This tutorials assumes you’ve already set up Liveblocks on the client-side, and
you’ve created a `liveblocks.config.ts` file containing your types.
</Banner>
The first thing we need to do is to install the required Node.js polyfills.
```bash
npm i node-fetch ws
```
After this we can create a server config file, which we’ll name
`liveblocks.server.config.ts`. In this file we’re implementing the following.
<Steps>
<StepCompact>Creating a node client with `new Liveblocks`.</StepCompact>
<StepCompact>
Creating a regular client to be used on the server, `serverClient`.
</StepCompact>
<StepCompact>Authenticating inside the regular client.</StepCompact>
<StepCompact>
Using the same `userId` for server changes, so that MAUs do not increase.
</StepCompact>
<StepCompact>Adding Node.js polyfills to the regular client.</StepCompact>
<StepCompact lastStep>Creating a typed enter room function.</StepCompact>
</Steps>
Here’s the full file:
```ts file="liveblocks.server.config.ts"
import { createClient } from "@liveblocks/client";
import type {
Presence,
Storage,
UserMeta,
RoomEvent,
} from "./liveblocks.config";
import { Liveblocks } from "@liveblocks/node";
import fetch from "node-fetch";
import WebSocket from "ws";
// 1. Creating a node client
const liveblocks = new Liveblocks({
secret: "{{SECRET_KEY}}",
});
// 2. Creating a regular client
export const serverClient = createClient({
// 3. Authenticating inside the client
authEndpoint: async (room) => {
const session = liveblocks.prepareSession(
// 4. Using a specific userId for all server changes
"_SERVICE_ACCOUNT"
);
session.allow(room, session.FULL_ACCESS);
const { body } = await session.authorize();
return JSON.parse(body);
},
// 5. Adding polyfills
polyfills: {
fetch: fetch as any,
WebSocket,
},
});
// 6. Creating a typed enter room function
export const enterRoom = (roomId: string) => {
return serverClient.enter<Presence, Storage, UserMeta, RoomEvent>(roomId, {
// Match the options in your browser code
});
};
```
## Create the modify storage function
Using `serverClient` and `enterRoom` from the previous file, we can create a
typed `modifyStorage` function that allows us to join a room, modify storage
(batching all changes into one request), before leaving the room.
```ts
import type { LiveObject } from "@liveblocks/client";
import type { Storage } from "./liveblocks.config";
import { serverClient, enterRoom } from "./liveblocks.server.config";
export async function modifyStorage(
roomId: string,
storageChanges: (root: LiveObject<Storage>) => void
) {
return new Promise(async (resolve) => {
const room = enterRoom(roomId);
const { root } = await room.getStorage();
// Make storage adjustments in a batch, so they all happen at once
room.batch(() => {
storageChanges(root);
});
// If storage changes are not synchronized, wait for them to finish
if (room.getStorageStatus() !== "synchronized") {
await room.events.storageStatus.waitUntil(
(status) => status === "synchronized"
);
}
// Leave when storage has been synchronized
serverClient.leave(roomId);
resolve();
});
}
```
## Start modifying storage
We can now start modify storage from the server! Import `modifyStorage`, pass a
room name, and use the callback to modify as you like.
```ts file="route.ts" highlight="6-8"
import { modifyStorage } from "./modifyStorage";
export async function POST() {
console.log("Updating storage");
await modifyStorage("my-liveblocks-room", (root) => {
root.get("list").push("item3");
});
console.log("Storage update complete!");
}
```
## Account for the service user in your app
Remember to account for the service user appearing in your presence. In our
`liveblocks.server.config.ts` we authenticated with `"_SERVICE_ACCOUNT"` as the
`userId`, so we’ll filter it out when using others in our application.
```tsx
import { shallow } from "@liveblocks/react";
import { useOthers } from "./liveblocks.config.ts";
function LiveAvatars() {
// Others, with the service account filtered out
const others = useOthers(
(others) => others.filter((other) => other.id !== "_SERVICE_ACCOUNT"),
shallow
);
// ...
}
```
A `shallow` equality check is necessary here, because `filter` creates a new
array every time.