diff --git a/CHANGELOG.md b/CHANGELOG.md
index c991b6f299..78178fa9dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,14 @@
## vNEXT (not yet released)
+## v3.24.1
+
### `@liveblocks/react`
- Add new hook `useMutableStorage()` to get direct access to the mutable Storage
root. See
[docs](https://liveblocks.io/docs/api-reference/liveblocks-react#useMutableStorage).
+- Fix thread resolved/unresolved status being reverted under some conditions.
+ (Thanks @VihaanAgarwal for the contribution!)
## v3.24.0
diff --git a/docs/pages/platform/data-storage.mdx b/docs/pages/platform/data-storage.mdx
index 01539910d7..9f885d9d36 100644
--- a/docs/pages/platform/data-storage.mdx
+++ b/docs/pages/platform/data-storage.mdx
@@ -21,6 +21,20 @@ and compliance reports, such as SOC 2 Type II and HIPAA.
+## Self-hosting
+
+Enterprise customers can self-host Liveblocks Sync, running it in their own
+cloud so that collaboration data stays within their infrastructure. Liveblocks
+provides ongoing releases, security updates, deployment guidance, and enterprise
+support. Self-hosting is available as a paid add-on on the
+[Enterprise plan](/docs/pricing/plans/enterprise), and we can discuss whether it
+meets your security, compliance, or data residency requirements. The rest of
+this page describes how data is stored on the Liveblocks cloud.
+
+
+
## Data stored
In most cases, sensitive or personal information can be excluded from what's
diff --git a/docs/pages/pricing/plans.mdx b/docs/pages/pricing/plans.mdx
index 0998df87a9..c19a5297e0 100644
--- a/docs/pages/pricing/plans.mdx
+++ b/docs/pages/pricing/plans.mdx
@@ -74,15 +74,16 @@ start of the next calendar month or you
## Plan inclusions
-| | Free | Pro | Team | Enterprise |
-| ------------------------------------------------------------------------- | ---- | -------- | ------------------------------------------------- | ---------- |
-| Remove "Powered by Liveblocks" badge | — | Included | Included | Included |
-| Multi-region hosting | — | — | — | Included |
-| SOC 2 report | — | — | Included | Included |
-| [SAML SSO](/docs/platform/account-management/saml) | — | — | Included | Included |
-| [SCIM / directory sync](/docs/platform/account-management/directory-sync) | — | — | — | Included |
-| Private Slack channel | — | — | Included | Included |
-| HIPAA BAA | — | — | per month add-on | Included |
-| [Management API](/docs/platform/management-api) | — | — | — | Included |
-| Role-based access control | — | — | Team level | Included |
-| Uptime SLA | — | — | — | 99.99% |
+| | Free | Pro | Team | Enterprise |
+| ------------------------------------------------------------------------- | ---- | -------- | ------------------------------------------------- | ----------- |
+| Remove "Powered by Liveblocks" badge | — | Included | Included | Included |
+| Multi-region hosting | — | — | — | Included |
+| SOC 2 report | — | — | Included | Included |
+| [SAML SSO](/docs/platform/account-management/saml) | — | — | Included | Included |
+| [SCIM / directory sync](/docs/platform/account-management/directory-sync) | — | — | — | Included |
+| Private Slack channel | — | — | Included | Included |
+| HIPAA BAA | — | — | per month add-on | Included |
+| [Management API](/docs/platform/management-api) | — | — | — | Included |
+| Role-based access control | — | — | Team level | Included |
+| [Self-hosting](/docs/platform/data-storage#Self-hosting) | — | — | — | Paid add-on |
+| Uptime SLA | — | — | — | 99.99% |
diff --git a/docs/pages/pricing/plans/enterprise.mdx b/docs/pages/pricing/plans/enterprise.mdx
index 725ff7d577..df352df542 100644
--- a/docs/pages/pricing/plans/enterprise.mdx
+++ b/docs/pages/pricing/plans/enterprise.mdx
@@ -44,6 +44,19 @@ Enterprise limits are customized for your organization. See
| Event log retention | Up to >}> |
| Environments | |
+## Self-hosting
+
+Self-hosting is available on the Enterprise plan as a paid add-on. You run
+Liveblocks Sync in your own cloud, so collaboration data stays within your
+infrastructure, while Liveblocks provides ongoing releases, security updates,
+deployment guidance, and enterprise support. See
+[data storage](/docs/platform/data-storage#Self-hosting) for more information,
+and get in touch to discuss whether we can meet your requirements.
+
+
+
## Security and compliance
Built-in support for global security and data protection standards, including:
diff --git a/packages/liveblocks-chat-sdk-adapter/package.json b/packages/liveblocks-chat-sdk-adapter/package.json
index 0bdf7e2fff..eb6bec7071 100644
--- a/packages/liveblocks-chat-sdk-adapter/package.json
+++ b/packages/liveblocks-chat-sdk-adapter/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/chat-sdk-adapter",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "Liveblocks adapter for the Chat SDK.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-client/package.json b/packages/liveblocks-client/package.json
index e200da8192..075ddee271 100644
--- a/packages/liveblocks-client/package.json
+++ b/packages/liveblocks-client/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/client",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A client that lets you interact with Liveblocks servers. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-codemirror/package.json b/packages/liveblocks-codemirror/package.json
index a40f294f1a..586c1d53bf 100644
--- a/packages/liveblocks-codemirror/package.json
+++ b/packages/liveblocks-codemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/codemirror",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "CodeMirror collaboration plugins backed by Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-core/package.json b/packages/liveblocks-core/package.json
index 1a0d797985..c7d4426f91 100644
--- a/packages/liveblocks-core/package.json
+++ b/packages/liveblocks-core/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/core",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "Private internals for Liveblocks. DO NOT import directly from this package!",
"type": "module",
"main": "./dist/index.cjs",
diff --git a/packages/liveblocks-emails/package.json b/packages/liveblocks-emails/package.json
index 9ae3af7b2b..3f406adefb 100644
--- a/packages/liveblocks-emails/package.json
+++ b/packages/liveblocks-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/emails",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A set of functions and utilities to make sending emails based on Liveblocks notification events easy. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-lexical/package.json b/packages/liveblocks-lexical/package.json
index ad433e6d5b..3a7cbffcd1 100644
--- a/packages/liveblocks-lexical/package.json
+++ b/packages/liveblocks-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/lexical",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "Lexical collaboration plugins backed by Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-node-lexical/package.json b/packages/liveblocks-node-lexical/package.json
index 60db275abb..175c51382c 100644
--- a/packages/liveblocks-node-lexical/package.json
+++ b/packages/liveblocks-node-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node-lexical",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A server-side utility that lets you modify lexical documents hosted in Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-node-prosemirror/package.json b/packages/liveblocks-node-prosemirror/package.json
index 5731f947c3..506931188b 100644
--- a/packages/liveblocks-node-prosemirror/package.json
+++ b/packages/liveblocks-node-prosemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node-prosemirror",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A server-side utility that lets you modify prosemirror and tiptap documents hosted in Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-node/package.json b/packages/liveblocks-node/package.json
index 43d1e2502c..b42b0f5d8d 100644
--- a/packages/liveblocks-node/package.json
+++ b/packages/liveblocks-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/node",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A server-side utility that lets you set up a Liveblocks authentication endpoint. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-prosemirror/package.json b/packages/liveblocks-prosemirror/package.json
index bc33b4dbf6..c1952bf18d 100644
--- a/packages/liveblocks-prosemirror/package.json
+++ b/packages/liveblocks-prosemirror/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/prosemirror",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "ProseMirror collaboration plugins backed by Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react-blocknote/package.json b/packages/liveblocks-react-blocknote/package.json
index 02c3ff92bd..483f1c8cca 100644
--- a/packages/liveblocks-react-blocknote/package.json
+++ b/packages/liveblocks-react-blocknote/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-blocknote",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "An integration of BlockNote + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react-flow/package.json b/packages/liveblocks-react-flow/package.json
index c587e509fa..479ff3b709 100644
--- a/packages/liveblocks-react-flow/package.json
+++ b/packages/liveblocks-react-flow/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-flow",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "An integration of React Flow to enable collaboration and realtime cursors with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react-lexical/package.json b/packages/liveblocks-react-lexical/package.json
index 6ba7e66594..b9bc5fb938 100644
--- a/packages/liveblocks-react-lexical/package.json
+++ b/packages/liveblocks-react-lexical/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-lexical",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "An integration of Lexical + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react-tiptap/package.json b/packages/liveblocks-react-tiptap/package.json
index 11c02a1648..ac3f6430d1 100644
--- a/packages/liveblocks-react-tiptap/package.json
+++ b/packages/liveblocks-react-tiptap/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-tiptap",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "An integration of TipTap + React to enable collaboration, comments, live cursors, and more with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react-ui/package.json b/packages/liveblocks-react-ui/package.json
index ccefcc417f..448d892e4a 100644
--- a/packages/liveblocks-react-ui/package.json
+++ b/packages/liveblocks-react-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react-ui",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A set of React pre-built components for the Liveblocks products. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react/package.json b/packages/liveblocks-react/package.json
index de070c0604..7d27deb634 100644
--- a/packages/liveblocks-react/package.json
+++ b/packages/liveblocks-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/react",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A set of React hooks and providers to use Liveblocks declaratively. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-react/src/__tests__/umbrella-store/patchThread.test.ts b/packages/liveblocks-react/src/__tests__/umbrella-store/patchThread.test.ts
new file mode 100644
index 0000000000..2258892bfb
--- /dev/null
+++ b/packages/liveblocks-react/src/__tests__/umbrella-store/patchThread.test.ts
@@ -0,0 +1,81 @@
+import { createClient } from "@liveblocks/client";
+import { describe, expect, test } from "vitest";
+
+import { UmbrellaStore } from "../../umbrella-store";
+import { createComment, createThread } from "./_dummies";
+
+describe("patchThread", () => {
+ test("should keep a settled resolve after a stale refetch", () => {
+ const store = createStore();
+ const thread = makeThread(false);
+ store.updateThreadifications([thread], [], []);
+
+ const settledAt = new Date("2024-01-01T00:01:00Z");
+ const optimisticId = store.optimisticUpdates.add({
+ type: "mark-thread-as-resolved",
+ threadId: thread.id,
+ updatedAt: settledAt,
+ });
+ store.patchThread(thread.id, optimisticId, { resolved: true }, settledAt);
+
+ expect(store.outputs.threads.get().get(thread.id)).toMatchObject({
+ resolved: true,
+ updatedAt: settledAt,
+ });
+
+ // A refetch started before the mutation settled can return an older snapshot.
+ store.updateThreadifications([thread], [], []);
+
+ expect(store.outputs.threads.get().get(thread.id)).toMatchObject({
+ resolved: true,
+ updatedAt: settledAt,
+ });
+ });
+
+ test("should keep a settled unresolve after a stale refetch", () => {
+ const store = createStore();
+ const thread = makeThread(true);
+ store.updateThreadifications([thread], [], []);
+
+ const settledAt = new Date("2024-01-01T00:01:00Z");
+ const optimisticId = store.optimisticUpdates.add({
+ type: "mark-thread-as-unresolved",
+ threadId: thread.id,
+ updatedAt: settledAt,
+ });
+ store.patchThread(thread.id, optimisticId, { resolved: false }, settledAt);
+
+ store.updateThreadifications([thread], [], []);
+
+ expect(store.outputs.threads.get().get(thread.id)).toMatchObject({
+ resolved: false,
+ updatedAt: settledAt,
+ });
+ });
+});
+
+function createStore() {
+ return new UmbrellaStore(
+ createClient({
+ publicApiKey: "pk_xxx",
+ })
+ );
+}
+
+function makeThread(resolved: boolean) {
+ const createdAt = new Date("2024-01-01T00:00:00Z");
+ return createThread({
+ id: "th_1",
+ roomId: "room_1",
+ createdAt,
+ updatedAt: createdAt,
+ resolved,
+ comments: [
+ createComment({
+ threadId: "th_1",
+ roomId: "room_1",
+ createdAt,
+ }),
+ ],
+ });
+}
diff --git a/packages/liveblocks-react/src/__tests__/useMarkThreadAsResolved.test.tsx b/packages/liveblocks-react/src/__tests__/useMarkThreadAsResolved.test.tsx
index caf912db80..7228df92f4 100644
--- a/packages/liveblocks-react/src/__tests__/useMarkThreadAsResolved.test.tsx
+++ b/packages/liveblocks-react/src/__tests__/useMarkThreadAsResolved.test.tsx
@@ -1,4 +1,4 @@
-import { nanoid, Permission } from "@liveblocks/core";
+import { nanoid, Permission, Promise_withResolvers } from "@liveblocks/core";
import { act, renderHook } from "@testing-library/react";
import { HttpResponse } from "msw";
import { setupServer } from "msw/node";
@@ -27,6 +27,7 @@ beforeEach(() => {
});
afterEach(() => {
+ vi.useRealTimers();
MockWebSocket.reset();
server.resetHandlers();
});
@@ -34,9 +35,26 @@ afterEach(() => {
afterAll(() => server.close());
describe("useMarkThreadAsResolved", () => {
- test("should mark thread as resolved optimistically", async () => {
+ test("should keep a settled resolve after an in-flight stale refetch", async () => {
+ const initialDate = new Date("2024-01-01T00:00:00Z");
+ const requestedAt = new Date("2024-01-01T00:01:00Z");
+ const refetchedAt = new Date("2024-01-01T00:02:00Z");
+ const settledAt = new Date("2024-01-01T00:03:00Z");
+ vi.useFakeTimers({ toFake: ["Date"] });
+ vi.setSystemTime(requestedAt);
+
const roomId = nanoid();
- const initialThread = dummyThreadData({ roomId, resolved: false });
+ const initialThread = dummyThreadData({
+ roomId,
+ createdAt: initialDate,
+ updatedAt: initialDate,
+ resolved: false,
+ });
+ const staleThread = {
+ ...initialThread,
+ updatedAt: refetchedAt,
+ };
+ const mutationResponse = Promise_withResolvers();
let hasCalledMarkThreadAsResolved = false;
server.use(
@@ -54,8 +72,9 @@ describe("useMarkThreadAsResolved", () => {
},
});
}),
- mockMarkThreadAsResolved({ threadId: initialThread.id }, () => {
+ mockMarkThreadAsResolved({ threadId: initialThread.id }, async () => {
hasCalledMarkThreadAsResolved = true;
+ await mutationResponse.promise;
return HttpResponse.json(null, { status: 200 });
})
@@ -63,6 +82,7 @@ describe("useMarkThreadAsResolved", () => {
const {
room: { RoomProvider, useThreads, useMarkThreadAsResolved },
+ umbrellaStore,
} = createContextsForTest();
const { result, unmount } = renderHook(
@@ -89,8 +109,21 @@ describe("useMarkThreadAsResolved", () => {
await vi.waitFor(() => expect(hasCalledMarkThreadAsResolved).toEqual(true));
+ act(() => umbrellaStore.updateThreadifications([staleThread], [], []));
+
expect(result.current.threads![0]?.resolved).toBe(true);
+ vi.setSystemTime(settledAt);
+ act(() => mutationResponse.resolve());
+
+ await vi.waitFor(() =>
+ expect(umbrellaStore.optimisticUpdates.signal.get()).toHaveLength(0)
+ );
+ expect(result.current.threads?.[0]?.resolved).toBe(true);
+ expect(result.current.threads?.[0]?.updatedAt.getTime()).toBeGreaterThan(
+ refetchedAt.getTime()
+ );
+
unmount();
});
});
diff --git a/packages/liveblocks-react/src/room.tsx b/packages/liveblocks-react/src/room.tsx
index 0c52401a2c..bfe240347c 100644
--- a/packages/liveblocks-react/src/room.tsx
+++ b/packages/liveblocks-react/src/room.tsx
@@ -2804,7 +2804,7 @@ function useMarkRoomThreadAsResolved(roomId: string) {
threadId,
optimisticId,
{ resolved: true },
- updatedAt
+ new Date()
);
},
(err: Error) =>
@@ -2868,7 +2868,7 @@ function useMarkRoomThreadAsUnresolved(roomId: string) {
threadId,
optimisticId,
{ resolved: false },
- updatedAt
+ new Date()
);
},
(err: Error) =>
diff --git a/packages/liveblocks-react/src/umbrella-store.ts b/packages/liveblocks-react/src/umbrella-store.ts
index 4c181158e9..48ad22d2dd 100644
--- a/packages/liveblocks-react/src/umbrella-store.ts
+++ b/packages/liveblocks-react/src/umbrella-store.ts
@@ -2113,7 +2113,7 @@ export class UmbrellaStore {
return this.#updateThread(
threadId,
optimisticId,
- (thread) => ({ ...thread, ...compactObject(patch) }),
+ (thread) => ({ ...thread, ...compactObject(patch), updatedAt }),
updatedAt
);
}
diff --git a/packages/liveblocks-redux/package.json b/packages/liveblocks-redux/package.json
index 6b46db8b26..907109f4bb 100644
--- a/packages/liveblocks-redux/package.json
+++ b/packages/liveblocks-redux/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/redux",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A store enhancer to integrate Liveblocks into Redux stores. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-yjs/package.json b/packages/liveblocks-yjs/package.json
index 7e51d4c6d9..49fc9a1c11 100644
--- a/packages/liveblocks-yjs/package.json
+++ b/packages/liveblocks-yjs/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/yjs",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "Integrate your existing or new Yjs documents with Liveblocks.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",
diff --git a/packages/liveblocks-zustand/package.json b/packages/liveblocks-zustand/package.json
index 9b346ea463..186369df7b 100644
--- a/packages/liveblocks-zustand/package.json
+++ b/packages/liveblocks-zustand/package.json
@@ -1,6 +1,6 @@
{
"name": "@liveblocks/zustand",
- "version": "3.24.0",
+ "version": "3.24.1",
"description": "A middleware for Zustand to automatically synchronize your stores with Liveblocks. Liveblocks is the all-in-one toolkit to build collaborative products like Figma, Notion, and more.",
"license": "Apache-2.0",
"author": "Liveblocks Inc.",