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.",