diff --git a/apps/web/src/components/HomeCodeDemo/HomeCodeDemo.tsx b/apps/web/src/components/HomeCodeDemo/HomeCodeDemo.tsx index e4c51974..c1037161 100644 --- a/apps/web/src/components/HomeCodeDemo/HomeCodeDemo.tsx +++ b/apps/web/src/components/HomeCodeDemo/HomeCodeDemo.tsx @@ -104,7 +104,7 @@ export const HomeCodeDemo = memo((props) => { }, { code: codeBlock` - import { y } from "@pluv/react"; + import { yjs } from "@pluv/crdt-yjs"; import { PluvRoomProvider } from "client/pluv"; import type { FC, ReactNode } from "react"; @@ -115,9 +115,9 @@ export const HomeCodeDemo = memo((props) => { const initialPresence = { selection: null }; const initialStorage = () => ({ - boxes: y.object({ - first: y.object({ x: -48, y: 0 }), - second: y.object({ x: 48, y: 0 }), + boxes: yjs.object({ + first: yjs.object({ x: -48, y: 0 }), + second: yjs.object({ x: 48, y: 0 }), }), }); @@ -138,7 +138,8 @@ export const HomeCodeDemo = memo((props) => { }, { code: codeBlock` - import { createBundle, createClient, y } from "@pluv/react"; + import { yjs } from "@pluv/crdt-yjs"; + import { createBundle, createClient } from "@pluv/react"; import type { io } from "server/pluv"; import { z } from "zod"; @@ -177,24 +178,28 @@ export const HomeCodeDemo = memo((props) => { selection: z.nullable(z.string()), }), // This can be overwritten at the provider level - initialStorage: () => ({ - boxes: y.object({ - first: y.object({ x: 0, y: 0 }), - second: y.object({ x: 0, y: 0 }), + initialStorage: yjs.doc(() => ({ + boxes: yjs.object({ + first: yjs.object({ x: 0, y: 0 }), + second: yjs.object({ x: 0, y: 0 }), }), - }), + })), }); `, name: "client/pluv.ts", }, { code: codeBlock` + import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { z } from "zod"; // Create @pluv/io websocket manager for Node.js - export const io = createIO({ platform: platformNode() }); + export const io = createIO({ + crdt: yjs, + platform: platformNode(), + }); `, name: "server/pluv.ts", }, diff --git a/apps/web/src/inputs/docs/1--Introduction.mdx b/apps/web/src/inputs/docs/1--Introduction.mdx index 6ac0e097..93dc6916 100644 --- a/apps/web/src/inputs/docs/1--Introduction.mdx +++ b/apps/web/src/inputs/docs/1--Introduction.mdx @@ -22,7 +22,7 @@ Multi-platform, E2E type-safe realtime packages npm @pluv/io - GitHub + License MIT Commitizen friendly @@ -39,9 +39,11 @@ Multi-platform, E2E type-safe realtime packages Existing open-source tools for building real-time APIs come with several trade-offs. With [socket.io](https://socket.io) or [y-websocket](https://github.com/yjs/y-websocket), you get built-in support for things like heartbeats, rooms and awareness without any automatic type-safety server-to-client. Meanwhile, with [GraphQL](https://graphql.org/) you get type-safety in subscriptions but lose those same features. pluv.io unifies these experiences by providing **common websocket API capabilities** and **end-to-end type-safety** server-to-client. -## Built for multiple runtimes and frameworks +## Bring your own runtime, framework and CRDT -pluv.io supports hosting on either [Node.js](https://nodejs.org/) or [Cloudflare Workers](https://workers.cloudflare.com/). Support for multiple frameworks is also planned, with [React](https://beta.es.reactjs.org/) already available with [@pluv/react](https://www.npmjs.com/package/@pluv/react). If you're building for other frameworks, [@pluv/client](https://www.npmjs.com/package/@pluv/client) is framework agnostic and can meet your needs. +Today, pluv.io supports hosting on either [Node.js](https://nodejs.org/) or [Cloudflare Workers](https://workers.cloudflare.com/). Support for multiple frameworks is also planned, with [React](https://beta.es.reactjs.org/) already available with [@pluv/react](https://www.npmjs.com/package/@pluv/react). If you're building for other frameworks, [@pluv/client](https://www.npmjs.com/package/@pluv/client) is framework agnostic and can meet your needs. + +pluv.io also supports [Yjs](https://docs.yjs.dev/) and experimentally supports the [Loro](https://loro.dev/) CRDT library. ## Features and roadmap @@ -52,14 +54,21 @@ pluv.io supports hosting on either [Node.js](https://nodejs.org/) or [Cloudflare - [x] Rooms - [x] Authentication - [x] Awareness + Presence -- [x] [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (with [Yjs](https://docs.yjs.dev/)) - - [x] **Shared Types** - - [x] [Map](https://docs.yjs.dev/api/shared-types/y.map) - - [x] [Array](https://docs.yjs.dev/api/shared-types/y.array) - - [x] [Text](https://docs.yjs.dev/api/shared-types/y.text) - - [x] [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment) - - [x] [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement) - - [x] [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext) +- [x] [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) + - [x] [Yjs](https://docs.yjs.dev/) + - [x] **Shared Types** + - [x] [Map](https://docs.yjs.dev/api/shared-types/y.map) + - [x] [Array](https://docs.yjs.dev/api/shared-types/y.array) + - [x] [Text](https://docs.yjs.dev/api/shared-types/y.text) + - [x] [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment) + - [x] [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement) + - [x] [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext) + - [x] [Loro](https://loro.dev/) + - [x] **Containers** + - [x] List + - [x] Map + - [x] Text + - [ ] Tree - [ ] Studio (admin & developer panel) ### Runtimes @@ -77,6 +86,7 @@ pluv.io supports hosting on either [Node.js](https://nodejs.org/) or [Cloudflare ### Frontends - [x] [React.js](https://beta.reactjs.org/) +- [ ] [Solid.js](https://www.solidjs.com/) - [ ] [Vue.js](https://vuejs.org/) - [ ] [Svelte](https://svelte.dev/) diff --git a/apps/web/src/inputs/docs/2--Quickstart.mdx b/apps/web/src/inputs/docs/2--Quickstart.mdx index ebbc25c9..c3da2ed5 100644 --- a/apps/web/src/inputs/docs/2--Quickstart.mdx +++ b/apps/web/src/inputs/docs/2--Quickstart.mdx @@ -15,11 +15,13 @@ Learn how to quickly setup and get started with @pluv/io. | Purpose | Location | Install command | |---------------------------------------------------------|----------|---------------------------------------| -| Register websockets and custom events | Server | npm install @pluv/io yjs | -| Call and listen to events. Interact with shared storage | Client | npm install @pluv/client yjs | -| React-bindings for @pluv/client | Client | npm install @pluv/react yjs | +| Register websockets and custom events | Server | npm install @pluv/io | +| Call and listen to events. Interact with shared storage | Client | npm install @pluv/client | +| React-bindings for @pluv/client | Client | npm install @pluv/react | | Adapter for Node.js runtime | Server | npm install @pluv/platform-node ws | | Adapter for Cloudflare Workers runtime | Server | npm install @pluv/platform-cloudflare | +| yjs CRDT | Both | npm install @pluv/crdt-yjs yjs | +| loro CRDT | Both | npm install @pluv/crdt-loro loro-crdt | ### Installation Example @@ -29,12 +31,15 @@ Here is an example installation for [npm](https://www.npmjs.com/), assuming you # For the server npm install @pluv/io @pluv/platform-node # Server peer-dependencies -npm install yjs ws zod +npm install ws zod # For the client npm install @pluv/react # Client peer-dependencies -npm install react react-dom yjs zod +npm install react react-dom zod + +# If you want to use storage features, install your preferred CRDT +npm install @pluv/crdt-yjs yjs ``` ## Defining a backend PluvIO instance @@ -48,10 +53,15 @@ Define an io (websocket client) instance on the server codebase: ```ts // backend/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; -export const io = createIO({ platform: platformNode() }); +export const io = createIO({ + // Optional: Only if you require CRDT features + crdt: yjs, + platform: platformNode(), +}); // Export the websocket client io type, instead of the client itself export type AppPluvIO = typeof io; @@ -67,11 +77,16 @@ Use `io.event` to define type-safe websocket events on the io instance. The two ```ts // backend/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { z } from "zod"; -export const io = createIO({ platform: platformNode() }) +export const io = createIO({ + // Optional: Only if you require CRDT features + crdt: yjs, + platform: platformNode(), +}) // When event "SEND_MESSAGE" is sent by the frontend and received // on the server .event("SEND_MESSAGE", { @@ -131,6 +146,7 @@ Now that the io instance is setup on the backend, we can setup the frontend clie ```ts // frontend/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createBundle, createClient, y } from "@pluv/react"; import type { AppPluvIO } from "server/io"; @@ -162,7 +178,11 @@ export const { useOthers, useRoom, useStorage, -} = createRoomBundle(); +} = createRoomBundle({ + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), +}); ``` ### Wrap with your pluv.io providers diff --git a/apps/web/src/inputs/docs/@pluv_client/API Reference.mdx b/apps/web/src/inputs/docs/@pluv_client/API Reference.mdx index 41e95e69..6784014c 100644 --- a/apps/web/src/inputs/docs/@pluv_client/API Reference.mdx +++ b/apps/web/src/inputs/docs/@pluv_client/API Reference.mdx @@ -75,7 +75,7 @@ Create a `PluvRoom` that websockets can join and leave. The second argument is a ```ts // frontend/room.ts -import { y } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; import { z } from "zod"; import { client } from "./io.ts"; @@ -95,27 +95,9 @@ const room = client.createRoom("my-example-room", { /** * @description Define the initial storage for the room */ - initialStorage: () => ({ - messages: y.array(["hello world!"]), - }), - /** - * @description This is the same `captureTimeout` option from yjs's UndoManager. - * This specifies a number in ms, during which edits are merged together to be - * undone together. Set this to 0, to track each transacted change individually. - * @see https://docs.yjs.dev/api/undo-manager - * @default 500 - */ - captureTimeout: 500, - /** - * @desription This is the same `trackedOrigins` option from yjs's UndoManager. - * This specifies transaction origins (strings only) to filter which transactions - * can be undone. - * When omitted, the user's connection id will be tracked. When provided, - * specifies additional tracked origins besides the user's connection id. - * @see https://docs.yjs.dev/api/undo-manager - * @default undefined - */ - trackedOrigins: ["user-123"], + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), }); ``` @@ -288,6 +270,20 @@ This returns state information for the current connection to this room. const connection = room.getConnection(); ``` +### PluvRoom.getDoc + +Returns `AbstractCrdtDoc` from `@pluv/crdt` + +This returns an instance of the `AbstractCrdtDoc` that holds the underlying CRDT doc. For instance, when using `@pluv/crdt-yjs`, this will hold a [yjs](https://yjs.dev/) in its `value` property. + +```ts +import { yjs } from "@pluv/crdt-yjs"; +import { Doc as YDoc } from "yjs"; + +const doc: yjs.CrdtYjsDoc = room.getDoc(); +const ydoc: YDoc = doc.value; +``` + ### PluvRoom.getMyPresence Returns `TPresence | null` @@ -332,11 +328,12 @@ const others = room.getOthers(); ### PluvRoom.getStorage -Returns a [yjs](https://yjs.dev/) shared type defined on the client config. +Returns a `CrdtAbstractType` from `@pluv/crdt` that holds a CRDT shared type. For instance, if using `@pluv/crdt-yjs`, the `CrdtAbstractType` will hold a [yjs](https://yjs.dev/) shared type in its `value` property. ```ts // frontend/room.ts -import { y } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; +import { Array as YArray } from "yjs"; import { z } from "zod"; import { client } from "./io.ts"; @@ -350,12 +347,15 @@ const room = client.createRoom("my-example-room", { selectionId: null, }, // Define the initial storage for the room - initialStorage: () => ({ - messages: y.array(["hello world!"]), - }), + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), }); -const messages = room.getStorage("messages"); +// Get messages as defined from the `createRoom` config +const messages: yjs.CrdtYjsArray = room.getStorage("messages"); +// Get the underlying CRDT shared type via the value property +const sharedType: YArray = messages.value; ``` ### PluvRoom.other @@ -397,11 +397,11 @@ room.redo(); Returns `() => void` -Subscribes to a given root-level value in the [yjs](https://yjs.dev) document storage. +Subscribes to a given root-level value in the room's document storage. The data in the callback will be a serialized value rather than the CRDT's instance. ```ts // frontend/room.ts -import { y } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; import { z } from "zod"; import { client } from "./io.ts"; @@ -415,13 +415,16 @@ const room = client.createRoom("my-example-room", { selectionId: null, }, // Define the initial storage for the room - initialStorage: () => ({ - messages: y.array(["hello world!"]), - }), + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), }); -const unsubscribe = room.storage("messages", (messages) => { +const unsubscribe = room.storage("messages", (messages: string[]) => { console.log(messages); + + // If the AbstractCrdtType is needed, you call call getStorage here. + const sharedType = room.getStorage("messages"); }); // ... diff --git a/apps/web/src/inputs/docs/@pluv_client/Create Client.mdx b/apps/web/src/inputs/docs/@pluv_client/Create Client.mdx index ee4ba491..db431aa6 100644 --- a/apps/web/src/inputs/docs/@pluv_client/Create Client.mdx +++ b/apps/web/src/inputs/docs/@pluv_client/Create Client.mdx @@ -19,8 +19,6 @@ Generally, it is recommended to use framework-specific bindings for your particu ```bash # For the frontend npm install @pluv/client -# Frontend peer-dependencies -npm install react react-dom yjs zod ``` ### Define a frontend PluvClient diff --git a/apps/web/src/inputs/docs/@pluv_client/Create Rooms.mdx b/apps/web/src/inputs/docs/@pluv_client/Create Rooms.mdx index b2e1caa8..b0be4468 100644 --- a/apps/web/src/inputs/docs/@pluv_client/Create Rooms.mdx +++ b/apps/web/src/inputs/docs/@pluv_client/Create Rooms.mdx @@ -16,7 +16,7 @@ To create a `PluvRoom`, you must first have a `PluvClient` defined. Refer to the ```ts // frontend/room.ts -import { y } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; import { z } from "zod"; import { client } from "./io"; @@ -32,9 +32,9 @@ export const room = client.createRoom(ROOM_NAME, { selectionId: null, }, // Define the initial storage for the room - initialStorage: () => ({ - messages: y.array(["hello world!"]), - }), + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), }); ``` diff --git a/apps/web/src/inputs/docs/@pluv_io/API Reference.mdx b/apps/web/src/inputs/docs/@pluv_io/API Reference.mdx index c5312926..fa0742af 100644 --- a/apps/web/src/inputs/docs/@pluv_io/API Reference.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/API Reference.mdx @@ -14,6 +14,7 @@ Returns a `PluvIO` instance. Creates a server-side `PluvIO` client that will create rooms to register your websockets to. Below will show a available configurations for `createIO`. To learn more about `createIO`, read [Authorization](/docs/io/authorization), [Cloudflare Workers](/docs/io/cloudflare-workers), [Node.js](/docs/io/node.js) and [PubSub](/docs/io/pubsub). ```ts +import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { z } from "zod"; @@ -35,6 +36,9 @@ const io = createIO({ name: z.string(), }), }, + // Optional. Specify to use Yjs CRDT for storage. + // This is required only if/when you wish to use storage features. + crdt: yjs, // Optional. Only needed to load persisted storage for rooms. // Load storage for a newly created room. getInitialStorage: async ({ room }) => { @@ -45,8 +49,8 @@ const io = createIO({ // Enable @pluv/io for Node.js platform: platformNode(), // Optional. Only needed for persisting storage for rooms. - // Before the room is destroyed, persist the state to a database - // to load when the room is re-created. + // Before the room is destroyed, persist the state to a database to load + // when the room is re-created. onRoomDeleted: async ({ encodedState, room }) => { await db.room.upsert({ where: { room }, @@ -54,9 +58,9 @@ const io = createIO({ update: { encodedState }, }); }, - // It is unnecessary to save the storage state in this listener. - // If a room already exists, users will load the currently active - // storage before the storage from initialStorage. + // Optional. It is unnecessary to save the storage state in this listener. + // If a room already exists, users will load the currently active storage + // before the storage from initialStorage. onStorageUpdated: ({ encodedState, room }) => { console.log(room, encodedState); }, @@ -94,7 +98,9 @@ import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { z } from "zod"; -const io = createIO({ platform: platformNode() }) +const io = createIO({ + platform: platformNode(), +}) // When event "SEND_MESSAGE" is sent by the frontend and received // on the server .event("SEND_MESSAGE", { diff --git a/apps/web/src/inputs/docs/@pluv_io/Authorization.mdx b/apps/web/src/inputs/docs/@pluv_io/Authorization.mdx index 5f9e37a7..70a69c7c 100644 --- a/apps/web/src/inputs/docs/@pluv_io/Authorization.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/Authorization.mdx @@ -82,7 +82,7 @@ server.listen(PORT, () => { ## Connect to your authorization endpoint -Now that we have our endpoint defined, connect our frontend client to our authorization endpoint. +Now that we have your endpoint defined, connect your frontend client to your authorization endpoint. ```ts // frontend/io.ts @@ -95,7 +95,7 @@ const client = createClient({ // Set to `true` if you have authorization on your io instance, and you // want to use the default of (room) => `/api/pluv/authorize?room=${room}` // This is the default from `createPluvHandler` - authEndpoint: (room) => `http://localhost:3000/api/authorize?room=${room}`, + authEndpoint: (room) => `/api/authorize?room=${room}`, // ... }); ``` @@ -110,7 +110,7 @@ import type { AppPluvIO } from "server/io"; const client = createClient({ authEndpoint: (room) => ({ - url: "http://localhost:3000/api/authorize", + url: "/api/authorize", // You can use fetch options as well like so options: { method: "POST", diff --git a/apps/web/src/inputs/docs/@pluv_io/Cloudflare Workers.mdx b/apps/web/src/inputs/docs/@pluv_io/Cloudflare Workers.mdx index 3fd8c2e5..3d474906 100644 --- a/apps/web/src/inputs/docs/@pluv_io/Cloudflare Workers.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/Cloudflare Workers.mdx @@ -18,8 +18,9 @@ Let's step through how we'd put together a real-time API for Cloudflare Workers. ```bash # For the server npm install @pluv/io @pluv/platform-cloudflare + # Server peer-dependencies -npm install yjs zod +npm install zod ``` ### Create PluvIO instance @@ -141,7 +142,9 @@ If you don't need to modify your DurableObject or Cloudflare Worker handler too import { createIO } from "@pluv/io"; import { createPluvHandler, platformCloudflare } from "@pluv/platform-cloudflare"; -const io = createIO({ platform: platformCloudflare() }); +const io = createIO({ + platform: platformCloudflare(), +}); const Pluv = createPluvHandler({ // Your PluvIO instance diff --git a/apps/web/src/inputs/docs/@pluv_io/Define Events.mdx b/apps/web/src/inputs/docs/@pluv_io/Define Events.mdx index 1a6ec81a..d7ad4fe7 100644 --- a/apps/web/src/inputs/docs/@pluv_io/Define Events.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/Define Events.mdx @@ -16,7 +16,9 @@ import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { z } from "zod"; -export const io = createIO({ platform: platformNode() }) +export const io = createIO({ + platform: platformNode(), +}) // When event "SEND_MESSAGE" is sent by the frontend and received // on the server .event("SEND_MESSAGE", { diff --git a/apps/web/src/inputs/docs/@pluv_io/Loading Storage.mdx b/apps/web/src/inputs/docs/@pluv_io/Loading Storage.mdx index 7eac9517..4b78c481 100644 --- a/apps/web/src/inputs/docs/@pluv_io/Loading Storage.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/Loading Storage.mdx @@ -14,11 +14,14 @@ Listen to when a room is deleted so that when the same room is re-created, users ```ts // server/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; import { db } from "./db"; export const io = createIO({ + // Specify which CRDT to use here + crdt: yjs, // Load storage for a newly created room. getInitialStorage: async ({ room }) => { const existingRoom = await db.room.findUnique({ where: { room } }); diff --git a/apps/web/src/inputs/docs/@pluv_io/Node.js.mdx b/apps/web/src/inputs/docs/@pluv_io/Node.js.mdx index 842da847..6bbefd88 100644 --- a/apps/web/src/inputs/docs/@pluv_io/Node.js.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/Node.js.mdx @@ -18,8 +18,9 @@ Let's step through how we'd put together a real-time API for Node.js. ```bash # For the server npm install @pluv/io @pluv/platform-node + # Server peer-dependencies -npm install ws yjs zod +npm install ws zod ``` ### Create PluvIO instance @@ -32,7 +33,9 @@ Define an io (websocket client) instance on the server codebase: import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; -export const io = createIO({ platform: platformNode() }); +export const io = createIO({ + platform: platformNode(), +}); // Export the websocket client io type, instead of the client itself export type AppPluvIO = typeof io; @@ -85,7 +88,9 @@ import express from "express"; import Http from "http"; import WS from "ws"; -const io = createIO({ platform: platformNode() }); +const io = createIO({ + platform: platformNode(), +}); const app = express(); const server = Http.createServer(app); diff --git a/apps/web/src/inputs/docs/@pluv_io/PubSub.mdx b/apps/web/src/inputs/docs/@pluv_io/PubSub.mdx index e33884cf..1ad75401 100644 --- a/apps/web/src/inputs/docs/@pluv_io/PubSub.mdx +++ b/apps/web/src/inputs/docs/@pluv_io/PubSub.mdx @@ -23,7 +23,7 @@ Install your PubSub and Persistance of your choosing: # For the server npm install @pluv/pubsub-redis @pluv/persistance-redis # Peer-dependencies -npm install ioredis yjs +npm install ioredis ``` ### Setup your PubSub and Persistance diff --git a/apps/web/src/inputs/docs/@pluv_react/API Reference.mdx b/apps/web/src/inputs/docs/@pluv_react/API Reference.mdx index 7bd421bc..356d1bc0 100644 --- a/apps/web/src/inputs/docs/@pluv_react/API Reference.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/API Reference.mdx @@ -30,6 +30,9 @@ Returns a `CreateRoomBundle` instance Creates a bundle to be able to access `PluvRoom` capabilities from various react hooks. ```ts +import { yjs } from "@pluv/crdt-yjs"; + +// Destructured room bundle export const { // components PluvRoomProvider, @@ -39,6 +42,7 @@ export const { useCanRedo, useCanUndo, useConnection, + useDoc, useEvent, useMyPresence, useMyself, @@ -51,40 +55,24 @@ export const { useUndo, } = createRoomBundle({ /** - * @description Define the initial storage for the room + * @optional + * @description Define the initial storage for the room, as well as the CRDT to use */ - initialStorage: () => ({ - messages: y.array([ - y.object({ + initialStorage: yjs.doc(() => ({ + messages: yjs.array([ + yjs.object({ message: "hello", name: "leedavidcs", }), ]), - }), + })), /** + * @optional * @description Define your presence schema */ presence: z.object({ count: z.number(), }), - /** - * @description This is the same `captureTimeout` option from yjs's UndoManager. - * This specifies a number in ms, during which edits are merged together to be - * undone together. Set this to 0, to track each transacted change individually. - * @see https://docs.yjs.dev/api/undo-manager - * @default 500 - */ - captureTimeout: 500, - /** - * @desription This is the same `trackedOrigins` option from yjs's UndoManager. - * This specifies transaction origins (strings only) to filter which transactions - * can be undone. - * When omitted, the user's connection id will be tracked. When provided, - * specifies additional tracked origins besides the user's connection id. - * @see https://docs.yjs.dev/api/undo-manager - * @default undefined - */ - trackedOrigins: ["user-123"], }); ``` @@ -108,41 +96,48 @@ This is the bundle returned from `createRoomBundle`. ### MockedRoomProvider -React provider component to mock `PluvRoomProvider`. Enables hooks from `CreateRoomBundle` while completely offline. This can be useful in [Storybook](https://storybook.js.org/) within a decorator when mocking `PluvRoomProvider`. +React provider component to mock `PluvRoomProvider`. Enables hooks from `CreateRoomBundle` while completely offline. This can be useful in [Storybook](https://storybook.js.org/) within a storybook decorator when mocking `PluvRoomProvider`. ```tsx -import { y } from "@pluv/react"; +import { yjs } from "@pluv/crdt-yjs"; import { MockedRoomProvider } from "./io"; - ({ - EMOJI_RECEIVED: { emojiCode }, - }), - }} - /** - * @description Define your initial presence - */ - initialPresence={{ - selectionId: null, - }} - /** - * @description Define the initial initial storage for the room - */ - initialStorage={() => ({ - messages: y.array([]), - })} - /** - * @description Set the room id - */ - room="my-mock-room" -> - {children} - +const MyComponent: FC = () => { + return ( + ({ + EMOJI_RECEIVED: { emojiCode }, + }), + }} + /** + * @optional + * @description Define your initial presence + */ + initialPresence={{ + selectionId: null, + }} + /** + * @optional + * @description Define the initial storage for the room, as well as the CRDT to use + */ + initialStorage={() => ({ + messages: yjs.array(), + })} + /** + * @required + * @description Set the room id + */ + room="my-mock-room" + > + {children} + + ); +}; ``` ### PluvRoomProvider @@ -150,36 +145,44 @@ import { MockedRoomProvider } from "./io"; React provider component to enable `CreateRoomBundle` hooks within child components. Mounting this component connects to a real-time room with other connected users. Unmounting this component will disconnect the room. See [Create Rooms](/docs/react/api-reference) for more details. ```tsx -import { y } from "@pluv/react"; +import { yjs } from "@pluv/crdt-yjs"; import { MockedRoomProvider } from "./io"; - ({ - messages: y.array([]), - })} - /** - * @description Emits an error whenever authorization fails, for - * monitoring or debugging purposes - */ - onAuthorizationFail={(error: Error) => { - console.error(error); - }} - /** - * @description Set the room id - */ - room="my-room-id" -> - {children} - +const MyComponent: FC = () => { + return ( + ({ + messages: yjs.array(), + })} + /** + * @optional + * @description Emits an error whenever authorization fails, for + * monitoring or debugging purposes + */ + onAuthorizationFail={(error: Error) => { + console.error(error); + }} + /** + * @required + * @description Set the room id + */ + room="my-room-id" + > + {children} + + ); +}; ``` ### useBroadcast @@ -248,6 +251,7 @@ Returns the current user's presence, and a function to update the user's presenc ```tsx const { useMyPresence } = createRoomBundle({ + // ... presence: z.object({ selectionId: z.nullable(z.string()), selectionColor: z.string(), @@ -341,22 +345,23 @@ redo(); ### useStorage -Returns `[TData | null, Yjs.SharedType | null]` +Returns `[TData | null, AbstractCrdtType | null]` -Returns a base-level property of our [yjs](https://yjs.dev) storage as a serialized value and a [Yjs SharedType](https://docs.yjs.dev/getting-started/working-with-shared-types). The component is re-rendered whenever the Yjs SharedType is updated. +Returns a base-level property of our CRDT storage as a serialized value and a `@pluv/crdt` `AbstractCrdtType` that holds a shared type of our specified CRDT. The component is re-rendered whenever the `AbstractCrdtType` is updated. The returned values are null while the room is still connecting or +initializing its storage. ```tsx -import { y } from "@pluv/react"; +import { yjs } from "@pluv/crdt-yjs"; const { useStorage } = createRoomBundle({ - initialStorage: () => ({ - messages: y.array(["hello"]), - }), + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello"]), + })), }); const [messages, sharedType] = useStorage("messages"); -sharedType.push(["world~!"]); +sharedType.push("world~!"); messages.map((message, key) =>
{message}
); ``` diff --git a/apps/web/src/inputs/docs/@pluv_react/Create Bundle.mdx b/apps/web/src/inputs/docs/@pluv_react/Create Bundle.mdx index 4fff06f0..43079c7a 100644 --- a/apps/web/src/inputs/docs/@pluv_react/Create Bundle.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/Create Bundle.mdx @@ -16,10 +16,18 @@ First, create a `PluvIO` instance from the `@pluv/io` package in your backend co ```ts // backend/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createIO } from "@pluv/io"; import { platformNode } from "@pluv/platform-node"; -export const io = createIO({ platform: platformNode() }); +export const io = createIO({ + /** + * @optional + * @description This is required if you intend to use storage. Specify crdt to use for storage + */ + crdt: yjs, + platform: platformNode(), +}); // Export the websocket client io type, instead of the client itself export type AppPluvIO = typeof io; @@ -32,6 +40,7 @@ Then, import your `PluvIO` type to the frontend, and create your type-safe React ```ts // frontend/io.ts +import { yjs } from "@pluv/crdt-yjs"; import { createBundle, createClient } from "@pluv/react"; import { z } from "zod"; // import your PluvIO instance from your backend codebase @@ -67,7 +76,10 @@ export const { useOthers, useRoom, useStorage, -} = createRoomBundle(); +} = createRoomBundle({ + // Optional: Specify which CRDT you are using, as well as the initial storage + initialStorage: yjs.doc(() => ({})), +}); ``` ## Next Steps diff --git a/apps/web/src/inputs/docs/@pluv_react/Create Rooms.mdx b/apps/web/src/inputs/docs/@pluv_react/Create Rooms.mdx index 8e391f22..15526777 100644 --- a/apps/web/src/inputs/docs/@pluv_react/Create Rooms.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/Create Rooms.mdx @@ -18,13 +18,13 @@ Create and connect to a `PluvRoom` by mounting a `PluvRoomProvider` from your `@ ```tsx import { FC } from "react"; -import { PluvRoomProvider } from "frontend/io"; +import { pluvRoom } from "frontend/io"; export const MyPage: FC = () => { return ( - + - + ); }; ``` diff --git a/apps/web/src/inputs/docs/@pluv_react/Custom Events.mdx b/apps/web/src/inputs/docs/@pluv_react/Custom Events.mdx index 7cbd6b12..46881f84 100644 --- a/apps/web/src/inputs/docs/@pluv_react/Custom Events.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/Custom Events.mdx @@ -18,17 +18,17 @@ To get started, define custom events in your backend `@pluv/io` instance (see [D Then, assuming you've already setup your React.js bundle and providers, listen and broadcast your custom events using `useBroadcast` and `useEvent` from your react bundle. ```ts -import { useBroadcast, useEvent } from "frontend/io"; +import { pluvRoom } from "frontend/io"; import { useCallback } from "react"; import { emojiMap } from "./emojiMap"; -useEvent("EMOJI_RECEIVED", ({ data }) => { +pluvRoom.useEvent("EMOJI_RECEIVED", ({ data }) => { const emoji = emojiMap[data.emojiCode]; console.log(emoji); }); -const broadcast = useBroadcast(); +const broadcast = pluvRoom.useBroadcast(); const onEmoji = useCallback((emojiCode: number): void => { broadcast("EMIT_EMOJI", { emojiCode }); diff --git a/apps/web/src/inputs/docs/@pluv_react/History.mdx b/apps/web/src/inputs/docs/@pluv_react/History.mdx index db1ac7eb..f7214e73 100644 --- a/apps/web/src/inputs/docs/@pluv_react/History.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/History.mdx @@ -7,7 +7,10 @@ import { DocsLayout } from "../../../components"; # History -`@pluv/react` comes with hooks to manipulate history that are built on top of Yjs's [UndoManager](https://docs.yjs.dev/api/undo-manager). +`@pluv/react` comes with hooks to manipulate history that are built on top of the CRDT library you are using. + +* For Yjs (`@pluv/crdt-yjs`), it is built on top of the [UndoManager](https://docs.yjs.dev/api/undo-manager). +* For Loro (`@pluv/crdt-loro`), this feature is not yet supported. This will allow users to apply undos and redos to Pluv storage mutations. @@ -24,37 +27,31 @@ This will allow users to apply undos and redos to Pluv storage mutations. Assume you have storage defined like so: ```ts -import { createBundle, createClient, y } from "@pluv/react"; +import { yjs } from "@pluv/crdt-yjs"; +import { createBundle, createClient } from "@pluv/react"; import type { io } from "../server/io"; const client = createClient({ /* ... */ }); export const { createRoomBundle } = createBundle(client); -export const { - useCanRedo, - useCanUndo, - useRedo, - useStorage, - useTransact, - useUndo, -} = createRoomBundle({ - initialStorage: () => ({ - messages: y.array([ - y.object({ +export const pluvRoom = createRoomBundle({ + initialStorage: yjs.doc(() => ({ + messages: yjs.array([ + yjs.object({ message: "hello", name: "leedavidcs", }), ]), - }), + })), }); ``` To undo a storage mutation, you will need to wrap your mutation with a transaction. ```ts -const transact = useTransact(); -const [messages, sharedType] = useStorage(); +const transact = pluvRoom.useTransact(); +const [messages, sharedType] = pluvRoom.useStorage(); // We can undo this transact(() => { @@ -73,58 +70,13 @@ sharedType.push(["world!"]); Then from anywhere within the `PluvRoomProvider`, you can undo your last transacted operation. ```ts -const undo = useUndo(); -const redo = useRedo(); +const undo = pluvRoom.useUndo(); +const redo = pluvRoom.useRedo(); undo(); redo(); ``` -### Custom transaction origins - -By default when `transact` is called, it will use the user's connection id as the transaction's origin. This should cover the majority of use-cases most of the time. However, if you need additional transaction origins, you can specify them with `trackedOrigins` on `createRoomBundle`. - -```ts -export const { /* ... */ } = createRoomBundle({ - initialStorage: () => ({ - messages: y.array([ - y.object({ - message: "hello", - name: "leedavidcs", - }), - ]), - }), - // Additionally track "my-custom-origin" - trackedOrigins: ["my-custom-origin"], -}); - -const transact = useTransact(); - -// We can undo this transaction, because "my-custom-origin" is tracked -transact((tx) => { - tx.messages.push(["world!"]); -}, "my-custom-origin"); -``` - -### Custom capture timeout - -By default, when tracked storage mutations are transacted, Pluv will merge all mutations within 500ms to be undone together. To capture each transaction individually, you can configure this capture timeout to 0ms. - -```ts -export const { /* ... */ } = createRoomBundle({ - initialStorage: () => ({ - messages: y.array([ - y.object({ - message: "hello", - name: "leedavidcs", - }), - ]), - }), - // By default, this is 500ms - captureTimeout: 500, -}); -``` - ## References * [Yjs UndoManager](https://docs.yjs.dev/api/undo-manager) diff --git a/apps/web/src/inputs/docs/@pluv_react/Presence.mdx b/apps/web/src/inputs/docs/@pluv_react/Presence.mdx index 4dcb9372..ce35d087 100644 --- a/apps/web/src/inputs/docs/@pluv_react/Presence.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/Presence.mdx @@ -15,23 +15,16 @@ To get started with presence for pluv.io, first set a `presence` config on your ```ts import { createBundle, createClient } from "@pluv/react"; -import { type AppPluvIO } from "backend/io"; +import type { AppPluvIO } from "backend/io"; import { z } from "zod"; const client = createClient({ wsEndpoint: (room) => `${process.env.WS_ENDPOINT}/api/room/${room}`, }); -const { createRoomBundle } = createBundle(client); +const pluv = createBundle(client); -export const { - PluvRoomProvider, - useMyPresence, - useMyself, - useOther, - useOthers, - useStorage, -} = createRoomBundle({ +export const pluvRoom = pluv.createRoomBundle({ // Define the validation schema for presence, using zod presence: z.object({ selectionId: z.nullable(z.string()), @@ -43,11 +36,11 @@ export const { ```tsx import { FC } from "react"; -import { PluvRoomProvider } from "frontend/io"; +import { pluvRoom } from "frontend/io"; const Room: FC = () => { return ( - { room="my-example-room" > - + ); }; ``` @@ -65,13 +58,13 @@ const Room: FC = () => { ### Current user's presence ```tsx -import { useMyPresence, useMyself } from "frontend/io"; +import { pluvRoom } from "frontend/io"; import { useCallback } from "react"; -const [myPresence, updateMyPresence] = useMyPresence(); +const [myPresence, updateMyPresence] = pluvRoom.useMyPresence(); // ^? const myPresence: { selectionId: string | null } | null -const myself = useMyself(); +const myself = pluvRoom.useMyself(); // ^? const myself: { // connectionId: string; // presence: { selectionId: string | null }; @@ -87,9 +80,9 @@ const selectInput = useCallback((selectionId: string) => { ### Others' presence ```tsx -import { useOther, useOthers } from "frontend/io"; +import { pluvRoom } from "frontend/io"; -const others = useOthers(); +const others = pluvRoom.useOthers(); // ^? const others: readonly { // connectionId: string; // presence: { selectionId: string | null }; @@ -98,7 +91,7 @@ const others = useOthers(); // const connectionId = others[0]?.connectionId!; -const other = useOther(connectionId); +const other = pluvRoom.useOther(connectionId); // ^? const other: { // connectionId: string; // presence: { selectionId: string | null }; diff --git a/apps/web/src/inputs/docs/@pluv_react/Yjs Storage.mdx b/apps/web/src/inputs/docs/@pluv_react/Yjs Storage.mdx index e96c8fcc..ab26267f 100644 --- a/apps/web/src/inputs/docs/@pluv_react/Yjs Storage.mdx +++ b/apps/web/src/inputs/docs/@pluv_react/Yjs Storage.mdx @@ -14,8 +14,9 @@ pluv.io supports conflict-free replicated data-types (CRDT) storage with [yjs](h To get started with yjs for pluv.io, first set an `initialStorage` config on your `createRoomBundle` config. ```ts -import { createBundle, createClient, y } from "@pluv/react"; -import { type AppPluvIO } from "backend/io"; +import { yjs } from "@pluv/crdt-yjs"; +import { createBundle, createClient } from "@pluv/react"; +import type { AppPluvIO } from "backend/io"; const client = createClient({ wsEndpoint: (room) => `${process.env.WS_ENDPOINT}/api/room/${room}`, @@ -23,15 +24,12 @@ const client = createClient({ const { createRoomBundle } = createBundle(client); -export const { - PluvRoomProvider, - useStorage, -} = createRoomBundle({ +export const pluvRoom = createRoomBundle({ // Set the initial storage value, and type here // Don't worry, we can set a different default value in the room component - initialStorage: () => ({ - messages: y.array(["hello world!"]), - }), + initialStorage: yjs.doc(() => ({ + messages: yjs.array(["hello world!"]), + })), }); ``` @@ -40,20 +38,20 @@ export const { Then, setup `PluvRoomProvider` with your new `initialStorage` if it is different than your default set from `createRoomBundle`. ```tsx -import { y } from "@pluv/react"; -import { FC } from "react"; -import { PluvRoomProvider } from "frontend/io"; +import { yjs } from "@pluv/crdt-yjs"; +import type { FC } from "react"; +import { pluvRoom } from "frontend/io"; export const MyPage: FC = () => { return ( - ({ - messages: y.array([]), + messages: yjs.array(), })} > - + ); }; ``` @@ -63,17 +61,17 @@ export const MyPage: FC = () => { We can then use [yjs shared-types](https://docs.yjs.dev/getting-started/working-with-shared-types) to leverage shared CRDTs between connected clients using `useStorage`. ```tsx -import { useStorage } from "frontend/io"; +import { pluvRoom } from "frontend/io"; import { useCallback } from "react"; // "messages" is a key from the root properties of `initialStorage`. -const [messages, sharedType] = useStorage("messages"); +const [messages, sharedType] = pluvRoom.useStorage("messages"); const addMessage = useCallback((message: string) => { - sharedType.push([message]); + sharedType?.push(message); }); -messages.map((message, i) =>
{message}
) +messages?.map((message, i) =>
{message}
) ``` export default ({ children }) => {children}; diff --git a/apps/web/src/inputs/docs/Ecosystem.mdx b/apps/web/src/inputs/docs/Ecosystem.mdx index 8d977c48..27ad4933 100644 --- a/apps/web/src/inputs/docs/Ecosystem.mdx +++ b/apps/web/src/inputs/docs/Ecosystem.mdx @@ -31,6 +31,8 @@ import { type io } from "../server/io"; const client = createClient(); const room = client.createRoom("my-room", { + // ... other configs here, + // ... addons: [addonIndexedDB({ enabled: true })], // Alternatively addons: [addonIndexedDB({ enabled: (room) => room.id === "my-room" })], @@ -46,6 +48,8 @@ const client = createClient(); const { createRoomBundle } = createBundle(client); const { useRoom } = createRoomBundle({ + // ... other configs here, + // ... addons: [addonIndexedDB({ enabled: true })], // Alternatively addons: [addonIndexedDB({ enabled: (room) => room.id === "my-room" })], diff --git a/packages/cli/README.md b/packages/cli/README.md index a2123078..6e4e11f9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -5,24 +5,24 @@
Pluv.IO
- Pluv.IO (preview) +
Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

- - npm @pluv/io + + npm @pluv/cli GitHub - TypeScript Commitizen friendly + TypeScript

> **Note** diff --git a/packages/client/README.md b/packages/client/README.md index 10162aab..317073b4 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/client` @@ -39,13 +35,13 @@ ```bash # npm -npm install @pluv/client yjs +npm install @pluv/client @pluv/crdt-yjs yjs # yarn -yarn add @pluv/client yjs +yarn add @pluv/client @pluv/crdt-yjs yjs # pnpm -pnpm add @pluv/client yjs +pnpm add @pluv/client @pluv/crdt-yjs yjs ``` If you plan on using presence and/or storage, make sure to also install `zod`. @@ -53,7 +49,8 @@ If you plan on using presence and/or storage, make sure to also install `zod`. ## Basic Example ```ts -import { createClient, y } from "@pluv/client"; +import { createClient } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; import { z } from "zod"; // Import the PluvIO instance as a type from your server file import { type io } from "./server"; @@ -79,7 +76,7 @@ interface Storage { // Create a room to join const room = client.createRoom("my-room", { initialPresence: { cursor: null }, - initialStorage: () => ({ messages: y.array([]) }), + initialStorage: yjs.doc(() => ({ messages: yjs.array([]) })), onAuthorizationFail: (error) => { console.log(error.message); }, diff --git a/packages/crdt-yjs/README.md b/packages/crdt-yjs/README.md index de0242a9..5590d05a 100644 --- a/packages/crdt-yjs/README.md +++ b/packages/crdt-yjs/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/crdt-yjs` @@ -54,43 +50,31 @@ pnpm add @pluv/crdt-yjs yjs ## Basic Example ```ts -import { createClient, y } from "@pluv/client"; -import type { - Array as YArray, - Map as YMap, - XmlFragment as YXmlFragment, -} from "yjs"; +import { createClient } from "@pluv/client"; +import { yjs } from "@pluv/crdt-yjs"; import { z } from "zod"; // Import the PluvIO instance as a type from your server file import { type io } from "./server"; const client = createClient({ /* client config here */ }); -type Presence = {}; - -interface Storage { - editor: YXmlFragment; - groceryList: YMap; - messages: YArray<{ name: string; message: string }>; -} - // Create a room to join -const room = client.createRoom("my-room", { - initialStorage: () => ({ - editor: y.xmlFragment({ +const room = client.createRoom("my-room", { + initialStorage: yjs.doc(() => ({ + editor: yjs.xmlFragment({ children: [ - y.xmlElement("paragraph", { - children: [y.xmlText("Hello World!")], + yjs.xmlElement("paragraph", { + children: [yjs.xmlText("Hello World!")], }), ] }), - groceryList: y.map([ + groceryList: yjs.map([ ["apricots", 2], ["bread", 3], ["cheese", 5], ]), - messages: y.array([]), - }), + messages: yjs.array([]), + })), presence: z.object({}), }); ``` diff --git a/packages/io/README.md b/packages/io/README.md index 44988d08..5a005c5d 100644 --- a/packages/io/README.md +++ b/packages/io/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -22,13 +22,7 @@ Commitizen friendly -

- -

TypeScript - - Featured on Openbase -

@@ -39,32 +33,12 @@ License

-

- Demo -

- ## Intro -Pluv.IO allows you to build real-time collaborative features with a fully end-to-end type-safe api. +pluv.io allows you to more easily build real-time collaborative experiences with a fully end-to-end type-safe api and the ecosystem of existing CRDT implementations such as **[yjs](https://docs.yjs.dev/)**. **👉 See full documentation on [pluv.io](https://pluv.io/docs/introduction). 👈** -### Why? - -So you can do this: - -```tsx -const broadcast = useBroadcast(); - -useEvent("RECEIVE_MESSAGE", ({ data }) => { - setMessages([...messages, data.message]); -}); - -broadcast("SEND_MESSAGE", { message: "Hello world!" }); -``` - -And more. With **E2E type-safety**, **great intellisense** and the **[yjs](https://docs.yjs.dev/) ecosystem**. - ### Features - ✅ Automatic type safety @@ -72,14 +46,21 @@ And more. With **E2E type-safety**, **great intellisense** and the **[yjs](https - ✅ Rooms - ✅ Authentication - ✅ Awareness + Presence -- ✅ [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) (with [Yjs](https://docs.yjs.dev/)) - - ✅ **Shared Types** - - ✅ [Map](https://docs.yjs.dev/api/shared-types/y.map) - - ✅ [Array](https://docs.yjs.dev/api/shared-types/y.array) - - ✅ [Text](https://docs.yjs.dev/api/shared-types/y.text) - - ✅ [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment) - - ✅ [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement) - - ✅ [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext) +- ✅ [CRDTs](https://en.wikipedia.org/wiki/Conflict-free_replicated_data_type) + - ✅ [Yjs](https://docs.yjs.dev/) + - ✅ **Shared Types** + - ✅ [Map](https://docs.yjs.dev/api/shared-types/y.map) + - ✅ [Array](https://docs.yjs.dev/api/shared-types/y.array) + - ✅ [Text](https://docs.yjs.dev/api/shared-types/y.text) + - ✅ [XmlFragment](https://docs.yjs.dev/api/shared-types/y.xmlfragment) + - ✅ [XmlElement](https://docs.yjs.dev/api/shared-types/y.xmlelement) + - ✅ [XmlText](https://docs.yjs.dev/api/shared-types/y.xmltext) + - ✅ [Loro](https://loro.dev/) + - ✅ **Containers** + - ✅ List + - ✅ Map + - ✅ Text + - ⬜ Tree - ⬜ Studio (admin & developer panel) ### Runtimes @@ -96,7 +77,8 @@ And more. With **E2E type-safety**, **great intellisense** and the **[yjs](https ### Frontends -- ✅ [React.js](https://beta.reactjs.org/) +- ✅ [React.js](https://react.dev/) +- ⬜ [Solid.js](https://www.solidjs.com/) - ⬜ [Vue.js](https://vuejs.org/) - ⬜ [Svelte](https://svelte.dev/) @@ -114,7 +96,8 @@ Documentation is available at [pluv.io](https://pluv.io/docs/introduction). ## Related - [@pluv/client](https://www.npmjs.com/package/@pluv/client) - Framework agnostic client -- [@pluv/crdt-yjs](https://www.npmjs.com/package/@pluv/crdt-yjs) - Yjs for Pluv.IO +- [@pluv/crdt-loro](https://www.npmjs.com/package/@pluv/crdt-loro) - Loro CRDT for Pluv.IO +- [@pluv/crdt-yjs](https://www.npmjs.com/package/@pluv/crdt-yjs) - Yjs CRDT for Pluv.IO - [@pluv/persistance-redis](https://www.npmjs.com/package/@pluv/persistance-redis) - Persistance for storage on distributed systems (Node.js only) - [@pluv/platform-cloudflare](https://www.npmjs.com/package/@pluv/platform-cloudflare) - Adapter to run @pluv/io on Cloudflare Workers - [@pluv/platform-node](https://www.npmjs.com/package/@pluv/platform-node) - Adapter to run @pluv/io on Node.js @@ -125,6 +108,7 @@ Documentation is available at [pluv.io](https://pluv.io/docs/introduction). This software uses the following open source tooling and libraries: +- [Loro](https://loro.dev/) - [Yjs](https://yjs.dev/) - [Node.js](https://nodejs.org/) - [Cloudflare Workers](https://workers.cloudflare.com/) diff --git a/packages/platform-cloudflare/README.md b/packages/platform-cloudflare/README.md index 36dd49b2..3531db3f 100644 --- a/packages/platform-cloudflare/README.md +++ b/packages/platform-cloudflare/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/platform-cloudflare` diff --git a/packages/platform-node/README.md b/packages/platform-node/README.md index 4f1d195a..49fbc134 100644 --- a/packages/platform-node/README.md +++ b/packages/platform-node/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/platform-node` diff --git a/packages/pubsub-redis/README.md b/packages/pubsub-redis/README.md index 77534dd6..963f5aad 100644 --- a/packages/pubsub-redis/README.md +++ b/packages/pubsub-redis/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/pubsub-redis` diff --git a/packages/react/README.md b/packages/react/README.md index 28d69819..ba082347 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -5,12 +5,12 @@
Pluv.IO
- Pluv.IO (preview) + Pluv.IO (preview)

Multi-platform, E2E type-safe realtime packages

-

💕 Inspired by trpc 💕 Built with yjs 💕

+

💕 Inspired by trpc 💕 yjs 💕 and Cloudflare 💕

@@ -19,14 +19,10 @@ GitHub - TypeScript Commitizen friendly -

- -

- Demo + TypeScript

## `@pluv/react` @@ -39,21 +35,20 @@ ```bash # npm -npm install @pluv/react yjs +npm install @pluv/react @pluv/crdt-yjs yjs zod # yarn -yarn add @pluv/react yjs +yarn add @pluv/react @pluv/crdt-yjs yjs zod # pnpm -pnpm add @pluv/react yjs +pnpm add @pluv/react @pluv/crdt-yjs yjs zod ``` -If you plan on using presence and/or storage, make sure to also install `zod`. - ## Basic Example ```tsx -import { createBundle, createClient, y } from "@pluv/react"; +import { yjs } from "@pluv/crdt-yjs"; +import { createBundle, createClient } from "@pluv/react"; import { z } from "zod"; // Import the PluvIO instance as a type from your server file import { type io } from "./server"; @@ -94,14 +89,14 @@ export const { useRoom, useStorage, } = createRoomBundle({ - initialStorage: () => ({ - messages: y.array([ - y.object({ + initialStorage: yjs.doc(() => ({ + messages: yjs.array([ + yjs.object({ message: "Hello World!", name: "johnathan_doe", }), ]), - }), + })), presence: z.object({ cursor: z.nullable( z.object({