├── .gitignore ├── .prettierrc.json ├── README.md ├── demo ├── index.html ├── package.json ├── src │ ├── App.css │ ├── App.tsx │ ├── favicon.svg │ ├── index.css │ ├── logo.svg │ ├── main.tsx │ ├── pages │ │ ├── Demo.tsx │ │ └── Minimal.tsx │ ├── store.ts │ ├── types.ts │ ├── utils.ts │ └── vite-env.d.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock ├── package.json ├── src ├── hooks.ts └── index.ts ├── tsconfig.json ├── vite.config.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | .env 7 | 8 | .vercel 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "arrowParens": "always", 5 | "useTabs": false, 6 | "semi": true, 7 | "singleQuote": false, 8 | "trailingComma": "es5" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jotai-yjs 💊🚀 2 | 3 | jotai-yjs makes yjs state [even easier](https://github.com/dai-shi/valtio-yjs) 4 | 5 | 6 | https://user-images.githubusercontent.com/47224540/127463982-3332da94-4bf6-46c5-9a2e-f67d54ac7cb3.mp4 7 | 8 | 9 | 10 | ## What is this 11 | 12 | [valtio](https://github.com/pmndrs/valtio) is 13 | a proxy state library for ReactJS and VanillaJS. 14 | [yjs](https://github.com/yjs/yjs) is 15 | an implmentation of CRDT algorithm. [jotai](https://github.com/pmndrs/jotai) is a primitive and flexible state management for React. 16 | 17 | jotai-yjs is a three-way binding to bridge them. Typescript ready btw. 18 | 19 | ## Project status 20 | 21 | It started as an experiment, and the experiment is finished. 22 | Now, it's in alpha. 23 | 24 | ## Install 25 | 26 | ```bash 27 | yarn add react jotai-yjs jotai valtio valtio-yjs yjs 28 | ``` 29 | 30 | ## How to use it 31 | 32 | ```ts 33 | import * as Y from "yjs"; 34 | import { useYArray, useYMap } from "jotai-yjs"; 35 | 36 | // create a new Y doc 37 | const ydoc = new Y.Doc(); 38 | 39 | // useYArray & useYMap returns the proxy source so that you can mutate it directly thanks to valtio-yjs 40 | const settings = useYMap(yDoc, "settings"); 41 | 42 | // here we're creating a proxy array (thanks to valtio-yjs) available globally (thanks to jotai) through its name, "games", attached to the yDoc we created earlier 43 | const gamesSource = useYArray(yDoc, "games"); 44 | 45 | // and here you can get the snapshot to read from it, as per the valtio docs https://github.com/pmndrs/valtio#react-via-usesnapshot 46 | const games = useSnapshot(gamesSource); 47 | 48 | // you can do anything you could do with valtio here with those 49 | ``` 50 | 51 | # Adding a provider 52 | 53 | > Currently, I only tried it with a WebsocketProvider but as long as the provider as an `awareness` property it should work the same. 54 | 55 | ```ts 56 | // [optional] create a provider 57 | const provider = new WebsocketProvider(wsUrl, yDoc.guid, yDoc, { connect: false }); 58 | 59 | // here we're creating an init function that should be called only once in the hook below 60 | const addProviderToDoc = () => { 61 | console.log("connect to a ws provider with room", yDoc.guid); 62 | 63 | provider.connect(); 64 | // do what/ever you want with that provider here 65 | 66 | return () => { 67 | console.log("disconnect", yDoc.guid); 68 | provider.destroy(); 69 | }; 70 | }; 71 | 72 | // very basic hook which purpose is to connect the provider to the server 73 | export const useProviderInit = () => { 74 | useEffect(() => { 75 | const unmount = addProviderToDoc(); 76 | return () => unmount(); 77 | }, []); 78 | 79 | return yDoc; 80 | }; 81 | ``` 82 | 83 | # Presence 84 | 85 | Using hooks, you get access to what I call your `presence`, which is the current user local awareness state in YJS terms. 86 | It is used like a `useState`, as simple as : 87 | 88 | ```ts 89 | const [presence, setPresence] = usePresence(); 90 | ``` 91 | 92 | But that `usePresence` hook doesn't come straight from `jotai-yjs`, it comes from you ! 93 | 94 | You can create it using the `makePresence` function and passing your custom arguments. 95 | 96 | ```ts 97 | import { makePresence } from "jotai-yjs"; 98 | 99 | // provider could be the WebsocketProvider that we created earlier 100 | // initialPresence is the object to use as the initial local awareness state, aka presence 101 | // onUpdate is an optional function that is called whenever the presence is updated and takes the current presence as argument, 102 | 103 | // here, persistPlayer could be an action like persisting the presence to localStorage 104 | export const { useYAwarenessInit, useYAwareness, presenceProxy, usePresence, usePresenceSnap } = makePresence({ 105 | provider, 106 | initialPresence: player, 107 | onUpdate: persistPlayer, 108 | }); 109 | ``` 110 | 111 | Inspired by [`zustand`](https://github.com/pmndrs/zustand) hook store. 112 | 113 | Quite a lot coming from that `makePresence` return ! Let's see what comes from it: 114 | 115 | - `useYAwarenessInit`: hook to init the provider, basically it syncs an atom with each updates from the [`awareness`](https://docs.yjs.dev/getting-started/adding-awareness) service 116 | - `useYAwareness`: returns the resulting `awareness` state set by`useYAwarenessInit` 117 | - `presenceProxy`: the actual proxy source created by `valtio` 118 | - `usePresence`: we talked about that one already [here](#presence) 119 | - `usePresenceSnap`: just a shortcut, it's basically `const usePresenceSnap = () => useSnapshot(presenceProxy);` 120 | 121 | And since all of these hooks were created by **YOU**, you don't need to pass any arguments ! You did that already when you used `makePresence` so everything will be deduced from here. 122 | 123 | ## Demos 124 | 125 | > you can check the [demo app in the repository](./demo/src/pages/Demo.tsx) made with [this template](https://github.com/astahmer/vite-chakra) 126 | 127 | Using `usePresence` and 128 | `WebsocketProvider` in [y-websocket](https://github.com/yjs/y-websocket), 129 | we can create multi-client React apps pretty easily. 130 | 131 | - https://codesandbox.io/s/github/astahmer/jotai-yjs/tree/main/demo?file=/src/pages/Demo.tsx 132 | - (...open a PR to add your demos) 133 | 134 | # Notes 135 | 136 | Huge thanks to [Daishi Kato](https://twitter.com/dai_shi) for everything he does. 137 | I copy/pasted valtio-yjs README and changed a few things to make this one so this might look familiar. 138 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jotai-yjs-demo", 3 | "version": "0.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/astahmer/jotai-yjs.git" 7 | }, 8 | "author": "Alexandre Stahmer ", 9 | "scripts": { 10 | "dev": "vite", 11 | "build": "tsc && vite build", 12 | "serve": "vite preview" 13 | }, 14 | "dependencies": { 15 | "@chakra-ui/react": "^1.6.5", 16 | "@emotion/react": "^11", 17 | "@emotion/styled": "^11", 18 | "@pastable/core": "^0.1.7", 19 | "@xstate/react": "^1.5.1", 20 | "axios": "^0.21.1", 21 | "framer-motion": "^4", 22 | "jotai": "^1.2.2", 23 | "jotai-yjs": "^0.0.0", 24 | "nanoid": "^3.1.23", 25 | "react": "^17.0.0", 26 | "react-dom": "^17.0.0", 27 | "react-hook-form": "^7.11.1", 28 | "react-query": "^3.19.0", 29 | "react-router-dom": "^5.2.0", 30 | "valtio": "^1.1.0", 31 | "valtio-yjs": "^0.1.0", 32 | "xstate": "^4.23.0", 33 | "y-websocket": "^1.3.16", 34 | "yjs": "^13.5.11" 35 | }, 36 | "devDependencies": { 37 | "@types/react": "^17.0.0", 38 | "@types/react-dom": "^17.0.0", 39 | "@types/react-router-dom": "^5.1.8", 40 | "@vitejs/plugin-react-refresh": "^1.3.1", 41 | "rollup-plugin-peer-deps-external": "^2.2.4", 42 | "typescript": "^4.3.2", 43 | "vite": "^2.4.3", 44 | "vite-plugin-pwa": "^0.8.2", 45 | "vite-react-jsx": "^1.1.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /demo/src/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /demo/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Center, ChakraProvider, extendTheme } from "@chakra-ui/react"; 2 | import "./App.css"; 3 | import { Demo } from "./pages/Demo"; 4 | import { BrowserRouter, Route, Switch } from "react-router-dom"; 5 | import { Minimal } from "./pages/Minimal"; 6 | 7 | const theme = extendTheme({ config: { initialColorMode: "light" } }); 8 | 9 | function App() { 10 | return ( 11 | 12 | 13 |
14 | 15 | } /> 16 | } /> 17 | 18 |
19 |
20 |
21 | ); 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /demo/src/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /demo/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /demo/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /demo/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ) 12 | -------------------------------------------------------------------------------- /demo/src/pages/Demo.tsx: -------------------------------------------------------------------------------- 1 | import { yDoc, useProviderInit, usePresence, useYAwarenessInit, useYAwareness } from "@/store"; 2 | import { Game, Player } from "@/types"; 3 | import { getRandomColor, makeGame, getSaturedColor, throttle } from "@/utils"; 4 | import { useYArray } from "jotai-yjs"; 5 | import { 6 | Box, 7 | Button, 8 | Center, 9 | chakra, 10 | Circle, 11 | CloseButton, 12 | Editable, 13 | EditableInput, 14 | EditablePreview, 15 | EditableProps, 16 | Flex, 17 | SimpleGrid, 18 | Spinner, 19 | Stack, 20 | } from "@chakra-ui/react"; 21 | import { removeItemMutate } from "@pastable/core"; 22 | import { useSnapshot } from "valtio"; 23 | 24 | export const Demo = () => { 25 | useProviderInit(); 26 | useYAwarenessInit(); 27 | 28 | const gamesSource = useYArray(yDoc, "games"); 29 | const games = useSnapshot(gamesSource); 30 | 31 | const [presence, setPresence] = usePresence(); 32 | 33 | const makeNewGame = () => gamesSource.push(makeGame(presence)); 34 | const updateName = (username: Player["username"]) => setPresence((player) => ({ ...player, username })); 35 | const updateColor = (color: Player["color"]) => setPresence((player) => ({ ...player, color })); 36 | const handleUpdateColor = throttle((e) => updateColor(e.target.value), 1000); 37 | const updateRandomColor = () => setPresence((player) => ({ ...player, color: getRandomColor() })); 38 | 39 | if (!presence) { 40 | return ( 41 |
42 | 43 |
44 | ); 45 | } 46 | 47 | return ( 48 | 49 |
50 | 51 | 52 | (Editable) Username: 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | 61 | {games.map((game: Game, gameIndex) => { 62 | const gameSrc = gamesSource[gameIndex]; 63 | const gameId = game.id; 64 | 65 | return ; 66 | })} 67 | 68 | 69 |
70 | ); 71 | }; 72 | 73 | const PlayerList = () => { 74 | const awareness = useYAwareness(); 75 | const players = Array.from(awareness.entries()).filter(([_id, player]) => player.id); 76 | 77 | return ( 78 | 79 | 80 | {players.map(([id, presence]) => ( 81 | 82 | 90 | {presence.username} 91 | 92 | ))} 93 | 94 | 95 | ); 96 | }; 97 | 98 | const DuelGameWidget = ({ game }) => { 99 | const gameSnap = useSnapshot(game); 100 | const [hostPlayer, opponentPlayer] = gameSnap.players || []; 101 | 102 | const gamesSource = useYArray(yDoc, "games"); 103 | const deleteGame = () => removeItemMutate(gamesSource, "id", game.id); 104 | const [presence] = usePresence(); 105 | const joinGame = () => game.players.push(presence); 106 | const isHost = presence.id === hostPlayer.id; 107 | 108 | return ( 109 | 110 | {} 111 | 112 | 113 | 114 |
115 | 116 |
117 | 118 | {opponentPlayer ? ( 119 | 120 | ) : isHost ? ( 121 | 122 | ) : ( 123 | 124 | )} 125 | 126 |
127 | ); 128 | }; 129 | 130 | const PlayerSlot = ({ children }) => ( 131 | 132 | {children} 133 | 134 | ); 135 | 136 | const PlayerSlotContent = ({ player }: { player: Player }) => { 137 | return ( 138 | 139 | 140 | 141 | {player.username} 142 | 143 | 144 | {player.elo} ELO 145 | 146 | 147 | ); 148 | }; 149 | 150 | const PlayerSlotJoinGame = ({ onJoin }) => { 151 | return ( 152 |
153 | 156 |
157 | ); 158 | }; 159 | 160 | const PlayerSlotWaitingForOpponent = () => { 161 | return ( 162 |
163 | 167 |
168 | ); 169 | }; 170 | 171 | const VsCircle = () => ( 172 | 173 | 174 | VS 175 | 176 | 177 | ); 178 | 179 | const EditableName = (props: EditableProps) => { 180 | return ( 181 | 182 | 183 | 184 | 185 | ); 186 | }; 187 | -------------------------------------------------------------------------------- /demo/src/pages/Minimal.tsx: -------------------------------------------------------------------------------- 1 | import { useProviderInit } from "@/store"; 2 | import { Game } from "@/types"; 3 | import { makeGame, makePlayer } from "@/utils"; 4 | import { useYArray } from "jotai-yjs"; 5 | import { Button, Center, chakra, SimpleGrid, Stack } from "@chakra-ui/react"; 6 | import { useSnapshot } from "valtio"; 7 | 8 | export const Minimal = () => { 9 | const yDoc = useProviderInit(); 10 | const gamesSource = useYArray(yDoc, "games.test"); 11 | const games = useSnapshot(gamesSource); 12 | const pushTo = () => gamesSource.push(makeGame(makePlayer())); 13 | 14 | return ( 15 | 16 |
17 | 18 |
19 | 20 | {games.map((game, gameIndex) => { 21 | return ( 22 |
23 | 24 | 25 | Game: {game.id} ({game.mode}) 26 | 27 | 28 | {game.players.map((player) => ( 29 | 30 | [{player.username} : {player.elo} ELO 31 | 32 | ))} 33 | 34 | 37 | 38 |
39 | ); 40 | })} 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /demo/src/store.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from "@pastable/core"; 2 | import { makePresence } from "jotai-yjs"; 3 | import { useEffect } from "react"; 4 | import { WebsocketProvider } from "y-websocket"; 5 | import * as Y from "yjs"; 6 | import { Player } from "./types"; 7 | import { makePlayer } from "./utils"; 8 | 9 | const yDocId = "jotai-yjs"; 10 | const wsUrl = "wss://y.svelt-yjs.dev"; 11 | 12 | const yDocOptions = { guid: yDocId }; 13 | export const yDoc = new Y.Doc(yDocOptions); 14 | 15 | const provider = new WebsocketProvider(wsUrl, yDoc.guid, yDoc, { 16 | connect: false, 17 | }); 18 | 19 | const getPlayer = (): Player => { 20 | const player = localStorage.getItem(yDocId + "/player"); 21 | return player ? JSON.parse(player) : makePlayer(); 22 | }; 23 | const player = getPlayer(); 24 | const persistPlayer = (player: Player) => localStorage.setItem(yDocId + "/player", stringify(player)); 25 | 26 | const addProviderToDoc = () => { 27 | console.log("connect to a ws provider with room", yDoc.guid); 28 | 29 | provider.connect(); 30 | provider.awareness.setLocalState(player); 31 | persistPlayer(player); 32 | 33 | return () => { 34 | console.log("disconnect", yDoc.guid); 35 | provider.destroy(); 36 | }; 37 | }; 38 | 39 | export const useProviderInit = () => { 40 | useEffect(() => { 41 | const unmount = addProviderToDoc(); 42 | return () => unmount(); 43 | }, []); 44 | 45 | return yDoc; 46 | }; 47 | 48 | export const { useYAwarenessInit, useYAwareness, presenceProxy, usePresence, usePresenceSnap } = makePresence({ 49 | provider, 50 | initialPresence: player, 51 | onUpdate: persistPlayer, 52 | }); 53 | -------------------------------------------------------------------------------- /demo/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Player { 2 | id: string; 3 | username: string; 4 | elo: number; 5 | color: string; 6 | } 7 | export interface Game { 8 | id: string; 9 | players: Array; 10 | mode: "duel" | "free-for-all"; 11 | } 12 | -------------------------------------------------------------------------------- /demo/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { getNextIndex, getNextItem, getRandomIntIn, pickOne } from "@pastable/core"; 2 | import { nanoid } from "nanoid"; 3 | import { Game, Player } from "./types"; 4 | 5 | export const makeId = () => nanoid(12); 6 | export const makeUsername = () => nanoid(getRandomIntIn(4, 10)); 7 | export const makePlayer = (): Player => ({ 8 | id: makeId(), 9 | username: makeUsername(), 10 | elo: getRandomIntIn(0, 2200), 11 | color: getRandomColor(), 12 | }); 13 | export const makeGame = (initialPlayer: Player): Game => ({ id: makeId(), players: [initialPlayer], mode: "duel" }); 14 | 15 | const hexLetters = "0123456789ABCDEF".toLowerCase(); 16 | const hexLettersArray = hexLetters.split(""); 17 | 18 | export const getRandomColor = () => 19 | rainbow(getRandomIntIn(1000) % 999) + pickOne(hexLettersArray.slice(2, 6)) + pickOne(hexLettersArray.slice()); 20 | 21 | const getNextHexChar = (char: string, step = 3) => 22 | hexLettersArray[getNextIndex(hexLetters.indexOf(char), hexLettersArray.length, false, step)]; 23 | export const getSaturedColor = (hexColor: string) => { 24 | const chars = hexColor.split(""); 25 | chars[5] = getNextHexChar(chars[5]); 26 | chars[6] = getNextHexChar(chars[6]); 27 | chars[7] = getNextHexChar(chars[7], 2); 28 | chars[8] = getNextHexChar(chars[8], 2); 29 | 30 | return chars.join(""); 31 | }; 32 | 33 | export function rainbow(step: number, numOfSteps = 1000) { 34 | // This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distinguishable vibrant markers in Google Maps and other apps. 35 | // Adam Cole, 2011-Sept-14 36 | // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript 37 | let r = 0, 38 | g = 0, 39 | b = 0; 40 | let h = step / numOfSteps; 41 | let i = ~~(h * 6); 42 | let f = h * 6 - i; 43 | let q = 1 - f; 44 | switch (i % 6) { 45 | case 0: 46 | r = 1; 47 | g = f; 48 | b = 0; 49 | break; 50 | case 1: 51 | r = q; 52 | g = 1; 53 | b = 0; 54 | break; 55 | case 2: 56 | r = 0; 57 | g = 1; 58 | b = f; 59 | break; 60 | case 3: 61 | r = 0; 62 | g = q; 63 | b = 1; 64 | break; 65 | case 4: 66 | r = f; 67 | g = 0; 68 | b = 1; 69 | break; 70 | case 5: 71 | r = 1; 72 | g = 0; 73 | b = q; 74 | break; 75 | } 76 | var c = 77 | "#" + 78 | ("00" + (~~(r * 255)).toString(16)).slice(-2) + 79 | ("00" + (~~(g * 255)).toString(16)).slice(-2) + 80 | ("00" + (~~(b * 255)).toString(16)).slice(-2); 81 | return c; 82 | } 83 | 84 | // https://gist.github.com/beaucharman/e46b8e4d03ef30480d7f4db5a78498ca 85 | export function throttle(callback, wait, immediate = false) { 86 | let timeout = null; 87 | let initialCall = true; 88 | 89 | return function () { 90 | const callNow = immediate && initialCall; 91 | const next = () => { 92 | callback.apply(this, arguments); 93 | timeout = null; 94 | }; 95 | 96 | if (callNow) { 97 | initialCall = false; 98 | next(); 99 | } 100 | 101 | if (!timeout) { 102 | timeout = setTimeout(next, wait); 103 | } 104 | }; 105 | } 106 | -------------------------------------------------------------------------------- /demo/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "baseUrl": "src", 6 | "paths": { 7 | "@/*": ["./*"], 8 | "jotai-yjs": ["../../src"] 9 | } 10 | }, 11 | "include": ["./src"] 12 | } 13 | -------------------------------------------------------------------------------- /demo/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import reactJsx from "vite-react-jsx"; 3 | 4 | import reactRefresh from "@vitejs/plugin-react-refresh"; 5 | import { VitePWA } from "vite-plugin-pwa"; 6 | import path from "path"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | base: "/", 11 | root: "./", 12 | plugins: [reactRefresh(), reactJsx(), VitePWA()], 13 | resolve: { 14 | alias: [ 15 | { find: "@", replacement: "/src" }, 16 | // { find: "jotai-yjs", replacement: path.join(process.cwd(), "../", "./src/index.ts") }, 17 | // { find: "jotai-yjs", replacement: path.join(process.cwd(), "../", "./dist/jotai-yjs.es.js") }, 18 | ], 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jotai-yjs", 3 | "description": "jotai-yjs makes yjs state even easier", 4 | "version": "0.0.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/astahmer/jotai-yjs.git" 8 | }, 9 | "author": "Alexandre Stahmer ", 10 | "files": [ 11 | "src", 12 | "dist" 13 | ], 14 | "source": "src/index.ts", 15 | "typings": "src/index.ts", 16 | "main": "./dist/jotai-yjs.umd.js", 17 | "module": "./dist/jotai-yjs.es.js", 18 | "exports": { 19 | ".": { 20 | "import": "./dist/jotai-yjs.es.js", 21 | "require": "./dist/jotai-yjs.umd.js" 22 | } 23 | }, 24 | "scripts": { 25 | "dev": "vite", 26 | "build": "tsc && vite build", 27 | "watch": "vite build --watch", 28 | "serve": "vite preview" 29 | }, 30 | "keywords": [ 31 | "react", 32 | "jotai", 33 | "valtio", 34 | "yjs", 35 | "crdt" 36 | ], 37 | "dependencies": { 38 | "@pastable/core": "^0.1.8", 39 | "jotai": "^1.2.2", 40 | "react": "^17.0.2", 41 | "valtio": "^1.1.0", 42 | "valtio-yjs": "^0.1.0", 43 | "y-websocket": "^1.3.16", 44 | "yjs": "^13.5.11" 45 | }, 46 | "peerDependencies": { 47 | "jotai": "*", 48 | "react": ">=16.8", 49 | "valtio": "*", 50 | "valtio-yjs": "*", 51 | "y-websocket": "*", 52 | "yjs": "*" 53 | }, 54 | "devDependencies": { 55 | "@types/react": "^17.0.15", 56 | "@types/rollup-plugin-peer-deps-external": "^2.2.1", 57 | "prettier": "^2.3.2", 58 | "rollup-plugin-peer-deps-external": "^2.2.4", 59 | "typescript": "^4.3.2", 60 | "vite": "^2.4.3" 61 | }, 62 | "publishConfig": { 63 | "access": "public" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ObjectLiteral, SetState } from "@pastable/core"; 2 | import { atom, useAtom } from "jotai"; 3 | import { atomFamily, useAtomValue, useUpdateAtom } from "jotai/utils"; 4 | import { atomWithProxy } from "jotai/valtio"; 5 | import { useEffect } from "react"; 6 | import { proxy, useSnapshot } from "valtio"; 7 | import { bindProxyAndYArray, bindProxyAndYMap } from "valtio-yjs"; 8 | import { WebsocketProvider } from "y-websocket"; 9 | import * as Y from "yjs"; 10 | 11 | const yAwarenessAtomFamily = atomFamily( 12 | (provider: WebsocketProvider) => atom(getStatesMap(provider)), 13 | (a, b) => a.doc.guid === b.doc.guid 14 | ); 15 | 16 | // type StatesMap = ReturnType; 17 | const getStatesMap = (provider: WebsocketProvider) => new Map(provider.awareness.getStates()); 18 | 19 | const yPresenceAtomFamily = atomFamily( 20 | ({ presenceProxy }: { provider: WebsocketProvider; presenceProxy: any }) => atomWithProxy(presenceProxy), 21 | (a, b) => a.provider.doc.guid === b.provider.doc.guid 22 | ); 23 | 24 | export const makePresence = ({ 25 | provider, 26 | initialPresence, 27 | onUpdate, 28 | }: { 29 | provider: WebsocketProvider; 30 | initialPresence: Value; 31 | onUpdate?: (presence: Value) => void; 32 | }) => { 33 | const useYAwarenessInit = () => { 34 | const setAwareness = useUpdateAtom(yAwarenessAtomFamily(provider)); 35 | 36 | useEffect(() => { 37 | const update = () => setAwareness(getStatesMap(provider)); 38 | 39 | update(); 40 | provider.awareness.on("update", update); 41 | 42 | return () => provider.awareness.destroy(); 43 | }, []); 44 | }; 45 | const useYAwareness = () => useAtomValue(yAwarenessAtomFamily(provider)) as Map; 46 | 47 | const presenceProxy = proxy(initialPresence); 48 | const usePresence = () => { 49 | const [presence, setPresence] = useAtom(yPresenceAtomFamily({ provider, presenceProxy })); 50 | const setAwareness: SetState = (state) => { 51 | const update = typeof state === "function" ? state(presence) : state; 52 | provider.awareness.setLocalState(update); 53 | setPresence(update); 54 | onUpdate?.(update); 55 | }; 56 | 57 | return [presenceProxy, setAwareness] as const; 58 | }; 59 | const usePresenceSnap = () => useSnapshot(presenceProxy); 60 | 61 | return { useYAwarenessInit, useYAwareness, presenceProxy, usePresence, usePresenceSnap }; 62 | }; 63 | 64 | const yArrayAtomFamily = atomFamily( 65 | ({ defaultValue }: { name: string; defaultValue: any[] }) => atom(proxy(defaultValue)), 66 | (a, b) => a.name === b.name 67 | ); 68 | export function useYArray(yDoc: Y.Doc, name: string): Array { 69 | const yArray = yDoc.getArray(name); 70 | const defaultValue = yArray.toArray() as Array; 71 | const source = useAtomValue(yArrayAtomFamily({ name, defaultValue })); 72 | 73 | useEffect(() => { 74 | bindProxyAndYArray(source, yArray); 75 | }, []); 76 | 77 | return source; 78 | } 79 | 80 | const yMapAtomFamily = atomFamily( 81 | ({ defaultValue }: { name: string; defaultValue: ObjectLiteral }) => atom(proxy(defaultValue)), 82 | (a, b) => a.name === b.name 83 | ); 84 | export function useYMap(yDoc: Y.Doc, name: string): T { 85 | const yMap = yDoc.getMap(name) as Y.Map; 86 | const defaultValue = yMap.toJSON() as T; 87 | const source = useAtomValue(yMapAtomFamily({ name, defaultValue })); 88 | 89 | useEffect(() => { 90 | bindProxyAndYMap(source, yMap); 91 | }, []); 92 | 93 | return source as T; 94 | } 95 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./hooks"; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "allowJs": false, 6 | "skipLibCheck": false, 7 | "esModuleInterop": false, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": false, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "ESNext", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import path from "path"; 3 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: "/", 8 | root: "./", 9 | plugins: [peerDepsExternal()], 10 | build: { 11 | sourcemap: true, 12 | lib: { 13 | entry: path.resolve(__dirname, "src/index.ts"), 14 | name: "jotai-yjs", 15 | fileName: (format) => `jotai-yjs.${format}.js`, 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@pastable/core@^0.1.8": 6 | version "0.1.8" 7 | resolved "https://registry.npmjs.org/@pastable/core/-/core-0.1.8.tgz" 8 | integrity sha512-d0CiNxlUecf3OO6fxrAzDdIPjwD8vmymYOo2zNTKVvtRBkWdgvVgv2qrgYhVQaZS56ix8CabBdTOv8FYbIZlWw== 9 | dependencies: 10 | "@pastable/react" "0.1.6" 11 | "@pastable/typings" "0.0.2" 12 | "@pastable/utils" "0.1.4" 13 | jotai "^0.16.5" 14 | react "^17.0.2" 15 | 16 | "@pastable/react@0.1.6": 17 | version "0.1.6" 18 | resolved "https://registry.npmjs.org/@pastable/react/-/react-0.1.6.tgz" 19 | integrity sha512-9GDoanJTZey2tEWLpHlcZ6P9/0oASBaLt2kQjQy8sFrK0SomHQeo3nic8+8N4Yqa+pc4IdkIjnXr0BIC0nhR7g== 20 | dependencies: 21 | "@pastable/utils" "0.1.3" 22 | jotai "^0.16.8" 23 | react "^17.0.2" 24 | 25 | "@pastable/typings@0.0.2": 26 | version "0.0.2" 27 | resolved "https://registry.npmjs.org/@pastable/typings/-/typings-0.0.2.tgz" 28 | integrity sha512-e5HAu5CJB0COp+zXHCSIcwQpJE28F6NBN7SYCfaKlJ5Xh3jCMwUbW6KVadnW6lEWxMXVRTrvA13Gy5SMGwijWQ== 29 | 30 | "@pastable/utils@0.1.3": 31 | version "0.1.3" 32 | resolved "https://registry.npmjs.org/@pastable/utils/-/utils-0.1.3.tgz" 33 | integrity sha512-pOoTc1xarmqkdWwoZC/pJUG7EgpEfH9MTQBwcVRLe1vrUz1xAk5dUBPyK6cmLdvf9SYx258N0s5PwQD3o4U0sA== 34 | 35 | "@pastable/utils@0.1.4": 36 | version "0.1.4" 37 | resolved "https://registry.npmjs.org/@pastable/utils/-/utils-0.1.4.tgz" 38 | integrity sha512-Necr+L5EAGQCsitzXh7Q/t97akAvH24aiNe3UgtsaHhj/EtsOGinngdwP/AxgDK0f5Xt4umhdTyEQeKUDXc7IQ== 39 | 40 | "@types/estree@0.0.39": 41 | version "0.0.39" 42 | resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" 43 | integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== 44 | 45 | "@types/node@*": 46 | version "16.4.6" 47 | resolved "https://registry.yarnpkg.com/@types/node/-/node-16.4.6.tgz#1734d119dfa8fede5606d35ae10f9fc9c84d889b" 48 | integrity sha512-FKyawK3o5KL16AwbeFajen8G4K3mmqUrQsehn5wNKs8IzlKHE8TfnSmILXVMVziAEcnB23u1RCFU1NT6hSyr7Q== 49 | 50 | "@types/prop-types@*": 51 | version "15.7.4" 52 | resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz" 53 | integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== 54 | 55 | "@types/react@^17.0.15": 56 | version "17.0.15" 57 | resolved "https://registry.npmjs.org/@types/react/-/react-17.0.15.tgz" 58 | integrity sha512-uTKHDK9STXFHLaKv6IMnwp52fm0hwU+N89w/p9grdUqcFA6WuqDyPhaWopbNyE1k/VhgzmHl8pu1L4wITtmlLw== 59 | dependencies: 60 | "@types/prop-types" "*" 61 | "@types/scheduler" "*" 62 | csstype "^3.0.2" 63 | 64 | "@types/rollup-plugin-peer-deps-external@^2.2.1": 65 | version "2.2.1" 66 | resolved "https://registry.yarnpkg.com/@types/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.1.tgz#7711fe77727c2ec920d3e096fa4e61d02734a23d" 67 | integrity sha512-CUK4Mi3M3A/0coRUeo4McvUFOxR5H6UYd6Wt3D+K4c4fJupolMSoIxsZaBG1g/BJQOaujf78Pf7KhgfhrX1FZA== 68 | dependencies: 69 | "@types/node" "*" 70 | rollup "^0.63.4" 71 | 72 | "@types/scheduler@*": 73 | version "0.16.2" 74 | resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" 75 | integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 76 | 77 | abstract-leveldown@^6.2.1: 78 | version "6.3.0" 79 | resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.3.0.tgz#d25221d1e6612f820c35963ba4bd739928f6026a" 80 | integrity sha512-TU5nlYgta8YrBMNpc9FwQzRbiXsj49gsALsXadbGHt9CROPzX5fB0rWDR5mtdpOOKa5XqRFpbj1QroPAoPzVjQ== 81 | dependencies: 82 | buffer "^5.5.0" 83 | immediate "^3.2.3" 84 | level-concat-iterator "~2.0.0" 85 | level-supports "~1.0.0" 86 | xtend "~4.0.0" 87 | 88 | abstract-leveldown@~6.2.1, abstract-leveldown@~6.2.3: 89 | version "6.2.3" 90 | resolved "https://registry.yarnpkg.com/abstract-leveldown/-/abstract-leveldown-6.2.3.tgz#036543d87e3710f2528e47040bc3261b77a9a8eb" 91 | integrity sha512-BsLm5vFMRUrrLeCcRc+G0t2qOaTzpoJQLOubq2XM72eNpjF5UdU5o/5NvlNhx95XHcAvcl8OMXr4mlg/fRgUXQ== 92 | dependencies: 93 | buffer "^5.5.0" 94 | immediate "^3.2.3" 95 | level-concat-iterator "~2.0.0" 96 | level-supports "~1.0.0" 97 | xtend "~4.0.0" 98 | 99 | async-limiter@~1.0.0: 100 | version "1.0.1" 101 | resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" 102 | integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== 103 | 104 | base64-js@^1.3.1: 105 | version "1.5.1" 106 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 107 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 108 | 109 | buffer@^5.5.0, buffer@^5.6.0: 110 | version "5.7.1" 111 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 112 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 113 | dependencies: 114 | base64-js "^1.3.1" 115 | ieee754 "^1.1.13" 116 | 117 | colorette@^1.2.2: 118 | version "1.2.2" 119 | resolved "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz" 120 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== 121 | 122 | csstype@^3.0.2: 123 | version "3.0.8" 124 | resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz" 125 | integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw== 126 | 127 | deferred-leveldown@~5.3.0: 128 | version "5.3.0" 129 | resolved "https://registry.yarnpkg.com/deferred-leveldown/-/deferred-leveldown-5.3.0.tgz#27a997ad95408b61161aa69bd489b86c71b78058" 130 | integrity sha512-a59VOT+oDy7vtAbLRCZwWgxu2BaCfd5Hk7wxJd48ei7I+nsg8Orlb9CLG0PMZienk9BSUKgeAqkO2+Lw+1+Ukw== 131 | dependencies: 132 | abstract-leveldown "~6.2.1" 133 | inherits "^2.0.3" 134 | 135 | encoding-down@^6.3.0: 136 | version "6.3.0" 137 | resolved "https://registry.yarnpkg.com/encoding-down/-/encoding-down-6.3.0.tgz#b1c4eb0e1728c146ecaef8e32963c549e76d082b" 138 | integrity sha512-QKrV0iKR6MZVJV08QY0wp1e7vF6QbhnbQhb07bwpEyuz4uZiZgPlEGdkCROuFkUwdxlFaiPIhjyarH1ee/3vhw== 139 | dependencies: 140 | abstract-leveldown "^6.2.1" 141 | inherits "^2.0.3" 142 | level-codec "^9.0.0" 143 | level-errors "^2.0.0" 144 | 145 | errno@~0.1.1: 146 | version "0.1.8" 147 | resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" 148 | integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== 149 | dependencies: 150 | prr "~1.0.1" 151 | 152 | esbuild@^0.12.8: 153 | version "0.12.15" 154 | resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.12.15.tgz" 155 | integrity sha512-72V4JNd2+48eOVCXx49xoSWHgC3/cCy96e7mbXKY+WOWghN00cCmlGnwVLRhRHorvv0dgCyuMYBZlM2xDM5OQw== 156 | 157 | fast-deep-equal@^3.1.3: 158 | version "3.1.3" 159 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" 160 | integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== 161 | 162 | fsevents@~2.3.2: 163 | version "2.3.2" 164 | resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" 165 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 166 | 167 | function-bind@^1.1.1: 168 | version "1.1.1" 169 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" 170 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 171 | 172 | has@^1.0.3: 173 | version "1.0.3" 174 | resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" 175 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 176 | dependencies: 177 | function-bind "^1.1.1" 178 | 179 | ieee754@^1.1.13: 180 | version "1.2.1" 181 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 182 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 183 | 184 | immediate@^3.2.3: 185 | version "3.3.0" 186 | resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.3.0.tgz#1aef225517836bcdf7f2a2de2600c79ff0269266" 187 | integrity sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q== 188 | 189 | inherits@^2.0.3, inherits@^2.0.4: 190 | version "2.0.4" 191 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 192 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 193 | 194 | is-core-module@^2.2.0: 195 | version "2.5.0" 196 | resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.5.0.tgz" 197 | integrity sha512-TXCMSDsEHMEEZ6eCA8rwRDbLu55MRGmrctljsBX/2v1d9/GzqHOxW5c5oPSgrUt2vBFXebu9rGqckXGPWOlYpg== 198 | dependencies: 199 | has "^1.0.3" 200 | 201 | isomorphic.js@^0.2.4: 202 | version "0.2.4" 203 | resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.2.4.tgz#24ca374163ae54a7ce3b86ce63b701b91aa84969" 204 | integrity sha512-Y4NjZceAwaPXctwsHgNsmfuPxR8lJ3f8X7QTAkhltrX4oGIv+eTlgHLXn4tWysC9zGTi929gapnPp+8F8cg7nA== 205 | 206 | jotai@^0.16.5, jotai@^0.16.8: 207 | version "0.16.11" 208 | resolved "https://registry.npmjs.org/jotai/-/jotai-0.16.11.tgz" 209 | integrity sha512-EPBeDSBc4FwbFRArRjcI6IsHrVkba771mSOFTN3M4r5rU3ZcZMDFLElZScZVr6w1kgrEXvaMm59VAyEB3QUEbA== 210 | 211 | jotai@^1.2.2: 212 | version "1.2.2" 213 | resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.2.2.tgz#631fd7ad44e9ac26cdf9874d52282c1cfe032807" 214 | integrity sha512-iqkkUdWsH2Mk4HY1biba/8kA77+8liVBy8E0d8Nce29qow4h9mzdDhpTasAruuFYPycw6JvfZgL5RB0JJuIZjw== 215 | 216 | "js-tokens@^3.0.0 || ^4.0.0": 217 | version "4.0.0" 218 | resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" 219 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 220 | 221 | level-codec@^9.0.0: 222 | version "9.0.2" 223 | resolved "https://registry.yarnpkg.com/level-codec/-/level-codec-9.0.2.tgz#fd60df8c64786a80d44e63423096ffead63d8cbc" 224 | integrity sha512-UyIwNb1lJBChJnGfjmO0OR+ezh2iVu1Kas3nvBS/BzGnx79dv6g7unpKIDNPMhfdTEGoc7mC8uAu51XEtX+FHQ== 225 | dependencies: 226 | buffer "^5.6.0" 227 | 228 | level-concat-iterator@~2.0.0: 229 | version "2.0.1" 230 | resolved "https://registry.yarnpkg.com/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz#1d1009cf108340252cb38c51f9727311193e6263" 231 | integrity sha512-OTKKOqeav2QWcERMJR7IS9CUo1sHnke2C0gkSmcR7QuEtFNLLzHQAvnMw8ykvEcv0Qtkg0p7FOwP1v9e5Smdcw== 232 | 233 | level-errors@^2.0.0, level-errors@~2.0.0: 234 | version "2.0.1" 235 | resolved "https://registry.yarnpkg.com/level-errors/-/level-errors-2.0.1.tgz#2132a677bf4e679ce029f517c2f17432800c05c8" 236 | integrity sha512-UVprBJXite4gPS+3VznfgDSU8PTRuVX0NXwoWW50KLxd2yw4Y1t2JUR5In1itQnudZqRMT9DlAM3Q//9NCjCFw== 237 | dependencies: 238 | errno "~0.1.1" 239 | 240 | level-iterator-stream@~4.0.0: 241 | version "4.0.2" 242 | resolved "https://registry.yarnpkg.com/level-iterator-stream/-/level-iterator-stream-4.0.2.tgz#7ceba69b713b0d7e22fcc0d1f128ccdc8a24f79c" 243 | integrity sha512-ZSthfEqzGSOMWoUGhTXdX9jv26d32XJuHz/5YnuHZzH6wldfWMOVwI9TBtKcya4BKTyTt3XVA0A3cF3q5CY30Q== 244 | dependencies: 245 | inherits "^2.0.4" 246 | readable-stream "^3.4.0" 247 | xtend "^4.0.2" 248 | 249 | level-js@^5.0.0: 250 | version "5.0.2" 251 | resolved "https://registry.yarnpkg.com/level-js/-/level-js-5.0.2.tgz#5e280b8f93abd9ef3a305b13faf0b5397c969b55" 252 | integrity sha512-SnBIDo2pdO5VXh02ZmtAyPP6/+6YTJg2ibLtl9C34pWvmtMEmRTWpra+qO/hifkUtBTOtfx6S9vLDjBsBK4gRg== 253 | dependencies: 254 | abstract-leveldown "~6.2.3" 255 | buffer "^5.5.0" 256 | inherits "^2.0.3" 257 | ltgt "^2.1.2" 258 | 259 | level-packager@^5.1.0: 260 | version "5.1.1" 261 | resolved "https://registry.yarnpkg.com/level-packager/-/level-packager-5.1.1.tgz#323ec842d6babe7336f70299c14df2e329c18939" 262 | integrity sha512-HMwMaQPlTC1IlcwT3+swhqf/NUO+ZhXVz6TY1zZIIZlIR0YSn8GtAAWmIvKjNY16ZkEg/JcpAuQskxsXqC0yOQ== 263 | dependencies: 264 | encoding-down "^6.3.0" 265 | levelup "^4.3.2" 266 | 267 | level-supports@~1.0.0: 268 | version "1.0.1" 269 | resolved "https://registry.yarnpkg.com/level-supports/-/level-supports-1.0.1.tgz#2f530a596834c7301622521988e2c36bb77d122d" 270 | integrity sha512-rXM7GYnW8gsl1vedTJIbzOrRv85c/2uCMpiiCzO2fndd06U/kUXEEU9evYn4zFggBOg36IsBW8LzqIpETwwQzg== 271 | dependencies: 272 | xtend "^4.0.2" 273 | 274 | level@^6.0.1: 275 | version "6.0.1" 276 | resolved "https://registry.yarnpkg.com/level/-/level-6.0.1.tgz#dc34c5edb81846a6de5079eac15706334b0d7cd6" 277 | integrity sha512-psRSqJZCsC/irNhfHzrVZbmPYXDcEYhA5TVNwr+V92jF44rbf86hqGp8fiT702FyiArScYIlPSBTDUASCVNSpw== 278 | dependencies: 279 | level-js "^5.0.0" 280 | level-packager "^5.1.0" 281 | leveldown "^5.4.0" 282 | 283 | leveldown@^5.4.0: 284 | version "5.6.0" 285 | resolved "https://registry.yarnpkg.com/leveldown/-/leveldown-5.6.0.tgz#16ba937bb2991c6094e13ac5a6898ee66d3eee98" 286 | integrity sha512-iB8O/7Db9lPaITU1aA2txU/cBEXAt4vWwKQRrrWuS6XDgbP4QZGj9BL2aNbwb002atoQ/lIotJkfyzz+ygQnUQ== 287 | dependencies: 288 | abstract-leveldown "~6.2.1" 289 | napi-macros "~2.0.0" 290 | node-gyp-build "~4.1.0" 291 | 292 | levelup@^4.3.2: 293 | version "4.4.0" 294 | resolved "https://registry.yarnpkg.com/levelup/-/levelup-4.4.0.tgz#f89da3a228c38deb49c48f88a70fb71f01cafed6" 295 | integrity sha512-94++VFO3qN95cM/d6eBXvd894oJE0w3cInq9USsyQzzoJxmiYzPAocNcuGCPGGjoXqDVJcr3C1jzt1TSjyaiLQ== 296 | dependencies: 297 | deferred-leveldown "~5.3.0" 298 | level-errors "~2.0.0" 299 | level-iterator-stream "~4.0.0" 300 | level-supports "~1.0.0" 301 | xtend "~4.0.0" 302 | 303 | lib0@^0.2.31, lib0@^0.2.41, lib0@^0.2.42: 304 | version "0.2.42" 305 | resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.42.tgz#6d8bf1fb8205dec37a953c521c5ee403fd8769b0" 306 | integrity sha512-8BNM4MiokEKzMvSxTOC3gnCBisJH+jL67CnSnqzHv3jli3pUvGC8wz+0DQ2YvGr4wVQdb2R2uNNPw9LEpVvJ4Q== 307 | dependencies: 308 | isomorphic.js "^0.2.4" 309 | 310 | lodash.debounce@^4.0.8: 311 | version "4.0.8" 312 | resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" 313 | integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= 314 | 315 | loose-envify@^1.1.0: 316 | version "1.4.0" 317 | resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" 318 | integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 319 | dependencies: 320 | js-tokens "^3.0.0 || ^4.0.0" 321 | 322 | ltgt@^2.1.2: 323 | version "2.2.1" 324 | resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" 325 | integrity sha1-81ypHEk/e3PaDgdJUwTxezH4fuU= 326 | 327 | nanoid@^3.1.23: 328 | version "3.1.23" 329 | resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz" 330 | integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== 331 | 332 | napi-macros@~2.0.0: 333 | version "2.0.0" 334 | resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" 335 | integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg== 336 | 337 | node-gyp-build@~4.1.0: 338 | version "4.1.1" 339 | resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.1.1.tgz#d7270b5d86717068d114cc57fff352f96d745feb" 340 | integrity sha512-dSq1xmcPDKPZ2EED2S6zw/b9NKsqzXRE6dVr8TVQnI3FJOTteUMuqF3Qqs6LZg+mLGYJWqQzMbIjMtJqTv87nQ== 341 | 342 | object-assign@^4.1.1: 343 | version "4.1.1" 344 | resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" 345 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 346 | 347 | path-parse@^1.0.6: 348 | version "1.0.7" 349 | resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" 350 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 351 | 352 | postcss@^8.3.5: 353 | version "8.3.6" 354 | resolved "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz" 355 | integrity sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A== 356 | dependencies: 357 | colorette "^1.2.2" 358 | nanoid "^3.1.23" 359 | source-map-js "^0.6.2" 360 | 361 | prettier@^2.3.2: 362 | version "2.3.2" 363 | resolved "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz" 364 | integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ== 365 | 366 | proxy-compare@2.0.1: 367 | version "2.0.1" 368 | resolved "https://registry.yarnpkg.com/proxy-compare/-/proxy-compare-2.0.1.tgz#3ae19cf47f64e89bd60fe4f57afd124f733ef64f" 369 | integrity sha512-uXj3TtWdR1S2SNwJKbgJB+1FJm9HM3sFzlVc8W6PZvU6ogt9mlkb1WwZQpuKFLkDS6LKY4+FBE18K6ZArphnHA== 370 | 371 | prr@~1.0.1: 372 | version "1.0.1" 373 | resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" 374 | integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= 375 | 376 | react@^17.0.2: 377 | version "17.0.2" 378 | resolved "https://registry.npmjs.org/react/-/react-17.0.2.tgz" 379 | integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== 380 | dependencies: 381 | loose-envify "^1.1.0" 382 | object-assign "^4.1.1" 383 | 384 | readable-stream@^3.4.0: 385 | version "3.6.0" 386 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 387 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 388 | dependencies: 389 | inherits "^2.0.3" 390 | string_decoder "^1.1.1" 391 | util-deprecate "^1.0.1" 392 | 393 | resolve@^1.20.0: 394 | version "1.20.0" 395 | resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz" 396 | integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== 397 | dependencies: 398 | is-core-module "^2.2.0" 399 | path-parse "^1.0.6" 400 | 401 | rollup-plugin-peer-deps-external@^2.2.4: 402 | version "2.2.4" 403 | resolved "https://registry.yarnpkg.com/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz#8a420bbfd6dccc30aeb68c9bf57011f2f109570d" 404 | integrity sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g== 405 | 406 | rollup@^0.63.4: 407 | version "0.63.5" 408 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db" 409 | integrity sha512-dFf8LpUNzIj3oE0vCvobX6rqOzHzLBoblyFp+3znPbjiSmSvOoK2kMKx+Fv9jYduG1rvcCfCveSgEaQHjWRF6g== 410 | dependencies: 411 | "@types/estree" "0.0.39" 412 | "@types/node" "*" 413 | 414 | rollup@^2.38.5: 415 | version "2.53.3" 416 | resolved "https://registry.npmjs.org/rollup/-/rollup-2.53.3.tgz" 417 | integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA== 418 | optionalDependencies: 419 | fsevents "~2.3.2" 420 | 421 | safe-buffer@~5.2.0: 422 | version "5.2.1" 423 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 424 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 425 | 426 | source-map-js@^0.6.2: 427 | version "0.6.2" 428 | resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz" 429 | integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug== 430 | 431 | string_decoder@^1.1.1: 432 | version "1.3.0" 433 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 434 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 435 | dependencies: 436 | safe-buffer "~5.2.0" 437 | 438 | typescript@^4.3.2: 439 | version "4.3.5" 440 | resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz" 441 | integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== 442 | 443 | util-deprecate@^1.0.1: 444 | version "1.0.2" 445 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 446 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 447 | 448 | valtio-yjs@^0.1.0: 449 | version "0.1.0" 450 | resolved "https://registry.yarnpkg.com/valtio-yjs/-/valtio-yjs-0.1.0.tgz#361e8e0687ce380b3e82709a987de5670d740d9e" 451 | integrity sha512-Xu7VMml6ZeHIboZHgqkJVvfhyTK8+cSA/6uSbazevv7x4kgBTdY20Fhua+ETV3Sj7VGc1mlgN3TB/+vRrY3ARg== 452 | dependencies: 453 | fast-deep-equal "^3.1.3" 454 | 455 | valtio@^1.1.0: 456 | version "1.1.0" 457 | resolved "https://registry.yarnpkg.com/valtio/-/valtio-1.1.0.tgz#6069ba7b5ae7e05a579e81ca678b4142dbd7e478" 458 | integrity sha512-CDRbi53VxMSB0oRJ4u/ueqrmHKkPxzRMif4Ew0SSqiyaDVvPnMULMPq+jmN3XXdLCmbQgh20Kt1G/YA48i5Kkg== 459 | dependencies: 460 | proxy-compare "2.0.1" 461 | 462 | vite@^2.4.3: 463 | version "2.4.3" 464 | resolved "https://registry.npmjs.org/vite/-/vite-2.4.3.tgz" 465 | integrity sha512-iT6NPeiUUZ2FkzC3eazytOEMRaM4J+xgRQcNcpRcbmfYjakCFP4WKPJpeEz1U5JEKHAtwv3ZBQketQUFhFU3ng== 466 | dependencies: 467 | esbuild "^0.12.8" 468 | postcss "^8.3.5" 469 | resolve "^1.20.0" 470 | rollup "^2.38.5" 471 | optionalDependencies: 472 | fsevents "~2.3.2" 473 | 474 | ws@^6.2.1: 475 | version "6.2.2" 476 | resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e" 477 | integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw== 478 | dependencies: 479 | async-limiter "~1.0.0" 480 | 481 | xtend@^4.0.2, xtend@~4.0.0: 482 | version "4.0.2" 483 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 484 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 485 | 486 | y-leveldb@^0.1.0: 487 | version "0.1.0" 488 | resolved "https://registry.yarnpkg.com/y-leveldb/-/y-leveldb-0.1.0.tgz#8b60c1af020252445875ebc70d52666017bcb038" 489 | integrity sha512-sMuitVrsAUNh+0b66I42nAuW3lCmez171uP4k0ePcTAJ+c+Iw9w4Yq3wwiyrDMFXBEyQSjSF86Inc23wEvWnxw== 490 | dependencies: 491 | level "^6.0.1" 492 | lib0 "^0.2.31" 493 | 494 | y-protocols@^1.0.5: 495 | version "1.0.5" 496 | resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.5.tgz#91d574250060b29fcac8f8eb5e276fbad594245e" 497 | integrity sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A== 498 | dependencies: 499 | lib0 "^0.2.42" 500 | 501 | y-websocket@^1.3.16: 502 | version "1.3.16" 503 | resolved "https://registry.yarnpkg.com/y-websocket/-/y-websocket-1.3.16.tgz#0ec1a141d593933dfbfba2fb9fa9d95dca332c89" 504 | integrity sha512-538dwNOQeZCpMfhh67y40goxHQZKubjoXtfhQieUF2bIQfHVV44bGFeAiYiBHgwOSRdwp7qG4MmDwU0M3U3vng== 505 | dependencies: 506 | lib0 "^0.2.42" 507 | lodash.debounce "^4.0.8" 508 | y-protocols "^1.0.5" 509 | optionalDependencies: 510 | ws "^6.2.1" 511 | y-leveldb "^0.1.0" 512 | 513 | yjs@^13.5.11: 514 | version "13.5.11" 515 | resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.5.11.tgz#8f41a61fd0039a8d720a5b3186b8ad319d88563b" 516 | integrity sha512-nJzML0NoSUh+kZLEOssYViPI1ZECv/7rnLk5mhXvhMTnezNAYWAIfNLvo+FHYRhWBojbrutT4d2IAP/IE9Xaog== 517 | dependencies: 518 | lib0 "^0.2.41" 519 | --------------------------------------------------------------------------------