├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── client ├── .eslintrc.js ├── .nojekyll ├── index.css ├── index.html ├── package-lock.json ├── package.json ├── snowpack.config.js └── src │ ├── components │ ├── ConnectionForm.jsx │ └── SlateEditor │ │ ├── components.jsx │ │ └── index.jsx │ ├── index.jsx │ └── services │ ├── state.js │ ├── useCursor.js │ ├── useLocalStorage.js │ └── y-websocket.js ├── docs ├── .eslintrc.js ├── .nojekyll ├── _snowpack │ ├── env.js │ └── pkg │ │ ├── @emotion │ │ └── css.js │ │ ├── common │ │ ├── _commonjsHelpers-8c19dec8.js │ │ ├── binary-e1a1f68b.js │ │ ├── buffer-551584fe.js │ │ ├── decoding-6e54b617.js │ │ ├── encoding-7fdf95b6.js │ │ ├── function-debeb549.js │ │ ├── index-57a74e37.js │ │ ├── index-8dbeb7e4.js │ │ ├── index.es-71fa96c1.js │ │ ├── map-c5ea9815.js │ │ ├── math-91bb74dc.js │ │ ├── object-034d355c.js │ │ ├── observable-363df4ab.js │ │ ├── process-2545f00a.js │ │ ├── time-c2bb43f3.js │ │ └── yjs-95ac26e6.js │ │ ├── import-map.json │ │ ├── is-hotkey.js │ │ ├── lib0 │ │ ├── broadcastchannel.js │ │ ├── buffer.js │ │ ├── decoding.js │ │ ├── encoding.js │ │ ├── math.js │ │ ├── mutex.js │ │ ├── observable.js │ │ ├── time.js │ │ └── url.js │ │ ├── randomcolor.js │ │ ├── react-dom.js │ │ ├── react.js │ │ ├── slate-history.js │ │ ├── slate-react.js │ │ ├── slate-yjs.js │ │ ├── slate.js │ │ ├── y-protocols │ │ ├── auth.js │ │ ├── awareness.js │ │ └── sync.js │ │ └── yjs.js ├── index.css ├── index.html ├── package-lock.json ├── package.json ├── snowpack.config.js └── src │ ├── components │ ├── ConnectionForm.js │ └── SlateEditor │ │ ├── components.js │ │ └── index.js │ ├── index.js │ └── services │ ├── state.js │ ├── useCursor.js │ ├── useLocalStorage.js │ └── y-websocket.js ├── jsconfig.json ├── local-db ├── cleanup.sh ├── connections_table.json ├── docker-compose.yml ├── docs_table.json ├── package.json └── setup.sh ├── package-lock.json ├── server ├── .eslintrc.js ├── babel.config.json ├── db │ └── aws.js ├── handler │ └── aws.js ├── local-env.cjs ├── package-lock.json ├── package.json ├── patches │ ├── aws-lambda-ws-server+0.1.21.patch │ └── aws-post-to-connection+0.1.21.patch └── rollup.config.js └── stack ├── .eslintrc.js ├── README.md ├── cdk.json ├── config.json ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | build 3 | web_modules 4 | node_modules 5 | dbDir 6 | cdk.out 7 | .cdk.staging 8 | .snowpack 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "javascript.preferences.importModuleSpecifierEnding": "js", 4 | "javascript.preferences.quoteStyle": "single", 5 | "javascript.format.semicolons": "remove", 6 | "files.insertFinalNewline": true, 7 | "files.trimFinalNewlines": true, 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": true 10 | }, 11 | "eslint.validate": ["javascript"] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gabe Rogan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Working Demo 2 | https://gaberogan.github.io/y-websocket-api/ 3 | 4 | ## YJS for AWS Websocket API 5 | This is a demo of YJS working with AWS Websocket API and DynamoDB. The intent is for this to become a library where you can run a few CLI commands and launch a fully scalable YJS infrastructure on AWS. 6 | 7 | ## Getting Started 8 | The `client` folder is a frontend demo of YJS with SlateJS. You can initialize `client` with `npm i && npm start`. It depends on the `server` folder which has a local version of the YJS for AWS backend, based off y-websocket. You can initialize `server` with `npm i && npm start`. The `server` folder in turn depends on the `local-db` folder, which you can find setup instructions for below. The `stack` folder is the full CDK stack for the `server`. Run `npm i` and `npm i -g aws-cdk` then use the `npm run deploy` to deploy the infrastructure. 9 | 10 | ## Docker 11 | 12 | with WSL2 see fix here https://github.com/docker/compose/issues/7495#issuecomment-649035078 13 | 14 | ```sh 15 | cd local-db 16 | npm start # run dynamodb local 17 | npm run setup # create tables on server start 18 | ``` 19 | 20 | Debug 21 | ```sh 22 | aws dynamodb list-tables --endpoint-url http://localhost:8000 23 | aws dynamodb scan --table-name docs --endpoint-url http://localhost:8000 24 | ``` 25 | 26 | ## Troubleshooting 27 | 28 | - use node >= v 14.15.4 29 | - make sure aws cli is correct region in stack/config.json and local-env.cjs 30 | - replace account id with yours in stack/config.json 31 | - make sure client/services/state.js has the endpoint you want 32 | - make sure you've configured your aws cli. if you have multiple accounts, add --profile MYPROFILE to deploy command in stack 33 | - in client/services/state.js change the endpoint to ws://localhost:9000 34 | 35 | ## Known Issues 36 | 37 | - can't handle json error from websocket 38 | - new connections keep getting created for some reason 39 | - max document size 400KB (planning to fix) 40 | - doesn't flush document history at the moment 41 | -------------------------------------------------------------------------------- /client/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es2021': true 4 | }, 5 | 'extends': [ 6 | 'plugin:react/recommended', 7 | 'plugin:react-hooks/recommended' 8 | ], 9 | 'parserOptions': { 10 | 'ecmaVersion': 12, 11 | 'sourceType': 'module', 12 | 'ecmaFeatures': { 13 | 'jsx': true 14 | }, 15 | }, 16 | 'rules': { 17 | 'indent': [ 18 | 'error', 19 | 2 20 | ], 21 | 'linebreak-style': [ 22 | 'error', 23 | 'unix' 24 | ], 25 | 'quotes': [ 26 | 'error', 27 | 'single' 28 | ], 29 | 'semi': [ 30 | 'error', 31 | 'never' 32 | ], 33 | 'react/prop-types': 0 34 | }, 35 | 'ignorePatterns': ['build'] 36 | } 37 | -------------------------------------------------------------------------------- /client/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaberogan/y-websocket-api/53cff338dfce95e180b1b99d0fdddd72292c3e90/client/.nojekyll -------------------------------------------------------------------------------- /client/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | input, 3 | textarea { 4 | font-family: 'Roboto', sans-serif; 5 | line-height: 1.4; 6 | background: #eee; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | p { 14 | margin: 0; 15 | } 16 | 17 | pre { 18 | padding: 10px; 19 | background-color: #eee; 20 | white-space: pre-wrap; 21 | } 22 | 23 | :not(pre) > code { 24 | font-family: monospace; 25 | background-color: #eee; 26 | padding: 3px; 27 | } 28 | 29 | img { 30 | max-width: 100%; 31 | max-height: 20em; 32 | } 33 | 34 | blockquote { 35 | border-left: 2px solid #ddd; 36 | margin-left: 0; 37 | margin-right: 0; 38 | padding-left: 10px; 39 | color: #aaa; 40 | font-style: italic; 41 | } 42 | 43 | blockquote[dir='rtl'] { 44 | border-left: none; 45 | padding-left: 0; 46 | padding-right: 10px; 47 | border-right: 2px solid #ddd; 48 | } 49 | 50 | table { 51 | border-collapse: collapse; 52 | } 53 | 54 | td { 55 | padding: 10px; 56 | border: 2px solid #ddd; 57 | } 58 | 59 | input { 60 | box-sizing: border-box; 61 | font-size: 0.85em; 62 | width: 100%; 63 | padding: 0.5em; 64 | border: 2px solid #ddd; 65 | background: #fafafa; 66 | } 67 | 68 | input:focus { 69 | outline: 0; 70 | border-color: blue; 71 | } 72 | 73 | [data-slate-editor] > * + * { 74 | margin-top: 1em; 75 | } 76 | 77 | /* Quill Background */ 78 | .ql-toolbar, #editor-container { 79 | background: white; 80 | } 81 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | y-websocket-api demo 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "snowpack dev", 4 | "build": "snowpack build", 5 | "test": "echo \"This template does not include a test runner by default.\" && exit 1" 6 | }, 7 | "devDependencies": { 8 | "eslint": "^7.17.0", 9 | "eslint-plugin-react": "^7.22.0", 10 | "eslint-plugin-react-hooks": "^4.2.0", 11 | "snowpack": "^3.0.11" 12 | }, 13 | "dependencies": { 14 | "@emotion/css": "^11.1.3", 15 | "is-hotkey": "^0.2.0", 16 | "lib0": "^0.2.35", 17 | "quill": "^1.3.7", 18 | "quill-cursors": "^3.0.1", 19 | "randomcolor": "^0.6.2", 20 | "react": "^17.0.1", 21 | "react-dom": "^17.0.1", 22 | "slate": "^0.59.0", 23 | "slate-history": "^0.59.0", 24 | "slate-react": "^0.59.0", 25 | "slate-yjs": "^1.0.0", 26 | "y-protocols": "^1.0.2", 27 | "y-quill": "^0.1.4", 28 | "yjs": "^13.4.9" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/snowpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packageOptions: { 3 | // source: 'remote', // TODO y-protocols fails :( submit PR? 4 | }, 5 | buildOptions: { 6 | out: '../docs', 7 | baseUrl: '/y-websocket-api' 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /client/src/components/ConnectionForm.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { css } from '@emotion/css' 3 | import useLocalStorage from '../services/useLocalStorage.js' 4 | 5 | const inputGroup = css` 6 | display: flex; 7 | gap: 10px; 8 | ` 9 | 10 | const submitBtn = css` 11 | background: #2185d0; 12 | white-space: nowrap; 13 | color: white; 14 | font-weight: bold; 15 | border: none; 16 | padding: 10px 15px; 17 | font-size: 14px; 18 | cursor: pointer; 19 | ` 20 | 21 | export default () => { 22 | const [storedValue, setStoredValue] = useLocalStorage('document', `doc-${Math.round(Math.random() * 1e4)}`) 23 | 24 | return ( 25 |
26 | setStoredValue(e.target.value)} /> 27 |
location.reload()}>Update Document
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /client/src/components/SlateEditor/components.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | // @ts-nocheck 3 | 4 | import React from 'react' 5 | import ReactDOM from 'react-dom' 6 | import { cx, css } from '@emotion/css' 7 | 8 | export const Button = React.forwardRef( 9 | ({ className, active, reversed, ...props }, ref) => ( 10 | 27 | ) 28 | ) 29 | 30 | export const EditorValue = React.forwardRef( 31 | ({ className, value, ...props }, ref) => { 32 | const textLines = value.document.nodes 33 | .map(node => node.text) 34 | .toArray() 35 | .join('\n') 36 | return ( 37 |
47 |
56 | Slate's value as text 57 |
58 |
69 | {textLines} 70 |
71 |
72 | ) 73 | } 74 | ) 75 | 76 | export const Icon = React.forwardRef( 77 | ({ className, ...props }, ref) => ( 78 | 90 | ) 91 | ) 92 | 93 | export const Instruction = React.forwardRef( 94 | ({ className, ...props }, ref) => ( 95 |
109 | ) 110 | ) 111 | 112 | export const Menu = React.forwardRef( 113 | ({ className, ...props }, ref) => ( 114 |
* { 121 | display: inline-block; 122 | } 123 | 124 | & > * + * { 125 | margin-left: 15px; 126 | } 127 | ` 128 | )} 129 | /> 130 | ) 131 | ) 132 | 133 | export const Portal = ({ children }) => { 134 | return ReactDOM.createPortal(children, document.body) 135 | } 136 | 137 | export const Toolbar = React.forwardRef( 138 | ({ className, ...props }, ref) => ( 139 | 153 | ) 154 | ) 155 | -------------------------------------------------------------------------------- /client/src/components/SlateEditor/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import React, { useCallback, useMemo, useState, useEffect } from 'react' 3 | import isHotkey from 'is-hotkey' 4 | import { Editable, withReact, useSlate, Slate } from 'slate-react' 5 | import { 6 | Editor, 7 | Transforms, 8 | createEditor, 9 | Element as SlateElement, 10 | } from 'slate' 11 | import { withHistory } from 'slate-history' 12 | import * as Y from 'yjs' 13 | import { withYjs, toSharedType } from 'slate-yjs' 14 | import randomColor from 'randomcolor' 15 | import { WebsocketProvider } from '../../services/y-websocket' 16 | import { cx, css } from '@emotion/css' 17 | import { Button, Icon, Toolbar } from './components' 18 | import { YJS_ENDPOINT } from '../../services/state.js' 19 | import useCursor from '../../services/useCursor.js' 20 | import useLocalStorage from '../../services/useLocalStorage.js' 21 | 22 | const HOTKEYS = { 23 | 'mod+b': 'bold', 24 | 'mod+i': 'italic', 25 | 'mod+u': 'underline', 26 | 'mod+`': 'code', 27 | } 28 | 29 | const LIST_TYPES = ['numbered-list', 'bulleted-list'] 30 | 31 | const SlateEditor = () => { 32 | const [value, setValue] = useState([]) 33 | const [editable, setEditable] = useState(false) 34 | const [storedValue] = useLocalStorage('document', `doc-${Math.round(Math.random() * 1e4)}-fallback`) 35 | 36 | const [sharedType, provider] = useMemo(() => { 37 | const doc = new Y.Doc() 38 | const sharedType = doc.getArray('content') 39 | const provider = new WebsocketProvider(YJS_ENDPOINT, `?doc=${storedValue}`, doc) 40 | return [sharedType, provider] 41 | }, []) 42 | 43 | const editor = useMemo(() => { 44 | const editor = withYjs( 45 | withReact(withHistory(createEditor())), 46 | sharedType 47 | ) 48 | 49 | return editor 50 | }, []) 51 | 52 | const color = useMemo( 53 | () => 54 | randomColor({ 55 | luminosity: 'dark', 56 | format: 'rgba', 57 | alpha: 1 58 | }), 59 | [] 60 | ) 61 | 62 | const cursorOptions = { 63 | name: `User ${Math.round(Math.random() * 1000)}`, 64 | color, 65 | alphaColor: color.slice(0, -2) + '0.2)' 66 | } 67 | 68 | const { decorate } = useCursor(editor, provider.awareness, cursorOptions) 69 | 70 | const renderElement = useCallback(props => , []) 71 | const renderLeaf = useCallback((props) => , [decorate]) 72 | 73 | useEffect(() => { 74 | provider.on('status', ({ status }) => { 75 | setEditable(true) 76 | }) 77 | 78 | // Super hacky way to provide a initial value from the client, if 79 | // you plan to use y-websocket in prod you probably should provide the 80 | // initial state from the server. 81 | provider.on('sync', (isSynced) => { 82 | if (isSynced && sharedType.length === 0) { 83 | toSharedType(sharedType, [ 84 | { type: 'paragraph', children: [{ text: '' }] }, 85 | ]) 86 | } 87 | }) 88 | 89 | return () => { 90 | provider.disconnect() 91 | } 92 | }, []) 93 | 94 | return ( 95 | 96 | setValue(value)}> 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | {!editable && ( 109 |
Loading...
110 | )} 111 | {editable && ( 112 | { 120 | for (const hotkey in HOTKEYS) { 121 | if (isHotkey(hotkey, event)) { 122 | event.preventDefault() 123 | const mark = HOTKEYS[hotkey] 124 | toggleMark(editor, mark) 125 | } 126 | } 127 | }} 128 | /> 129 | )} 130 |
131 |
132 | ) 133 | } 134 | 135 | const toggleBlock = (editor, format) => { 136 | const isActive = isBlockActive(editor, format) 137 | const isList = LIST_TYPES.includes(format) 138 | 139 | Transforms.unwrapNodes(editor, { 140 | match: n => 141 | LIST_TYPES.includes( 142 | !Editor.isEditor(n) && SlateElement.isElement(n) && n.type 143 | ), 144 | split: true, 145 | }) 146 | const newProperties = { 147 | type: isActive ? 'paragraph' : isList ? 'list-item' : format, 148 | } 149 | Transforms.setNodes(editor, newProperties) 150 | 151 | if (!isActive && isList) { 152 | const block = { type: format, children: [] } 153 | Transforms.wrapNodes(editor, block) 154 | } 155 | } 156 | 157 | const toggleMark = (editor, format) => { 158 | const isActive = isMarkActive(editor, format) 159 | 160 | if (isActive) { 161 | Editor.removeMark(editor, format) 162 | } else { 163 | Editor.addMark(editor, format, true) 164 | } 165 | } 166 | 167 | const isBlockActive = (editor, format) => { 168 | const [match] = Editor.nodes(editor, { 169 | match: n => 170 | !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === format, 171 | }) 172 | 173 | return !!match 174 | } 175 | 176 | const isMarkActive = (editor, format) => { 177 | const marks = Editor.marks(editor) 178 | return marks ? marks[format] === true : false 179 | } 180 | 181 | const Element = ({ attributes, children, element }) => { 182 | switch (element.type) { 183 | case 'block-quote': 184 | return
{children}
185 | case 'bulleted-list': 186 | return
    {children}
187 | case 'heading-one': 188 | return

{children}

189 | case 'heading-two': 190 | return

{children}

191 | case 'list-item': 192 | return
  • {children}
  • 193 | case 'numbered-list': 194 | return
      {children}
    195 | default: 196 | return

    {children}

    197 | } 198 | } 199 | 200 | const Leaf = ({ attributes, children, leaf }) => { 201 | if (leaf.bold) { 202 | children = {children} 203 | } 204 | 205 | if (leaf.code) { 206 | children = {children} 207 | } 208 | 209 | if (leaf.italic) { 210 | children = {children} 211 | } 212 | 213 | if (leaf.underline) { 214 | children = {children} 215 | } 216 | 217 | return ( 218 | 227 | {leaf.isCaret ? : null} 228 | {children} 229 | 230 | ) 231 | } 232 | 233 | const BlockButton = ({ format, icon }) => { 234 | const editor = useSlate() 235 | return ( 236 | 245 | ) 246 | } 247 | 248 | const MarkButton = ({ format, icon }) => { 249 | const editor = useSlate() 250 | return ( 251 | 260 | ) 261 | } 262 | 263 | const Wrapper = ({ className, ...props }) => ( 264 |
    274 | ) 275 | 276 | const ExampleContent = props => ( 277 | 283 | ) 284 | 285 | // Cursor Caret 286 | const Caret = ({ color, isForward, name }) => { 287 | const cursorStyles = { 288 | ...cursorStyleBase, 289 | background: color, 290 | left: isForward ? '100%' : '0%' 291 | } 292 | const caretStyles = { 293 | ...caretStyleBase, 294 | background: color, 295 | left: isForward ? '100%' : '0%' 296 | } 297 | 298 | caretStyles[isForward ? 'bottom' : 'top'] = 0 299 | 300 | return ( 301 | <> 302 | 303 | 304 | 305 | {name} 306 | 307 | 308 | 309 | 310 | ) 311 | } 312 | 313 | const cursorStyleBase = { 314 | position: 'absolute', 315 | top: -2, 316 | pointerEvents: 'none', 317 | userSelect: 'none', 318 | transform: 'translateY(-100%)', 319 | fontSize: 10, 320 | color: 'white', 321 | background: 'palevioletred', 322 | whiteSpace: 'nowrap' 323 | } 324 | 325 | const caretStyleBase = { 326 | position: 'absolute', 327 | pointerEvents: 'none', 328 | userSelect: 'none', 329 | height: '1.2em', 330 | width: 2, 331 | background: 'palevioletred' 332 | } 333 | 334 | export default SlateEditor 335 | -------------------------------------------------------------------------------- /client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { css } from '@emotion/css' 4 | import SlateEditor from './components/SlateEditor' 5 | import ConnectionForm from './components/ConnectionForm' 6 | 7 | const pageStyle = css` 8 | margin: 24px; 9 | ` 10 | 11 | const App = () => ( 12 |
    13 | 14 | 15 |
    16 | ) 17 | 18 | ReactDOM.render(, document.getElementById('root')) 19 | -------------------------------------------------------------------------------- /client/src/services/state.js: -------------------------------------------------------------------------------- 1 | export const YJS_ENDPOINT = `wss://yrk3e12ayj.execute-api.us-east-1.amazonaws.com/dev` 2 | -------------------------------------------------------------------------------- /client/src/services/useCursor.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect } from 'react' 2 | import { Text, Range, Path } from 'slate' 3 | 4 | // Apply slate cursor to YJS 5 | export const applySlateCursor = (editor, awareness, cursorOptions) => { 6 | const selection = editor.selection 7 | const localCursor = awareness.getLocalState().cursor 8 | 9 | if (selection) { 10 | const updatedCursor = Object.assign( 11 | {}, 12 | localCursor, 13 | selection, 14 | cursorOptions, 15 | { 16 | isForward: Range.isForward(selection) 17 | } 18 | ) 19 | 20 | // Broadcast cursor 21 | if (JSON.stringify(updatedCursor) !== JSON.stringify(localCursor)) { 22 | awareness.setLocalStateField('cursor', updatedCursor) 23 | } 24 | } else { 25 | // Broadcast remove cursor 26 | awareness.setLocalStateField('cursor', null) 27 | } 28 | } 29 | 30 | const useCursor = (editor, awareness, cursorOptions) => { 31 | const [cursors, setCursors] = useState([]) 32 | 33 | useEffect(() => { 34 | const oldOnChange = editor.onChange 35 | 36 | editor.onChange = () => { 37 | if (!editor.isRemote) { 38 | applySlateCursor(editor, awareness, cursorOptions) 39 | } 40 | 41 | if (oldOnChange) { 42 | oldOnChange() 43 | } 44 | } 45 | 46 | awareness.on('change', () => { 47 | const localState = awareness.getLocalState() 48 | if (!localState) return // page is closing 49 | // Pull cursors from awareness 50 | setCursors( 51 | [...awareness.getStates().values()] 52 | .filter(_ => _ !== localState) 53 | .map(_ => _.cursor) 54 | .filter(_ => _) 55 | ) 56 | }) 57 | }, []) 58 | 59 | // Supply decorations to slate leaves 60 | const decorate = useCallback( 61 | ([node, path]) => { 62 | const ranges = [] 63 | 64 | if (Text.isText(node) && cursors?.length) { 65 | cursors.forEach(cursor => { 66 | if (Range.includes(cursor, path)) { 67 | const { focus, anchor, isForward } = cursor 68 | 69 | const isFocusNode = Path.equals(focus.path, path) 70 | const isAnchorNode = Path.equals(anchor.path, path) 71 | 72 | ranges.push({ 73 | ...cursor, 74 | isCaret: isFocusNode, 75 | anchor: { 76 | path, 77 | offset: isAnchorNode 78 | ? anchor.offset 79 | : isForward 80 | ? 0 81 | : node.text.length 82 | }, 83 | focus: { 84 | path, 85 | offset: isFocusNode 86 | ? focus.offset 87 | : isForward 88 | ? node.text.length 89 | : 0 90 | } 91 | }) 92 | } 93 | }) 94 | } 95 | 96 | return ranges 97 | }, 98 | [cursors] 99 | ) 100 | 101 | return { 102 | cursors, 103 | decorate 104 | } 105 | } 106 | 107 | export default useCursor 108 | -------------------------------------------------------------------------------- /client/src/services/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | /** 4 | * 5 | * @param {*} key 6 | * @param {*} initialValue 7 | * @returns {*} 8 | */ 9 | export default function useLocalStorage(key, initialValue) { 10 | // Init synchronously 11 | if (!window.localStorage.getItem(key)) { 12 | window.localStorage.setItem(key, initialValue) 13 | } 14 | 15 | const [value, setValue] = useState(window.localStorage.getItem(key)) 16 | 17 | const setValueWrapper = (value) => { 18 | window.localStorage.setItem(key, value) 19 | setValue(value) 20 | } 21 | 22 | return [value, setValueWrapper]; 23 | } 24 | -------------------------------------------------------------------------------- /client/src/services/y-websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unlike stated in the LICENSE file, it is not necessary to include the copyright notice and permission notice when you copy code from this file. 3 | */ 4 | 5 | /** 6 | * @module provider/websocket 7 | */ 8 | 9 | /* eslint-env browser */ 10 | 11 | import * as Y from 'yjs' // eslint-disable-line 12 | import * as bc from 'lib0/broadcastchannel.js' 13 | import * as time from 'lib0/time.js' 14 | import * as encoding from 'lib0/encoding.js' 15 | import * as decoding from 'lib0/decoding.js' 16 | import * as syncProtocol from 'y-protocols/sync.js' 17 | import * as authProtocol from 'y-protocols/auth.js' 18 | import * as awarenessProtocol from 'y-protocols/awareness.js' 19 | import * as mutex from 'lib0/mutex.js' 20 | import { Observable } from 'lib0/observable.js' 21 | import * as math from 'lib0/math.js' 22 | import * as url from 'lib0/url.js' 23 | import { toBase64, fromBase64 } from 'lib0/buffer.js' 24 | 25 | const messageSync = 0 26 | const messageQueryAwareness = 3 27 | const messageAwareness = 1 28 | const messageAuth = 2 29 | 30 | const reconnectTimeoutBase = 1200 31 | const maxReconnectTimeout = 2500 32 | // @todo - this should depend on awareness.outdatedTime 33 | const messageReconnectTimeout = 30000 34 | 35 | /** 36 | * @param {WebsocketProvider} provider 37 | * @param {string} reason 38 | */ 39 | const permissionDeniedHandler = (provider, reason) => console.warn(`Permission denied to access ${provider.url}.\n${reason}`) 40 | 41 | /** 42 | * @param {WebsocketProvider} provider 43 | * @param {Uint8Array} buf 44 | * @param {boolean} emitSynced 45 | * @return {encoding.Encoder} 46 | */ 47 | const readMessage = (provider, buf, emitSynced) => { 48 | const decoder = decoding.createDecoder(buf) 49 | const encoder = encoding.createEncoder() 50 | const messageType = decoding.readVarUint(decoder) 51 | switch (messageType) { 52 | case messageSync: { 53 | encoding.writeVarUint(encoder, messageSync) 54 | const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider) 55 | if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { 56 | provider.synced = true 57 | } 58 | break 59 | } 60 | case messageQueryAwareness: 61 | encoding.writeVarUint(encoder, messageAwareness) 62 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))) 63 | break 64 | case messageAwareness: 65 | awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider) 66 | break 67 | case messageAuth: 68 | authProtocol.readAuthMessage(decoder, provider.doc, permissionDeniedHandler) 69 | break 70 | default: 71 | console.error('Unable to compute message') 72 | return encoder 73 | } 74 | return encoder 75 | } 76 | 77 | /** 78 | * @param {WebsocketProvider} provider 79 | */ 80 | const setupWS = provider => { 81 | if (provider.shouldConnect && provider.ws === null) { 82 | const websocket = new provider._WS(provider.url) 83 | websocket.binaryType = 'arraybuffer' 84 | provider.ws = websocket 85 | provider.wsconnecting = true 86 | provider.wsconnected = false 87 | provider.synced = false 88 | 89 | websocket.onmessage = event => { 90 | provider.wsLastMessageReceived = time.getUnixTime() 91 | const encoder = readMessage(provider, new Uint8Array(fromBase64(event.data)), true) 92 | if (encoding.length(encoder) > 1) { 93 | websocket.send(toBase64(encoding.toUint8Array(encoder))) 94 | } 95 | } 96 | websocket.onclose = () => { 97 | provider.ws = null 98 | provider.wsconnecting = false 99 | if (provider.wsconnected) { 100 | provider.wsconnected = false 101 | provider.synced = false 102 | // update awareness (all users except local left) 103 | awarenessProtocol.removeAwarenessStates(provider.awareness, Array.from(provider.awareness.getStates().keys()).filter(client => client !== provider.doc.clientID), provider) 104 | provider.emit('status', [{ 105 | status: 'disconnected' 106 | }]) 107 | } else { 108 | provider.wsUnsuccessfulReconnects++ 109 | } 110 | // Start with no reconnect timeout and increase timeout by 111 | // log10(wsUnsuccessfulReconnects). 112 | // The idea is to increase reconnect timeout slowly and have no reconnect 113 | // timeout at the beginning (log(1) = 0) 114 | setTimeout(setupWS, math.min(math.log10(provider.wsUnsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), provider) 115 | } 116 | websocket.onopen = () => { 117 | provider.wsLastMessageReceived = time.getUnixTime() 118 | provider.wsconnecting = false 119 | provider.wsconnected = true 120 | provider.wsUnsuccessfulReconnects = 0 121 | provider.emit('status', [{ 122 | status: 'connected' 123 | }]) 124 | // always send sync step 1 when connected 125 | const encoder = encoding.createEncoder() 126 | encoding.writeVarUint(encoder, messageSync) 127 | syncProtocol.writeSyncStep1(encoder, provider.doc) 128 | websocket.send(toBase64(encoding.toUint8Array(encoder))) 129 | // broadcast local awareness state 130 | if (provider.awareness.getLocalState() !== null) { 131 | const encoderAwarenessState = encoding.createEncoder() 132 | encoding.writeVarUint(encoderAwarenessState, messageAwareness) 133 | encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [provider.doc.clientID])) 134 | websocket.send(toBase64(encoding.toUint8Array(encoderAwarenessState))) 135 | } 136 | } 137 | 138 | provider.emit('status', [{ 139 | status: 'connecting' 140 | }]) 141 | } 142 | } 143 | 144 | /** 145 | * @param {WebsocketProvider} provider 146 | * @param {ArrayBuffer} buf 147 | */ 148 | const broadcastMessage = (provider, buf) => { 149 | if (provider.wsconnected) { 150 | // @ts-ignore We know that wsconnected = true 151 | provider.ws.send(toBase64(buf)) 152 | } 153 | if (provider.bcconnected) { 154 | provider.mux(() => { 155 | bc.publish(provider.bcChannel, buf) 156 | }) 157 | } 158 | } 159 | 160 | /** 161 | * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. 162 | * The document name is attached to the provided url. I.e. the following example 163 | * creates a websocket connection to http://localhost:1234/my-document-name 164 | * 165 | * @example 166 | * import * as Y from 'yjs' 167 | * import { WebsocketProvider } from 'y-websocket' 168 | * const doc = new Y.Doc() 169 | * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) 170 | * 171 | * @extends {Observable} 172 | */ 173 | export class WebsocketProvider extends Observable { 174 | /** 175 | * @param {string} serverUrl 176 | * @param {string} roomname 177 | * @param {Y.Doc} doc 178 | * @param {object} [opts] 179 | * @param {boolean} [opts.connect] 180 | * @param {awarenessProtocol.Awareness} [opts.awareness] 181 | * @param {Object} [opts.params] 182 | * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill 183 | * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds 184 | */ 185 | constructor (serverUrl, roomname, doc, { connect = true, awareness = new awarenessProtocol.Awareness(doc), params = {}, WebSocketPolyfill = WebSocket, resyncInterval = -1 } = {}) { 186 | super() 187 | // ensure that url is always ends with / 188 | while (serverUrl[serverUrl.length - 1] === '/') { 189 | serverUrl = serverUrl.slice(0, serverUrl.length - 1) 190 | } 191 | const encodedParams = url.encodeQueryParams(params) 192 | this.bcChannel = serverUrl + '/' + roomname 193 | this.url = serverUrl + '/' + roomname + (encodedParams.length === 0 ? '' : '?' + encodedParams) 194 | this.roomname = roomname 195 | this.doc = doc 196 | this._WS = WebSocketPolyfill 197 | this.awareness = awareness 198 | this.wsconnected = false 199 | this.wsconnecting = false 200 | this.bcconnected = false 201 | this.wsUnsuccessfulReconnects = 0 202 | this.mux = mutex.createMutex() 203 | /** 204 | * @type {boolean} 205 | */ 206 | this._synced = false 207 | /** 208 | * @type {WebSocket?} 209 | */ 210 | this.ws = null 211 | this.wsLastMessageReceived = 0 212 | /** 213 | * Whether to connect to other peers or not 214 | * @type {boolean} 215 | */ 216 | this.shouldConnect = connect 217 | 218 | /** 219 | * @type {NodeJS.Timeout | number} 220 | */ 221 | this._resyncInterval = 0 222 | if (resyncInterval > 0) { 223 | this._resyncInterval = setInterval(() => { 224 | if (this.ws) { 225 | // resend sync step 1 226 | const encoder = encoding.createEncoder() 227 | encoding.writeVarUint(encoder, messageSync) 228 | syncProtocol.writeSyncStep1(encoder, doc) 229 | this.ws.send(toBase64(encoding.toUint8Array(encoder))) 230 | } 231 | }, resyncInterval) 232 | } 233 | 234 | /** 235 | * @param {ArrayBuffer} data 236 | */ 237 | this._bcSubscriber = data => { 238 | this.mux(() => { 239 | const encoder = readMessage(this, new Uint8Array(data), false) 240 | if (encoding.length(encoder) > 1) { 241 | bc.publish(this.bcChannel, encoding.toUint8Array(encoder)) 242 | } 243 | }) 244 | } 245 | /** 246 | * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) 247 | * @param {Uint8Array} update 248 | * @param {any} origin 249 | */ 250 | this._updateHandler = (update, origin) => { 251 | if (origin !== this || origin === null) { 252 | const encoder = encoding.createEncoder() 253 | encoding.writeVarUint(encoder, messageSync) 254 | syncProtocol.writeUpdate(encoder, update) 255 | broadcastMessage(this, encoding.toUint8Array(encoder)) 256 | } 257 | } 258 | this.doc.on('update', this._updateHandler) 259 | /** 260 | * @param {any} changed 261 | * @param {any} origin 262 | */ 263 | this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => { 264 | const changedClients = added.concat(updated).concat(removed) 265 | const encoder = encoding.createEncoder() 266 | encoding.writeVarUint(encoder, messageAwareness) 267 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)) 268 | broadcastMessage(this, encoding.toUint8Array(encoder)) 269 | } 270 | window.addEventListener('beforeunload', () => { 271 | awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload') 272 | }) 273 | awareness.on('update', this._awarenessUpdateHandler) 274 | this._checkInterval = setInterval(() => { 275 | if (this.wsconnected && messageReconnectTimeout < time.getUnixTime() - this.wsLastMessageReceived) { 276 | // no message received in a long time - not even your own awareness 277 | // updates (which are updated every 15 seconds) 278 | /** @type {WebSocket} */ (this.ws).close() 279 | } 280 | }, messageReconnectTimeout / 10) 281 | if (connect) { 282 | this.connect() 283 | } 284 | } 285 | 286 | /** 287 | * @type {boolean} 288 | */ 289 | get synced () { 290 | return this._synced 291 | } 292 | 293 | set synced (state) { 294 | if (this._synced !== state) { 295 | this._synced = state 296 | this.emit('synced', [state]) 297 | this.emit('sync', [state]) 298 | } 299 | } 300 | 301 | destroy () { 302 | if (this._resyncInterval !== 0) { 303 | clearInterval(/** @type {NodeJS.Timeout} */ (this._resyncInterval)) 304 | } 305 | clearInterval(this._checkInterval) 306 | this.disconnect() 307 | this.awareness.off('update', this._awarenessUpdateHandler) 308 | this.doc.off('update', this._updateHandler) 309 | super.destroy() 310 | } 311 | 312 | connectBc () { 313 | if (!this.bcconnected) { 314 | bc.subscribe(this.bcChannel, this._bcSubscriber) 315 | this.bcconnected = true 316 | } 317 | // send sync step1 to bc 318 | this.mux(() => { 319 | // write sync step 1 320 | const encoderSync = encoding.createEncoder() 321 | encoding.writeVarUint(encoderSync, messageSync) 322 | syncProtocol.writeSyncStep1(encoderSync, this.doc) 323 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync)) 324 | // broadcast local state 325 | const encoderState = encoding.createEncoder() 326 | encoding.writeVarUint(encoderState, messageSync) 327 | syncProtocol.writeSyncStep2(encoderState, this.doc) 328 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderState)) 329 | // write queryAwareness 330 | const encoderAwarenessQuery = encoding.createEncoder() 331 | encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness) 332 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery)) 333 | // broadcast local awareness state 334 | const encoderAwarenessState = encoding.createEncoder() 335 | encoding.writeVarUint(encoderAwarenessState, messageAwareness) 336 | encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID])) 337 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState)) 338 | }) 339 | } 340 | 341 | disconnectBc () { 342 | // broadcast message with local awareness state set to null (indicating disconnect) 343 | const encoder = encoding.createEncoder() 344 | encoding.writeVarUint(encoder, messageAwareness) 345 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map())) 346 | broadcastMessage(this, encoding.toUint8Array(encoder)) 347 | if (this.bcconnected) { 348 | bc.unsubscribe(this.bcChannel, this._bcSubscriber) 349 | this.bcconnected = false 350 | } 351 | } 352 | 353 | disconnect () { 354 | this.shouldConnect = false 355 | this.disconnectBc() 356 | if (this.ws !== null) { 357 | this.ws.close() 358 | } 359 | } 360 | 361 | connect () { 362 | this.shouldConnect = true 363 | if (!this.wsconnected && this.ws === null) { 364 | setupWS(this) 365 | this.connectBc() 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'es2021': true 4 | }, 5 | 'extends': [ 6 | 'plugin:react/recommended', 7 | 'plugin:react-hooks/recommended' 8 | ], 9 | 'parserOptions': { 10 | 'ecmaVersion': 12, 11 | 'sourceType': 'module', 12 | 'ecmaFeatures': { 13 | 'jsx': true 14 | }, 15 | }, 16 | 'rules': { 17 | 'indent': [ 18 | 'error', 19 | 2 20 | ], 21 | 'linebreak-style': [ 22 | 'error', 23 | 'unix' 24 | ], 25 | 'quotes': [ 26 | 'error', 27 | 'single' 28 | ], 29 | 'semi': [ 30 | 'error', 31 | 'never' 32 | ], 33 | 'react/prop-types': 0 34 | }, 35 | 'ignorePatterns': ['build'] 36 | } 37 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaberogan/y-websocket-api/53cff338dfce95e180b1b99d0fdddd72292c3e90/docs/.nojekyll -------------------------------------------------------------------------------- /docs/_snowpack/env.js: -------------------------------------------------------------------------------- 1 | export const MODE = "production"; 2 | export const NODE_ENV = "production"; 3 | export const SSR = false; -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/_commonjsHelpers-8c19dec8.js: -------------------------------------------------------------------------------- 1 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 2 | 3 | function getDefaultExportFromCjs (x) { 4 | return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; 5 | } 6 | 7 | function createCommonjsModule(fn, basedir, module) { 8 | return module = { 9 | path: basedir, 10 | exports: {}, 11 | require: function (path, base) { 12 | return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); 13 | } 14 | }, fn(module, module.exports), module.exports; 15 | } 16 | 17 | function commonjsRequire () { 18 | throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); 19 | } 20 | 21 | export { commonjsGlobal as a, createCommonjsModule as c, getDefaultExportFromCjs as g }; 22 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/binary-e1a1f68b.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | /** 4 | * Binary data constants. 5 | * 6 | * @module binary 7 | */ 8 | 9 | /** 10 | * n-th bit activated. 11 | * 12 | * @type {number} 13 | */ 14 | const BIT1 = 1; 15 | const BIT2 = 2; 16 | const BIT3 = 4; 17 | const BIT4 = 8; 18 | const BIT6 = 32; 19 | const BIT7 = 64; 20 | const BIT8 = 128; 21 | const BITS5 = 31; 22 | const BITS6 = 63; 23 | const BITS7 = 127; 24 | const BITS8 = 255; 25 | /** 26 | * @type {number} 27 | */ 28 | const BITS31 = 0x7FFFFFFF; 29 | 30 | export { BIT8 as B, BITS7 as a, BITS6 as b, BIT7 as c, BITS8 as d, BITS31 as e, BIT2 as f, BIT4 as g, BIT1 as h, BIT3 as i, BITS5 as j, BIT6 as k }; 31 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/buffer-551584fe.js: -------------------------------------------------------------------------------- 1 | import { p as process } from './process-2545f00a.js'; 2 | import { c as create } from './map-c5ea9815.js'; 3 | 4 | /** 5 | * Utility module to work with strings. 6 | * 7 | * @module string 8 | */ 9 | 10 | const fromCharCode = String.fromCharCode; 11 | 12 | /** 13 | * @param {string} s 14 | * @return {string} 15 | */ 16 | const toLowerCase = s => s.toLowerCase(); 17 | 18 | const trimLeftRegex = /^\s*/g; 19 | 20 | /** 21 | * @param {string} s 22 | * @return {string} 23 | */ 24 | const trimLeft = s => s.replace(trimLeftRegex, ''); 25 | 26 | const fromCamelCaseRegex = /([A-Z])/g; 27 | 28 | /** 29 | * @param {string} s 30 | * @param {string} separator 31 | * @return {string} 32 | */ 33 | const fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, match => `${separator}${toLowerCase(match)}`)); 34 | 35 | /* istanbul ignore next */ 36 | /** @type {TextEncoder} */ (typeof TextEncoder !== 'undefined' ? new TextEncoder() : null); 37 | 38 | /* istanbul ignore next */ 39 | let utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }); 40 | 41 | /* istanbul ignore next */ 42 | if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { 43 | // Safari doesn't handle BOM correctly. 44 | // This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called. 45 | // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and 46 | // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call 47 | // Another issue is that from then on no BOM chars are recognized anymore 48 | /* istanbul ignore next */ 49 | utf8TextDecoder = null; 50 | } 51 | 52 | /** 53 | * Often used conditions. 54 | * 55 | * @module conditions 56 | */ 57 | 58 | /** 59 | * @template T 60 | * @param {T|null|undefined} v 61 | * @return {T|null} 62 | */ 63 | /* istanbul ignore next */ 64 | const undefinedToNull = v => v === undefined ? null : v; 65 | 66 | /* global localStorage */ 67 | 68 | /** 69 | * Isomorphic variable storage. 70 | * 71 | * Uses LocalStorage in the browser and falls back to in-memory storage. 72 | * 73 | * @module storage 74 | */ 75 | 76 | /* istanbul ignore next */ 77 | class VarStoragePolyfill { 78 | constructor () { 79 | this.map = new Map(); 80 | } 81 | 82 | /** 83 | * @param {string} key 84 | * @param {any} value 85 | */ 86 | setItem (key, value) { 87 | this.map.set(key, value); 88 | } 89 | 90 | /** 91 | * @param {string} key 92 | */ 93 | getItem (key) { 94 | return this.map.get(key) 95 | } 96 | } 97 | 98 | /* istanbul ignore next */ 99 | /** 100 | * @type {any} 101 | */ 102 | let _localStorage = new VarStoragePolyfill(); 103 | 104 | try { 105 | // if the same-origin rule is violated, accessing localStorage might thrown an error 106 | /* istanbul ignore next */ 107 | if (typeof localStorage !== 'undefined') { 108 | _localStorage = localStorage; 109 | } 110 | } catch (e) { } 111 | 112 | /* istanbul ignore next */ 113 | /** 114 | * This is basically localStorage in browser, or a polyfill in nodejs 115 | */ 116 | const varStorage = _localStorage; 117 | 118 | /* istanbul ignore next */ 119 | // @ts-ignore 120 | const isNode = typeof process !== 'undefined' && process.release && /node|io\.js/.test(process.release.name); 121 | /* istanbul ignore next */ 122 | const isBrowser = typeof window !== 'undefined' && !isNode; 123 | /* istanbul ignore next */ 124 | typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false; 125 | 126 | /** 127 | * @type {Map} 128 | */ 129 | let params; 130 | 131 | /* istanbul ignore next */ 132 | const computeParams = () => { 133 | if (params === undefined) { 134 | if (isNode) { 135 | params = create(); 136 | const pargs = process.argv; 137 | let currParamName = null; 138 | /* istanbul ignore next */ 139 | for (let i = 0; i < pargs.length; i++) { 140 | const parg = pargs[i]; 141 | if (parg[0] === '-') { 142 | if (currParamName !== null) { 143 | params.set(currParamName, ''); 144 | } 145 | currParamName = parg; 146 | } else { 147 | if (currParamName !== null) { 148 | params.set(currParamName, parg); 149 | currParamName = null; 150 | } 151 | } 152 | } 153 | if (currParamName !== null) { 154 | params.set(currParamName, ''); 155 | } 156 | // in ReactNative for example this would not be true (unless connected to the Remote Debugger) 157 | } else if (typeof location === 'object') { 158 | params = create() 159 | // eslint-disable-next-line no-undef 160 | ;(location.search || '?').slice(1).split('&').forEach(kv => { 161 | if (kv.length !== 0) { 162 | const [key, value] = kv.split('='); 163 | params.set(`--${fromCamelCase(key, '-')}`, value); 164 | params.set(`-${fromCamelCase(key, '-')}`, value); 165 | } 166 | }); 167 | } else { 168 | params = create(); 169 | } 170 | } 171 | return params 172 | }; 173 | 174 | /** 175 | * @param {string} name 176 | * @return {boolean} 177 | */ 178 | /* istanbul ignore next */ 179 | const hasParam = name => computeParams().has(name); 180 | // export const getArgs = name => computeParams() && args 181 | 182 | /** 183 | * @param {string} name 184 | * @return {string|null} 185 | */ 186 | /* istanbul ignore next */ 187 | const getVariable = name => isNode ? undefinedToNull(process.env[name.toUpperCase()]) : undefinedToNull(varStorage.getItem(name)); 188 | 189 | /** 190 | * @param {string} name 191 | * @return {boolean} 192 | */ 193 | /* istanbul ignore next */ 194 | const hasConf = name => hasParam('--' + name) || getVariable(name) !== null; 195 | 196 | /* istanbul ignore next */ 197 | hasConf('production'); 198 | 199 | /** 200 | * Utility functions to work with buffers (Uint8Array). 201 | * 202 | * @module buffer 203 | */ 204 | 205 | /** 206 | * @param {number} len 207 | */ 208 | const createUint8ArrayFromLen = len => new Uint8Array(len); 209 | 210 | /** 211 | * Create Uint8Array with initial content from buffer 212 | * 213 | * @param {ArrayBuffer} buffer 214 | * @param {number} byteOffset 215 | * @param {number} length 216 | */ 217 | const createUint8ArrayViewFromArrayBuffer = (buffer, byteOffset, length) => new Uint8Array(buffer, byteOffset, length); 218 | 219 | /** 220 | * Create Uint8Array with initial content from buffer 221 | * 222 | * @param {ArrayBuffer} buffer 223 | */ 224 | const createUint8ArrayFromArrayBuffer = buffer => new Uint8Array(buffer); 225 | 226 | /* istanbul ignore next */ 227 | /** 228 | * @param {Uint8Array} bytes 229 | * @return {string} 230 | */ 231 | const toBase64Browser = bytes => { 232 | let s = ''; 233 | for (let i = 0; i < bytes.byteLength; i++) { 234 | s += fromCharCode(bytes[i]); 235 | } 236 | // eslint-disable-next-line no-undef 237 | return btoa(s) 238 | }; 239 | 240 | /** 241 | * @param {Uint8Array} bytes 242 | * @return {string} 243 | */ 244 | const toBase64Node = bytes => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64'); 245 | 246 | /* istanbul ignore next */ 247 | /** 248 | * @param {string} s 249 | * @return {Uint8Array} 250 | */ 251 | const fromBase64Browser = s => { 252 | // eslint-disable-next-line no-undef 253 | const a = atob(s); 254 | const bytes = createUint8ArrayFromLen(a.length); 255 | for (let i = 0; i < a.length; i++) { 256 | bytes[i] = a.charCodeAt(i); 257 | } 258 | return bytes 259 | }; 260 | 261 | /** 262 | * @param {string} s 263 | */ 264 | const fromBase64Node = s => { 265 | const buf = Buffer.from(s, 'base64'); 266 | return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) 267 | }; 268 | 269 | /* istanbul ignore next */ 270 | const toBase64 = isBrowser ? toBase64Browser : toBase64Node; 271 | 272 | /* istanbul ignore next */ 273 | const fromBase64 = isBrowser ? fromBase64Browser : fromBase64Node; 274 | 275 | /** 276 | * Copy the content of an Uint8Array view to a new ArrayBuffer. 277 | * 278 | * @param {Uint8Array} uint8Array 279 | * @return {Uint8Array} 280 | */ 281 | const copyUint8Array = uint8Array => { 282 | const newBuf = createUint8ArrayFromLen(uint8Array.byteLength); 283 | newBuf.set(uint8Array); 284 | return newBuf 285 | }; 286 | 287 | export { createUint8ArrayViewFromArrayBuffer as a, copyUint8Array as b, createUint8ArrayFromArrayBuffer as c, fromBase64 as f, isNode as i, toBase64 as t, varStorage as v }; 288 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/function-debeb549.js: -------------------------------------------------------------------------------- 1 | import { l as length, h as hasProperty } from './object-034d355c.js'; 2 | 3 | /** 4 | * Common functions and function call helpers. 5 | * 6 | * @module function 7 | */ 8 | 9 | /** 10 | * Calls all functions in `fs` with args. Only throws after all functions were called. 11 | * 12 | * @param {Array} fs 13 | * @param {Array} args 14 | */ 15 | const callAll = (fs, args, i = 0) => { 16 | try { 17 | for (; i < fs.length; i++) { 18 | fs[i](...args); 19 | } 20 | } finally { 21 | if (i < fs.length) { 22 | callAll(fs, args, i + 1); 23 | } 24 | } 25 | }; 26 | 27 | /** 28 | * @template T 29 | * 30 | * @param {T} a 31 | * @param {T} b 32 | * @return {boolean} 33 | */ 34 | const equalityStrict = (a, b) => a === b; 35 | 36 | /** 37 | * @param {any} a 38 | * @param {any} b 39 | * @return {boolean} 40 | */ 41 | const equalityDeep = (a, b) => { 42 | if (a == null || b == null) { 43 | return equalityStrict(a, b) 44 | } 45 | if (a.constructor !== b.constructor) { 46 | return false 47 | } 48 | if (a === b) { 49 | return true 50 | } 51 | switch (a.constructor) { 52 | case ArrayBuffer: 53 | a = new Uint8Array(a); 54 | b = new Uint8Array(b); 55 | // eslint-disable-next-line no-fallthrough 56 | case Uint8Array: { 57 | if (a.byteLength !== b.byteLength) { 58 | return false 59 | } 60 | for (let i = 0; i < a.length; i++) { 61 | if (a[i] !== b[i]) { 62 | return false 63 | } 64 | } 65 | break 66 | } 67 | case Set: { 68 | if (a.size !== b.size) { 69 | return false 70 | } 71 | for (const value of a) { 72 | if (!b.has(value)) { 73 | return false 74 | } 75 | } 76 | break 77 | } 78 | case Map: { 79 | if (a.size !== b.size) { 80 | return false 81 | } 82 | for (const key of a.keys()) { 83 | if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { 84 | return false 85 | } 86 | } 87 | break 88 | } 89 | case Object: 90 | if (length(a) !== length(b)) { 91 | return false 92 | } 93 | for (const key in a) { 94 | if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) { 95 | return false 96 | } 97 | } 98 | break 99 | case Array: 100 | if (a.length !== b.length) { 101 | return false 102 | } 103 | for (let i = 0; i < a.length; i++) { 104 | if (!equalityDeep(a[i], b[i])) { 105 | return false 106 | } 107 | } 108 | break 109 | default: 110 | return false 111 | } 112 | return true 113 | }; 114 | 115 | export { callAll as c, equalityDeep as e }; 116 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/index-57a74e37.js: -------------------------------------------------------------------------------- 1 | import { c as createCommonjsModule } from './_commonjsHelpers-8c19dec8.js'; 2 | 3 | /* 4 | object-assign 5 | (c) Sindre Sorhus 6 | @license MIT 7 | */ 8 | /* eslint-disable no-unused-vars */ 9 | var getOwnPropertySymbols = Object.getOwnPropertySymbols; 10 | var hasOwnProperty = Object.prototype.hasOwnProperty; 11 | var propIsEnumerable = Object.prototype.propertyIsEnumerable; 12 | 13 | function toObject(val) { 14 | if (val === null || val === undefined) { 15 | throw new TypeError('Object.assign cannot be called with null or undefined'); 16 | } 17 | 18 | return Object(val); 19 | } 20 | 21 | function shouldUseNative() { 22 | try { 23 | if (!Object.assign) { 24 | return false; 25 | } 26 | 27 | // Detect buggy property enumeration order in older V8 versions. 28 | 29 | // https://bugs.chromium.org/p/v8/issues/detail?id=4118 30 | var test1 = new String('abc'); // eslint-disable-line no-new-wrappers 31 | test1[5] = 'de'; 32 | if (Object.getOwnPropertyNames(test1)[0] === '5') { 33 | return false; 34 | } 35 | 36 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 37 | var test2 = {}; 38 | for (var i = 0; i < 10; i++) { 39 | test2['_' + String.fromCharCode(i)] = i; 40 | } 41 | var order2 = Object.getOwnPropertyNames(test2).map(function (n) { 42 | return test2[n]; 43 | }); 44 | if (order2.join('') !== '0123456789') { 45 | return false; 46 | } 47 | 48 | // https://bugs.chromium.org/p/v8/issues/detail?id=3056 49 | var test3 = {}; 50 | 'abcdefghijklmnopqrst'.split('').forEach(function (letter) { 51 | test3[letter] = letter; 52 | }); 53 | if (Object.keys(Object.assign({}, test3)).join('') !== 54 | 'abcdefghijklmnopqrst') { 55 | return false; 56 | } 57 | 58 | return true; 59 | } catch (err) { 60 | // We don't expect any of the above to throw, but better to be safe. 61 | return false; 62 | } 63 | } 64 | 65 | var objectAssign = shouldUseNative() ? Object.assign : function (target, source) { 66 | var from; 67 | var to = toObject(target); 68 | var symbols; 69 | 70 | for (var s = 1; s < arguments.length; s++) { 71 | from = Object(arguments[s]); 72 | 73 | for (var key in from) { 74 | if (hasOwnProperty.call(from, key)) { 75 | to[key] = from[key]; 76 | } 77 | } 78 | 79 | if (getOwnPropertySymbols) { 80 | symbols = getOwnPropertySymbols(from); 81 | for (var i = 0; i < symbols.length; i++) { 82 | if (propIsEnumerable.call(from, symbols[i])) { 83 | to[symbols[i]] = from[symbols[i]]; 84 | } 85 | } 86 | } 87 | } 88 | 89 | return to; 90 | }; 91 | 92 | var react_production_min = createCommonjsModule(function (module, exports) { 93 | var n=60103,p=60106;exports.Fragment=60107;exports.StrictMode=60108;exports.Profiler=60114;var q=60109,r=60110,t=60112;exports.Suspense=60113;var u=60115,v=60116; 94 | if("function"===typeof Symbol&&Symbol.for){var w=Symbol.for;n=w("react.element");p=w("react.portal");exports.Fragment=w("react.fragment");exports.StrictMode=w("react.strict_mode");exports.Profiler=w("react.profiler");q=w("react.provider");r=w("react.context");t=w("react.forward_ref");exports.Suspense=w("react.suspense");u=w("react.memo");v=w("react.lazy");}var x="function"===typeof Symbol&&Symbol.iterator; 95 | function y(a){if(null===a||"object"!==typeof a)return null;a=x&&a[x]||a["@@iterator"];return "function"===typeof a?a:null}function z(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;c} 12 | * 13 | * @function 14 | */ 15 | const create = () => new Map(); 16 | 17 | /** 18 | * Copy a Map object into a fresh Map object. 19 | * 20 | * @function 21 | * @template X,Y 22 | * @param {Map} m 23 | * @return {Map} 24 | */ 25 | const copy = m => { 26 | const r = create(); 27 | m.forEach((v, k) => { r.set(k, v); }); 28 | return r 29 | }; 30 | 31 | /** 32 | * Get map property. Create T if property is undefined and set T on map. 33 | * 34 | * ```js 35 | * const listeners = map.setIfUndefined(events, 'eventName', set.create) 36 | * listeners.add(listener) 37 | * ``` 38 | * 39 | * @function 40 | * @template T,K 41 | * @param {Map} map 42 | * @param {K} key 43 | * @param {function():T} createT 44 | * @return {T} 45 | */ 46 | const setIfUndefined = (map, key, createT) => { 47 | let set = map.get(key); 48 | if (set === undefined) { 49 | map.set(key, set = createT()); 50 | } 51 | return set 52 | }; 53 | 54 | /** 55 | * Creates an Array and populates it with the content of all key-value pairs using the `f(value, key)` function. 56 | * 57 | * @function 58 | * @template K 59 | * @template V 60 | * @template R 61 | * @param {Map} m 62 | * @param {function(V,K):R} f 63 | * @return {Array} 64 | */ 65 | const map = (m, f) => { 66 | const res = []; 67 | for (const [key, value] of m) { 68 | res.push(f(value, key)); 69 | } 70 | return res 71 | }; 72 | 73 | /** 74 | * Tests whether any key-value pairs pass the test implemented by `f(value, key)`. 75 | * 76 | * @todo should rename to some - similarly to Array.some 77 | * 78 | * @function 79 | * @template K 80 | * @template V 81 | * @param {Map} m 82 | * @param {function(V,K):boolean} f 83 | * @return {boolean} 84 | */ 85 | const any = (m, f) => { 86 | for (const [key, value] of m) { 87 | if (f(value, key)) { 88 | return true 89 | } 90 | } 91 | return false 92 | }; 93 | 94 | export { copy as a, any as b, create as c, map as m, setIfUndefined as s }; 95 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/math-91bb74dc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common Math expressions. 3 | * 4 | * @module math 5 | */ 6 | 7 | const floor = Math.floor; 8 | const ceil = Math.ceil; 9 | const abs = Math.abs; 10 | const imul = Math.imul; 11 | const round = Math.round; 12 | const log10 = Math.log10; 13 | const log2 = Math.log2; 14 | const log = Math.log; 15 | const sqrt = Math.sqrt; 16 | 17 | /** 18 | * @function 19 | * @param {number} a 20 | * @param {number} b 21 | * @return {number} The sum of a and b 22 | */ 23 | const add = (a, b) => a + b; 24 | 25 | /** 26 | * @function 27 | * @param {number} a 28 | * @param {number} b 29 | * @return {number} The smaller element of a and b 30 | */ 31 | const min = (a, b) => a < b ? a : b; 32 | 33 | /** 34 | * @function 35 | * @param {number} a 36 | * @param {number} b 37 | * @return {number} The bigger element of a and b 38 | */ 39 | const max = (a, b) => a > b ? a : b; 40 | 41 | const isNaN = Number.isNaN; 42 | 43 | const pow = Math.pow; 44 | /** 45 | * Base 10 exponential function. Returns the value of 10 raised to the power of pow. 46 | * 47 | * @param {number} exp 48 | * @return {number} 49 | */ 50 | const exp10 = exp => Math.pow(10, exp); 51 | 52 | const sign = Math.sign; 53 | 54 | /** 55 | * @param {number} n 56 | * @return {boolean} Wether n is negative. This function also differentiates between -0 and +0 57 | */ 58 | const isNegativeZero = n => n !== 0 ? n < 0 : 1 / n < 0; 59 | 60 | export { max as a, abs as b, ceil as c, imul as d, log2 as e, floor as f, log as g, add as h, isNegativeZero as i, isNaN as j, exp10 as k, log10 as l, min as m, sign as n, pow as p, round as r, sqrt as s }; 61 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/object-034d355c.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions for working with EcmaScript objects. 3 | * 4 | * @module object 5 | */ 6 | 7 | /** 8 | * @param {Object} obj 9 | */ 10 | const keys = Object.keys; 11 | 12 | /** 13 | * @template R 14 | * @param {Object} obj 15 | * @param {function(any,string):R} f 16 | * @return {Array} 17 | */ 18 | const map = (obj, f) => { 19 | const results = []; 20 | for (const key in obj) { 21 | results.push(f(obj[key], key)); 22 | } 23 | return results 24 | }; 25 | 26 | /** 27 | * @param {Object} obj 28 | * @return {number} 29 | */ 30 | const length = obj => keys(obj).length; 31 | 32 | /** 33 | * @param {Object} obj 34 | * @param {function(any,string):boolean} f 35 | * @return {boolean} 36 | */ 37 | const every = (obj, f) => { 38 | for (const key in obj) { 39 | if (!f(obj[key], key)) { 40 | return false 41 | } 42 | } 43 | return true 44 | }; 45 | 46 | /** 47 | * Calls `Object.prototype.hasOwnProperty`. 48 | * 49 | * @param {any} obj 50 | * @param {string|symbol} key 51 | * @return {boolean} 52 | */ 53 | const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); 54 | 55 | /** 56 | * @param {Object} a 57 | * @param {Object} b 58 | * @return {boolean} 59 | */ 60 | const equalFlat = (a, b) => a === b || (length(a) === length(b) && every(a, (val, key) => (val !== undefined || hasProperty(b, key)) && b[key] === val)); 61 | 62 | export { equalFlat as e, hasProperty as h, length as l, map as m }; 63 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/observable-363df4ab.js: -------------------------------------------------------------------------------- 1 | import { c as create$1, s as setIfUndefined } from './map-c5ea9815.js'; 2 | 3 | /** 4 | * Utility module to work with sets. 5 | * 6 | * @module set 7 | */ 8 | 9 | const create = () => new Set(); 10 | 11 | /** 12 | * Utility module to work with Arrays. 13 | * 14 | * @module array 15 | */ 16 | 17 | /** 18 | * Return the last element of an array. The element must exist 19 | * 20 | * @template L 21 | * @param {Array} arr 22 | * @return {L} 23 | */ 24 | const last = arr => arr[arr.length - 1]; 25 | 26 | /** 27 | * Append elements from src to dest 28 | * 29 | * @template M 30 | * @param {Array} dest 31 | * @param {Array} src 32 | */ 33 | const appendTo = (dest, src) => { 34 | for (let i = 0; i < src.length; i++) { 35 | dest.push(src[i]); 36 | } 37 | }; 38 | 39 | /** 40 | * Transforms something array-like to an actual Array. 41 | * 42 | * @function 43 | * @template T 44 | * @param {ArrayLike|Iterable} arraylike 45 | * @return {T} 46 | */ 47 | const from = Array.from; 48 | 49 | /** 50 | * Observable class prototype. 51 | * 52 | * @module observable 53 | */ 54 | 55 | /** 56 | * Handles named events. 57 | * 58 | * @template N 59 | */ 60 | class Observable { 61 | constructor () { 62 | /** 63 | * Some desc. 64 | * @type {Map} 65 | */ 66 | this._observers = create$1(); 67 | } 68 | 69 | /** 70 | * @param {N} name 71 | * @param {function} f 72 | */ 73 | on (name, f) { 74 | setIfUndefined(this._observers, name, create).add(f); 75 | } 76 | 77 | /** 78 | * @param {N} name 79 | * @param {function} f 80 | */ 81 | once (name, f) { 82 | /** 83 | * @param {...any} args 84 | */ 85 | const _f = (...args) => { 86 | this.off(name, _f); 87 | f(...args); 88 | }; 89 | this.on(name, _f); 90 | } 91 | 92 | /** 93 | * @param {N} name 94 | * @param {function} f 95 | */ 96 | off (name, f) { 97 | const observers = this._observers.get(name); 98 | if (observers !== undefined) { 99 | observers.delete(f); 100 | if (observers.size === 0) { 101 | this._observers.delete(name); 102 | } 103 | } 104 | } 105 | 106 | /** 107 | * Emit a named event. All registered event listeners that listen to the 108 | * specified name will receive the event. 109 | * 110 | * @todo This should catch exceptions 111 | * 112 | * @param {N} name The event name. 113 | * @param {Array} args The arguments that are applied to the event listener. 114 | */ 115 | emit (name, args) { 116 | // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called. 117 | return from((this._observers.get(name) || create$1()).values()).forEach(f => f(...args)) 118 | } 119 | 120 | destroy () { 121 | this._observers = create$1(); 122 | } 123 | } 124 | 125 | export { Observable as O, appendTo as a, create as c, from as f, last as l }; 126 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/process-2545f00a.js: -------------------------------------------------------------------------------- 1 | /* SNOWPACK PROCESS POLYFILL (based on https://github.com/calvinmetcalf/node-process-es6) */ 2 | function defaultSetTimout() { 3 | throw new Error('setTimeout has not been defined'); 4 | } 5 | function defaultClearTimeout () { 6 | throw new Error('clearTimeout has not been defined'); 7 | } 8 | var cachedSetTimeout = defaultSetTimout; 9 | var cachedClearTimeout = defaultClearTimeout; 10 | var globalContext; 11 | if (typeof window !== 'undefined') { 12 | globalContext = window; 13 | } else if (typeof self !== 'undefined') { 14 | globalContext = self; 15 | } else { 16 | globalContext = {}; 17 | } 18 | if (typeof globalContext.setTimeout === 'function') { 19 | cachedSetTimeout = setTimeout; 20 | } 21 | if (typeof globalContext.clearTimeout === 'function') { 22 | cachedClearTimeout = clearTimeout; 23 | } 24 | 25 | function runTimeout(fun) { 26 | if (cachedSetTimeout === setTimeout) { 27 | //normal enviroments in sane situations 28 | return setTimeout(fun, 0); 29 | } 30 | // if setTimeout wasn't available but was latter defined 31 | if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { 32 | cachedSetTimeout = setTimeout; 33 | return setTimeout(fun, 0); 34 | } 35 | try { 36 | // when when somebody has screwed with setTimeout but no I.E. maddness 37 | return cachedSetTimeout(fun, 0); 38 | } catch(e){ 39 | try { 40 | // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally 41 | return cachedSetTimeout.call(null, fun, 0); 42 | } catch(e){ 43 | // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error 44 | return cachedSetTimeout.call(this, fun, 0); 45 | } 46 | } 47 | 48 | 49 | } 50 | function runClearTimeout(marker) { 51 | if (cachedClearTimeout === clearTimeout) { 52 | //normal enviroments in sane situations 53 | return clearTimeout(marker); 54 | } 55 | // if clearTimeout wasn't available but was latter defined 56 | if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { 57 | cachedClearTimeout = clearTimeout; 58 | return clearTimeout(marker); 59 | } 60 | try { 61 | // when when somebody has screwed with setTimeout but no I.E. maddness 62 | return cachedClearTimeout(marker); 63 | } catch (e){ 64 | try { 65 | // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally 66 | return cachedClearTimeout.call(null, marker); 67 | } catch (e){ 68 | // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. 69 | // Some versions of I.E. have different rules for clearTimeout vs setTimeout 70 | return cachedClearTimeout.call(this, marker); 71 | } 72 | } 73 | 74 | 75 | 76 | } 77 | var queue = []; 78 | var draining = false; 79 | var currentQueue; 80 | var queueIndex = -1; 81 | 82 | function cleanUpNextTick() { 83 | if (!draining || !currentQueue) { 84 | return; 85 | } 86 | draining = false; 87 | if (currentQueue.length) { 88 | queue = currentQueue.concat(queue); 89 | } else { 90 | queueIndex = -1; 91 | } 92 | if (queue.length) { 93 | drainQueue(); 94 | } 95 | } 96 | 97 | function drainQueue() { 98 | if (draining) { 99 | return; 100 | } 101 | var timeout = runTimeout(cleanUpNextTick); 102 | draining = true; 103 | 104 | var len = queue.length; 105 | while(len) { 106 | currentQueue = queue; 107 | queue = []; 108 | while (++queueIndex < len) { 109 | if (currentQueue) { 110 | currentQueue[queueIndex].run(); 111 | } 112 | } 113 | queueIndex = -1; 114 | len = queue.length; 115 | } 116 | currentQueue = null; 117 | draining = false; 118 | runClearTimeout(timeout); 119 | } 120 | function nextTick(fun) { 121 | var args = new Array(arguments.length - 1); 122 | if (arguments.length > 1) { 123 | for (var i = 1; i < arguments.length; i++) { 124 | args[i - 1] = arguments[i]; 125 | } 126 | } 127 | queue.push(new Item(fun, args)); 128 | if (queue.length === 1 && !draining) { 129 | runTimeout(drainQueue); 130 | } 131 | } 132 | // v8 likes predictible objects 133 | function Item(fun, array) { 134 | this.fun = fun; 135 | this.array = array; 136 | } 137 | Item.prototype.run = function () { 138 | this.fun.apply(null, this.array); 139 | }; 140 | var title = 'browser'; 141 | var platform = 'browser'; 142 | var browser = true; 143 | var argv = []; 144 | var version = ''; // empty string to avoid regexp issues 145 | var versions = {}; 146 | var release = {}; 147 | var config = {}; 148 | 149 | function noop() {} 150 | 151 | var on = noop; 152 | var addListener = noop; 153 | var once = noop; 154 | var off = noop; 155 | var removeListener = noop; 156 | var removeAllListeners = noop; 157 | var emit = noop; 158 | 159 | function binding(name) { 160 | throw new Error('process.binding is not supported'); 161 | } 162 | 163 | function cwd () { return '/' } 164 | function chdir (dir) { 165 | throw new Error('process.chdir is not supported'); 166 | }function umask() { return 0; } 167 | 168 | // from https://github.com/kumavis/browser-process-hrtime/blob/master/index.js 169 | var performance = globalContext.performance || {}; 170 | var performanceNow = 171 | performance.now || 172 | performance.mozNow || 173 | performance.msNow || 174 | performance.oNow || 175 | performance.webkitNow || 176 | function(){ return (new Date()).getTime() }; 177 | 178 | // generate timestamp or delta 179 | // see http://nodejs.org/api/process.html#process_process_hrtime 180 | function hrtime(previousTimestamp){ 181 | var clocktime = performanceNow.call(performance)*1e-3; 182 | var seconds = Math.floor(clocktime); 183 | var nanoseconds = Math.floor((clocktime%1)*1e9); 184 | if (previousTimestamp) { 185 | seconds = seconds - previousTimestamp[0]; 186 | nanoseconds = nanoseconds - previousTimestamp[1]; 187 | if (nanoseconds<0) { 188 | seconds--; 189 | nanoseconds += 1e9; 190 | } 191 | } 192 | return [seconds,nanoseconds] 193 | } 194 | 195 | var startTime = new Date(); 196 | function uptime() { 197 | var currentTime = new Date(); 198 | var dif = currentTime - startTime; 199 | return dif / 1000; 200 | } 201 | 202 | var process = { 203 | nextTick: nextTick, 204 | title: title, 205 | browser: browser, 206 | env: {"NODE_ENV":"production"}, 207 | argv: argv, 208 | version: version, 209 | versions: versions, 210 | on: on, 211 | addListener: addListener, 212 | once: once, 213 | off: off, 214 | removeListener: removeListener, 215 | removeAllListeners: removeAllListeners, 216 | emit: emit, 217 | binding: binding, 218 | cwd: cwd, 219 | chdir: chdir, 220 | umask: umask, 221 | hrtime: hrtime, 222 | platform: platform, 223 | release: release, 224 | config: config, 225 | uptime: uptime 226 | }; 227 | 228 | export { process as p }; 229 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/common/time-c2bb43f3.js: -------------------------------------------------------------------------------- 1 | import { r as round, k as exp10, l as log10, f as floor } from './math-91bb74dc.js'; 2 | 3 | /** 4 | * Utility module to convert metric values. 5 | * 6 | * @module metric 7 | */ 8 | 9 | const prefixUp = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; 10 | const prefixDown = ['', 'm', 'μ', 'n', 'p', 'f', 'a', 'z', 'y']; 11 | 12 | /** 13 | * Calculate the metric prefix for a number. Assumes E.g. `prefix(1000) = { n: 1, prefix: 'k' }` 14 | * 15 | * @param {number} n 16 | * @param {number} [baseMultiplier] Multiplier of the base (10^(3*baseMultiplier)). E.g. `convert(time, -3)` if time is already in milli seconds 17 | * @return {{n:number,prefix:string}} 18 | */ 19 | const prefix = (n, baseMultiplier = 0) => { 20 | const nPow = n === 0 ? 0 : log10(n); 21 | let mult = 0; 22 | while (nPow < mult * 3 && baseMultiplier > -8) { 23 | baseMultiplier--; 24 | mult--; 25 | } 26 | while (nPow >= 3 + mult * 3 && baseMultiplier < 8) { 27 | baseMultiplier++; 28 | mult++; 29 | } 30 | const prefix = baseMultiplier < 0 ? prefixDown[-baseMultiplier] : prefixUp[baseMultiplier]; 31 | return { 32 | n: round((mult > 0 ? n / exp10(mult * 3) : n * exp10(mult * -3)) * 1e12) / 1e12, 33 | prefix 34 | } 35 | }; 36 | 37 | /** 38 | * Utility module to work with time. 39 | * 40 | * @module time 41 | */ 42 | 43 | /** 44 | * Return current time. 45 | * 46 | * @return {Date} 47 | */ 48 | const getDate = () => new Date(); 49 | 50 | /** 51 | * Return current unix time. 52 | * 53 | * @return {number} 54 | */ 55 | const getUnixTime = Date.now; 56 | 57 | /** 58 | * Transform time (in ms) to a human readable format. E.g. 1100 => 1.1s. 60s => 1min. .001 => 10μs. 59 | * 60 | * @param {number} d duration in milliseconds 61 | * @return {string} humanized approximation of time 62 | */ 63 | const humanizeDuration = d => { 64 | if (d < 60000) { 65 | const p = prefix(d, -1); 66 | return round(p.n * 100) / 100 + p.prefix + 's' 67 | } 68 | d = floor(d / 1000); 69 | const seconds = d % 60; 70 | const minutes = floor(d / 60) % 60; 71 | const hours = floor(d / 3600) % 24; 72 | const days = floor(d / 86400); 73 | if (days > 0) { 74 | return days + 'd' + ((hours > 0 || minutes > 30) ? ' ' + (minutes > 30 ? hours + 1 : hours) + 'h' : '') 75 | } 76 | if (hours > 0) { 77 | /* istanbul ignore next */ 78 | return hours + 'h' + ((minutes > 0 || seconds > 30) ? ' ' + (seconds > 30 ? minutes + 1 : minutes) + 'min' : '') 79 | } 80 | return minutes + 'min' + (seconds > 0 ? ' ' + seconds + 's' : '') 81 | }; 82 | 83 | export { getUnixTime as a, getDate as g, humanizeDuration as h }; 84 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/import-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "@emotion/css": "./@emotion/css.js", 4 | "is-hotkey": "./is-hotkey.js", 5 | "lib0/broadcastchannel.js": "./lib0/broadcastchannel.js", 6 | "lib0/buffer.js": "./lib0/buffer.js", 7 | "lib0/decoding.js": "./lib0/decoding.js", 8 | "lib0/encoding.js": "./lib0/encoding.js", 9 | "lib0/math.js": "./lib0/math.js", 10 | "lib0/mutex.js": "./lib0/mutex.js", 11 | "lib0/observable.js": "./lib0/observable.js", 12 | "lib0/time.js": "./lib0/time.js", 13 | "lib0/url.js": "./lib0/url.js", 14 | "randomcolor": "./randomcolor.js", 15 | "react": "./react.js", 16 | "react-dom": "./react-dom.js", 17 | "slate": "./slate.js", 18 | "slate-history": "./slate-history.js", 19 | "slate-react": "./slate-react.js", 20 | "slate-yjs": "./slate-yjs.js", 21 | "y-protocols/auth.js": "./y-protocols/auth.js", 22 | "y-protocols/awareness.js": "./y-protocols/awareness.js", 23 | "y-protocols/sync.js": "./y-protocols/sync.js", 24 | "yjs": "./yjs.js" 25 | } 26 | } -------------------------------------------------------------------------------- /docs/_snowpack/pkg/is-hotkey.js: -------------------------------------------------------------------------------- 1 | import { g as getDefaultExportFromCjs, c as createCommonjsModule } from './common/_commonjsHelpers-8c19dec8.js'; 2 | 3 | var lib = createCommonjsModule(function (module, exports) { 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | 9 | /** 10 | * Constants. 11 | */ 12 | 13 | var IS_MAC = typeof window != 'undefined' && /Mac|iPod|iPhone|iPad/.test(window.navigator.platform); 14 | 15 | var MODIFIERS = { 16 | alt: 'altKey', 17 | control: 'ctrlKey', 18 | meta: 'metaKey', 19 | shift: 'shiftKey' 20 | }; 21 | 22 | var ALIASES = { 23 | add: '+', 24 | break: 'pause', 25 | cmd: 'meta', 26 | command: 'meta', 27 | ctl: 'control', 28 | ctrl: 'control', 29 | del: 'delete', 30 | down: 'arrowdown', 31 | esc: 'escape', 32 | ins: 'insert', 33 | left: 'arrowleft', 34 | mod: IS_MAC ? 'meta' : 'control', 35 | opt: 'alt', 36 | option: 'alt', 37 | return: 'enter', 38 | right: 'arrowright', 39 | space: ' ', 40 | spacebar: ' ', 41 | up: 'arrowup', 42 | win: 'meta', 43 | windows: 'meta' 44 | }; 45 | 46 | var CODES = { 47 | backspace: 8, 48 | tab: 9, 49 | enter: 13, 50 | shift: 16, 51 | control: 17, 52 | alt: 18, 53 | pause: 19, 54 | capslock: 20, 55 | escape: 27, 56 | ' ': 32, 57 | pageup: 33, 58 | pagedown: 34, 59 | end: 35, 60 | home: 36, 61 | arrowleft: 37, 62 | arrowup: 38, 63 | arrowright: 39, 64 | arrowdown: 40, 65 | insert: 45, 66 | delete: 46, 67 | meta: 91, 68 | numlock: 144, 69 | scrolllock: 145, 70 | ';': 186, 71 | '=': 187, 72 | ',': 188, 73 | '-': 189, 74 | '.': 190, 75 | '/': 191, 76 | '`': 192, 77 | '[': 219, 78 | '\\': 220, 79 | ']': 221, 80 | '\'': 222 81 | }; 82 | 83 | for (var f = 1; f < 20; f++) { 84 | CODES['f' + f] = 111 + f; 85 | } 86 | 87 | /** 88 | * Is hotkey? 89 | */ 90 | 91 | function isHotkey(hotkey, options, event) { 92 | if (options && !('byKey' in options)) { 93 | event = options; 94 | options = null; 95 | } 96 | 97 | if (!Array.isArray(hotkey)) { 98 | hotkey = [hotkey]; 99 | } 100 | 101 | var array = hotkey.map(function (string) { 102 | return parseHotkey(string, options); 103 | }); 104 | var check = function check(e) { 105 | return array.some(function (object) { 106 | return compareHotkey(object, e); 107 | }); 108 | }; 109 | var ret = event == null ? check : check(event); 110 | return ret; 111 | } 112 | 113 | function isCodeHotkey(hotkey, event) { 114 | return isHotkey(hotkey, event); 115 | } 116 | 117 | function isKeyHotkey(hotkey, event) { 118 | return isHotkey(hotkey, { byKey: true }, event); 119 | } 120 | 121 | /** 122 | * Parse. 123 | */ 124 | 125 | function parseHotkey(hotkey, options) { 126 | var byKey = options && options.byKey; 127 | var ret = {}; 128 | 129 | // Special case to handle the `+` key since we use it as a separator. 130 | hotkey = hotkey.replace('++', '+add'); 131 | var values = hotkey.split('+'); 132 | var length = values.length; 133 | 134 | // Ensure that all the modifiers are set to false unless the hotkey has them. 135 | 136 | for (var k in MODIFIERS) { 137 | ret[MODIFIERS[k]] = false; 138 | } 139 | 140 | var _iteratorNormalCompletion = true; 141 | var _didIteratorError = false; 142 | var _iteratorError = undefined; 143 | 144 | try { 145 | for (var _iterator = values[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { 146 | var value = _step.value; 147 | 148 | var optional = value.endsWith('?') && value.length > 1; 149 | 150 | if (optional) { 151 | value = value.slice(0, -1); 152 | } 153 | 154 | var name = toKeyName(value); 155 | var modifier = MODIFIERS[name]; 156 | 157 | if (value.length > 1 && !modifier && !ALIASES[value] && !CODES[name]) { 158 | throw new TypeError('Unknown modifier: "' + value + '"'); 159 | } 160 | 161 | if (length === 1 || !modifier) { 162 | if (byKey) { 163 | ret.key = name; 164 | } else { 165 | ret.which = toKeyCode(value); 166 | } 167 | } 168 | 169 | if (modifier) { 170 | ret[modifier] = optional ? null : true; 171 | } 172 | } 173 | } catch (err) { 174 | _didIteratorError = true; 175 | _iteratorError = err; 176 | } finally { 177 | try { 178 | if (!_iteratorNormalCompletion && _iterator.return) { 179 | _iterator.return(); 180 | } 181 | } finally { 182 | if (_didIteratorError) { 183 | throw _iteratorError; 184 | } 185 | } 186 | } 187 | 188 | return ret; 189 | } 190 | 191 | /** 192 | * Compare. 193 | */ 194 | 195 | function compareHotkey(object, event) { 196 | for (var key in object) { 197 | var expected = object[key]; 198 | var actual = void 0; 199 | 200 | if (expected == null) { 201 | continue; 202 | } 203 | 204 | if (key === 'key' && event.key != null) { 205 | actual = event.key.toLowerCase(); 206 | } else if (key === 'which') { 207 | actual = expected === 91 && event.which === 93 ? 91 : event.which; 208 | } else { 209 | actual = event[key]; 210 | } 211 | 212 | if (actual == null && expected === false) { 213 | continue; 214 | } 215 | 216 | if (actual !== expected) { 217 | return false; 218 | } 219 | } 220 | 221 | return true; 222 | } 223 | 224 | /** 225 | * Utils. 226 | */ 227 | 228 | function toKeyCode(name) { 229 | name = toKeyName(name); 230 | var code = CODES[name] || name.toUpperCase().charCodeAt(0); 231 | return code; 232 | } 233 | 234 | function toKeyName(name) { 235 | name = name.toLowerCase(); 236 | name = ALIASES[name] || name; 237 | return name; 238 | } 239 | 240 | /** 241 | * Export. 242 | */ 243 | 244 | exports.default = isHotkey; 245 | exports.isHotkey = isHotkey; 246 | exports.isCodeHotkey = isCodeHotkey; 247 | exports.isKeyHotkey = isKeyHotkey; 248 | exports.parseHotkey = parseHotkey; 249 | exports.compareHotkey = compareHotkey; 250 | exports.toKeyCode = toKeyCode; 251 | exports.toKeyName = toKeyName; 252 | }); 253 | 254 | var __pika_web_default_export_for_treeshaking__ = /*@__PURE__*/getDefaultExportFromCjs(lib); 255 | 256 | export default __pika_web_default_export_for_treeshaking__; 257 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/broadcastchannel.js: -------------------------------------------------------------------------------- 1 | import { s as setIfUndefined } from '../common/map-c5ea9815.js'; 2 | import { f as fromBase64, v as varStorage, t as toBase64, c as createUint8ArrayFromArrayBuffer } from '../common/buffer-551584fe.js'; 3 | import '../common/process-2545f00a.js'; 4 | 5 | /* eslint-env browser */ 6 | 7 | /** 8 | * @typedef {Object} Channel 9 | * @property {Set} Channel.subs 10 | * @property {any} Channel.bc 11 | */ 12 | 13 | /** 14 | * @type {Map} 15 | */ 16 | const channels = new Map(); 17 | 18 | class LocalStoragePolyfill { 19 | /** 20 | * @param {string} room 21 | */ 22 | constructor (room) { 23 | this.room = room; 24 | /** 25 | * @type {null|function({data:ArrayBuffer}):void} 26 | */ 27 | this.onmessage = null; 28 | addEventListener('storage', e => e.key === room && this.onmessage !== null && this.onmessage({ data: fromBase64(e.newValue || '') })); 29 | } 30 | 31 | /** 32 | * @param {ArrayBuffer} buf 33 | */ 34 | postMessage (buf) { 35 | varStorage.setItem(this.room, toBase64(createUint8ArrayFromArrayBuffer(buf))); 36 | } 37 | } 38 | 39 | // Use BroadcastChannel or Polyfill 40 | const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel; 41 | 42 | /** 43 | * @param {string} room 44 | * @return {Channel} 45 | */ 46 | const getChannel = room => 47 | setIfUndefined(channels, room, () => { 48 | const subs = new Set(); 49 | const bc = new BC(room); 50 | /** 51 | * @param {{data:ArrayBuffer}} e 52 | */ 53 | bc.onmessage = e => subs.forEach(sub => sub(e.data)); 54 | return { 55 | bc, subs 56 | } 57 | }); 58 | 59 | /** 60 | * Subscribe to global `publish` events. 61 | * 62 | * @function 63 | * @param {string} room 64 | * @param {function(any):any} f 65 | */ 66 | const subscribe = (room, f) => getChannel(room).subs.add(f); 67 | 68 | /** 69 | * Unsubscribe from `publish` global events. 70 | * 71 | * @function 72 | * @param {string} room 73 | * @param {function(any):any} f 74 | */ 75 | const unsubscribe = (room, f) => getChannel(room).subs.delete(f); 76 | 77 | /** 78 | * Publish data to all subscribers (including subscribers on this tab) 79 | * 80 | * @function 81 | * @param {string} room 82 | * @param {any} data 83 | */ 84 | const publish = (room, data) => { 85 | const c = getChannel(room); 86 | c.bc.postMessage(data); 87 | c.subs.forEach(sub => sub(data)); 88 | }; 89 | 90 | export { publish, subscribe, unsubscribe }; 91 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/buffer.js: -------------------------------------------------------------------------------- 1 | export { f as fromBase64, t as toBase64 } from '../common/buffer-551584fe.js'; 2 | import '../common/process-2545f00a.js'; 3 | import '../common/map-c5ea9815.js'; 4 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/decoding.js: -------------------------------------------------------------------------------- 1 | export { D as Decoder, B as IncUintOptRleDecoder, I as IntDiffDecoder, C as IntDiffOptRleDecoder, R as RleDecoder, A as RleIntDiffDecoder, S as StringDecoder, U as UintOptRleDecoder, a as clone, c as createDecoder, h as hasContent, j as peekUint16, k as peekUint32, p as peekUint8, o as peekVarInt, t as peekVarString, n as peekVarUint, z as readAny, x as readBigInt64, y as readBigUint64, v as readFloat32, w as readFloat64, u as readFromDataView, d as readTailAsUint8Array, f as readUint16, g as readUint32, i as readUint32BigEndian, e as readUint8, r as readUint8Array, m as readVarInt, q as readVarString, l as readVarUint, b as readVarUint8Array, s as skip8 } from '../common/decoding-6e54b617.js'; 2 | import '../common/buffer-551584fe.js'; 3 | import '../common/process-2545f00a.js'; 4 | import '../common/map-c5ea9815.js'; 5 | import '../common/binary-e1a1f68b.js'; 6 | import '../common/math-91bb74dc.js'; 7 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/encoding.js: -------------------------------------------------------------------------------- 1 | export { E as Encoder, z as IncUintOptRleEncoder, I as IntDiffEncoder, A as IntDiffOptRleEncoder, R as RleEncoder, y as RleIntDiffEncoder, S as StringEncoder, U as UintOptRleEncoder, c as createEncoder, l as length, s as set, e as setUint16, h as setUint32, b as setUint8, t as toUint8Array, w as write, x as writeAny, u as writeBigInt64, v as writeBigUint64, m as writeBinaryEncoder, q as writeFloat32, r as writeFloat64, p as writeOnDataView, d as writeUint16, f as writeUint32, g as writeUint32BigEndian, a as writeUint8, n as writeUint8Array, j as writeVarInt, k as writeVarString, i as writeVarUint, o as writeVarUint8Array } from '../common/encoding-7fdf95b6.js'; 2 | import '../common/buffer-551584fe.js'; 3 | import '../common/process-2545f00a.js'; 4 | import '../common/map-c5ea9815.js'; 5 | import '../common/math-91bb74dc.js'; 6 | import '../common/binary-e1a1f68b.js'; 7 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/math.js: -------------------------------------------------------------------------------- 1 | export { b as abs, h as add, c as ceil, k as exp10, f as floor, d as imul, j as isNaN, i as isNegativeZero, g as log, l as log10, e as log2, a as max, m as min, p as pow, r as round, n as sign, s as sqrt } from '../common/math-91bb74dc.js'; 2 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/mutex.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutual exclude for JavaScript. 3 | * 4 | * @module mutex 5 | */ 6 | 7 | /** 8 | * @callback mutex 9 | * @param {function():void} cb Only executed when this mutex is not in the current stack 10 | * @param {function():void} [elseCb] Executed when this mutex is in the current stack 11 | */ 12 | 13 | /** 14 | * Creates a mutual exclude function with the following property: 15 | * 16 | * ```js 17 | * const mutex = createMutex() 18 | * mutex(() => { 19 | * // This function is immediately executed 20 | * mutex(() => { 21 | * // This function is not executed, as the mutex is already active. 22 | * }) 23 | * }) 24 | * ``` 25 | * 26 | * @return {mutex} A mutual exclude function 27 | * @public 28 | */ 29 | const createMutex = () => { 30 | let token = true; 31 | return (f, g) => { 32 | if (token) { 33 | token = false; 34 | try { 35 | f(); 36 | } finally { 37 | token = true; 38 | } 39 | } else if (g !== undefined) { 40 | g(); 41 | } 42 | } 43 | }; 44 | 45 | export { createMutex }; 46 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/observable.js: -------------------------------------------------------------------------------- 1 | export { O as Observable } from '../common/observable-363df4ab.js'; 2 | import '../common/map-c5ea9815.js'; 3 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/time.js: -------------------------------------------------------------------------------- 1 | export { g as getDate, a as getUnixTime, h as humanizeDuration } from '../common/time-c2bb43f3.js'; 2 | import '../common/math-91bb74dc.js'; 3 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/lib0/url.js: -------------------------------------------------------------------------------- 1 | import { m as map } from '../common/object-034d355c.js'; 2 | 3 | /** 4 | * Utility module to work with urls. 5 | * 6 | * @module url 7 | */ 8 | 9 | /** 10 | * Parse query parameters from an url. 11 | * 12 | * @param {string} url 13 | * @return {Object} 14 | */ 15 | const decodeQueryParams = url => { 16 | /** 17 | * @type {Object} 18 | */ 19 | const query = {}; 20 | const urlQuerySplit = url.split('?'); 21 | const pairs = urlQuerySplit[urlQuerySplit.length - 1].split('&'); 22 | for (var i = 0; i < pairs.length; i++) { 23 | const item = pairs[i]; 24 | if (item.length > 0) { 25 | const pair = item.split('='); 26 | query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ''); 27 | } 28 | } 29 | return query 30 | }; 31 | 32 | /** 33 | * @param {Object} params 34 | * @return {string} 35 | */ 36 | const encodeQueryParams = params => 37 | map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&'); 38 | 39 | export { decodeQueryParams, encodeQueryParams }; 40 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/randomcolor.js: -------------------------------------------------------------------------------- 1 | import { c as createCommonjsModule, a as commonjsGlobal } from './common/_commonjsHelpers-8c19dec8.js'; 2 | 3 | var randomColor = createCommonjsModule(function (module, exports) { 4 | (function(root, factory) { 5 | 6 | // Support CommonJS 7 | { 8 | var randomColor = factory(); 9 | 10 | // Support NodeJS & Component, which allow module.exports to be a function 11 | if ( module && module.exports) { 12 | exports = module.exports = randomColor; 13 | } 14 | 15 | // Support CommonJS 1.1.1 spec 16 | exports.randomColor = randomColor; 17 | 18 | // Support AMD 19 | } 20 | 21 | }(commonjsGlobal, function() { 22 | 23 | // Seed to get repeatable colors 24 | var seed = null; 25 | 26 | // Shared color dictionary 27 | var colorDictionary = {}; 28 | 29 | // Populate the color dictionary 30 | loadColorBounds(); 31 | 32 | // check if a range is taken 33 | var colorRanges = []; 34 | 35 | var randomColor = function (options) { 36 | 37 | options = options || {}; 38 | 39 | // Check if there is a seed and ensure it's an 40 | // integer. Otherwise, reset the seed value. 41 | if (options.seed !== undefined && options.seed !== null && options.seed === parseInt(options.seed, 10)) { 42 | seed = options.seed; 43 | 44 | // A string was passed as a seed 45 | } else if (typeof options.seed === 'string') { 46 | seed = stringToInteger(options.seed); 47 | 48 | // Something was passed as a seed but it wasn't an integer or string 49 | } else if (options.seed !== undefined && options.seed !== null) { 50 | throw new TypeError('The seed value must be an integer or string'); 51 | 52 | // No seed, reset the value outside. 53 | } else { 54 | seed = null; 55 | } 56 | 57 | var H,S,B; 58 | 59 | // Check if we need to generate multiple colors 60 | if (options.count !== null && options.count !== undefined) { 61 | 62 | var totalColors = options.count, 63 | colors = []; 64 | // Value false at index i means the range i is not taken yet. 65 | for (var i = 0; i < options.count; i++) { 66 | colorRanges.push(false); 67 | } 68 | options.count = null; 69 | 70 | while (totalColors > colors.length) { 71 | 72 | var color = randomColor(options); 73 | 74 | if (seed !== null) { 75 | options.seed = seed; 76 | } 77 | 78 | colors.push(color); 79 | } 80 | 81 | options.count = totalColors; 82 | 83 | return colors; 84 | } 85 | 86 | // First we pick a hue (H) 87 | H = pickHue(options); 88 | 89 | // Then use H to determine saturation (S) 90 | S = pickSaturation(H, options); 91 | 92 | // Then use S and H to determine brightness (B). 93 | B = pickBrightness(H, S, options); 94 | 95 | // Then we return the HSB color in the desired format 96 | return setFormat([H,S,B], options); 97 | }; 98 | 99 | function pickHue(options) { 100 | if (colorRanges.length > 0) { 101 | var hueRange = getRealHueRange(options.hue); 102 | 103 | var hue = randomWithin(hueRange); 104 | 105 | //Each of colorRanges.length ranges has a length equal approximatelly one step 106 | var step = (hueRange[1] - hueRange[0]) / colorRanges.length; 107 | 108 | var j = parseInt((hue - hueRange[0]) / step); 109 | 110 | //Check if the range j is taken 111 | if (colorRanges[j] === true) { 112 | j = (j + 2) % colorRanges.length; 113 | } 114 | else { 115 | colorRanges[j] = true; 116 | } 117 | 118 | var min = (hueRange[0] + j * step) % 359, 119 | max = (hueRange[0] + (j + 1) * step) % 359; 120 | 121 | hueRange = [min, max]; 122 | 123 | hue = randomWithin(hueRange); 124 | 125 | if (hue < 0) {hue = 360 + hue;} 126 | return hue 127 | } 128 | else { 129 | var hueRange = getHueRange(options.hue); 130 | 131 | hue = randomWithin(hueRange); 132 | // Instead of storing red as two seperate ranges, 133 | // we group them, using negative numbers 134 | if (hue < 0) { 135 | hue = 360 + hue; 136 | } 137 | 138 | return hue; 139 | } 140 | } 141 | 142 | function pickSaturation (hue, options) { 143 | 144 | if (options.hue === 'monochrome') { 145 | return 0; 146 | } 147 | 148 | if (options.luminosity === 'random') { 149 | return randomWithin([0,100]); 150 | } 151 | 152 | var saturationRange = getSaturationRange(hue); 153 | 154 | var sMin = saturationRange[0], 155 | sMax = saturationRange[1]; 156 | 157 | switch (options.luminosity) { 158 | 159 | case 'bright': 160 | sMin = 55; 161 | break; 162 | 163 | case 'dark': 164 | sMin = sMax - 10; 165 | break; 166 | 167 | case 'light': 168 | sMax = 55; 169 | break; 170 | } 171 | 172 | return randomWithin([sMin, sMax]); 173 | 174 | } 175 | 176 | function pickBrightness (H, S, options) { 177 | 178 | var bMin = getMinimumBrightness(H, S), 179 | bMax = 100; 180 | 181 | switch (options.luminosity) { 182 | 183 | case 'dark': 184 | bMax = bMin + 20; 185 | break; 186 | 187 | case 'light': 188 | bMin = (bMax + bMin)/2; 189 | break; 190 | 191 | case 'random': 192 | bMin = 0; 193 | bMax = 100; 194 | break; 195 | } 196 | 197 | return randomWithin([bMin, bMax]); 198 | } 199 | 200 | function setFormat (hsv, options) { 201 | 202 | switch (options.format) { 203 | 204 | case 'hsvArray': 205 | return hsv; 206 | 207 | case 'hslArray': 208 | return HSVtoHSL(hsv); 209 | 210 | case 'hsl': 211 | var hsl = HSVtoHSL(hsv); 212 | return 'hsl('+hsl[0]+', '+hsl[1]+'%, '+hsl[2]+'%)'; 213 | 214 | case 'hsla': 215 | var hslColor = HSVtoHSL(hsv); 216 | var alpha = options.alpha || Math.random(); 217 | return 'hsla('+hslColor[0]+', '+hslColor[1]+'%, '+hslColor[2]+'%, ' + alpha + ')'; 218 | 219 | case 'rgbArray': 220 | return HSVtoRGB(hsv); 221 | 222 | case 'rgb': 223 | var rgb = HSVtoRGB(hsv); 224 | return 'rgb(' + rgb.join(', ') + ')'; 225 | 226 | case 'rgba': 227 | var rgbColor = HSVtoRGB(hsv); 228 | var alpha = options.alpha || Math.random(); 229 | return 'rgba(' + rgbColor.join(', ') + ', ' + alpha + ')'; 230 | 231 | default: 232 | return HSVtoHex(hsv); 233 | } 234 | 235 | } 236 | 237 | function getMinimumBrightness(H, S) { 238 | 239 | var lowerBounds = getColorInfo(H).lowerBounds; 240 | 241 | for (var i = 0; i < lowerBounds.length - 1; i++) { 242 | 243 | var s1 = lowerBounds[i][0], 244 | v1 = lowerBounds[i][1]; 245 | 246 | var s2 = lowerBounds[i+1][0], 247 | v2 = lowerBounds[i+1][1]; 248 | 249 | if (S >= s1 && S <= s2) { 250 | 251 | var m = (v2 - v1)/(s2 - s1), 252 | b = v1 - m*s1; 253 | 254 | return m*S + b; 255 | } 256 | 257 | } 258 | 259 | return 0; 260 | } 261 | 262 | function getHueRange (colorInput) { 263 | 264 | if (typeof parseInt(colorInput) === 'number') { 265 | 266 | var number = parseInt(colorInput); 267 | 268 | if (number < 360 && number > 0) { 269 | return [number, number]; 270 | } 271 | 272 | } 273 | 274 | if (typeof colorInput === 'string') { 275 | 276 | if (colorDictionary[colorInput]) { 277 | var color = colorDictionary[colorInput]; 278 | if (color.hueRange) {return color.hueRange;} 279 | } else if (colorInput.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)) { 280 | var hue = HexToHSB(colorInput)[0]; 281 | return [ hue, hue ]; 282 | } 283 | } 284 | 285 | return [0,360]; 286 | 287 | } 288 | 289 | function getSaturationRange (hue) { 290 | return getColorInfo(hue).saturationRange; 291 | } 292 | 293 | function getColorInfo (hue) { 294 | 295 | // Maps red colors to make picking hue easier 296 | if (hue >= 334 && hue <= 360) { 297 | hue-= 360; 298 | } 299 | 300 | for (var colorName in colorDictionary) { 301 | var color = colorDictionary[colorName]; 302 | if (color.hueRange && 303 | hue >= color.hueRange[0] && 304 | hue <= color.hueRange[1]) { 305 | return colorDictionary[colorName]; 306 | } 307 | } return 'Color not found'; 308 | } 309 | 310 | function randomWithin (range) { 311 | if (seed === null) { 312 | //generate random evenly destinct number from : https://martin.ankerl.com/2009/12/09/how-to-create-random-colors-programmatically/ 313 | var golden_ratio = 0.618033988749895; 314 | var r=Math.random(); 315 | r += golden_ratio; 316 | r %= 1; 317 | return Math.floor(range[0] + r*(range[1] + 1 - range[0])); 318 | } else { 319 | //Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ 320 | var max = range[1] || 1; 321 | var min = range[0] || 0; 322 | seed = (seed * 9301 + 49297) % 233280; 323 | var rnd = seed / 233280.0; 324 | return Math.floor(min + rnd * (max - min)); 325 | } 326 | } 327 | 328 | function HSVtoHex (hsv){ 329 | 330 | var rgb = HSVtoRGB(hsv); 331 | 332 | function componentToHex(c) { 333 | var hex = c.toString(16); 334 | return hex.length == 1 ? '0' + hex : hex; 335 | } 336 | 337 | var hex = '#' + componentToHex(rgb[0]) + componentToHex(rgb[1]) + componentToHex(rgb[2]); 338 | 339 | return hex; 340 | 341 | } 342 | 343 | function defineColor (name, hueRange, lowerBounds) { 344 | 345 | var sMin = lowerBounds[0][0], 346 | sMax = lowerBounds[lowerBounds.length - 1][0], 347 | 348 | bMin = lowerBounds[lowerBounds.length - 1][1], 349 | bMax = lowerBounds[0][1]; 350 | 351 | colorDictionary[name] = { 352 | hueRange: hueRange, 353 | lowerBounds: lowerBounds, 354 | saturationRange: [sMin, sMax], 355 | brightnessRange: [bMin, bMax] 356 | }; 357 | 358 | } 359 | 360 | function loadColorBounds () { 361 | 362 | defineColor( 363 | 'monochrome', 364 | null, 365 | [[0,0],[100,0]] 366 | ); 367 | 368 | defineColor( 369 | 'red', 370 | [-26,18], 371 | [[20,100],[30,92],[40,89],[50,85],[60,78],[70,70],[80,60],[90,55],[100,50]] 372 | ); 373 | 374 | defineColor( 375 | 'orange', 376 | [18,46], 377 | [[20,100],[30,93],[40,88],[50,86],[60,85],[70,70],[100,70]] 378 | ); 379 | 380 | defineColor( 381 | 'yellow', 382 | [46,62], 383 | [[25,100],[40,94],[50,89],[60,86],[70,84],[80,82],[90,80],[100,75]] 384 | ); 385 | 386 | defineColor( 387 | 'green', 388 | [62,178], 389 | [[30,100],[40,90],[50,85],[60,81],[70,74],[80,64],[90,50],[100,40]] 390 | ); 391 | 392 | defineColor( 393 | 'blue', 394 | [178, 257], 395 | [[20,100],[30,86],[40,80],[50,74],[60,60],[70,52],[80,44],[90,39],[100,35]] 396 | ); 397 | 398 | defineColor( 399 | 'purple', 400 | [257, 282], 401 | [[20,100],[30,87],[40,79],[50,70],[60,65],[70,59],[80,52],[90,45],[100,42]] 402 | ); 403 | 404 | defineColor( 405 | 'pink', 406 | [282, 334], 407 | [[20,100],[30,90],[40,86],[60,84],[80,80],[90,75],[100,73]] 408 | ); 409 | 410 | } 411 | 412 | function HSVtoRGB (hsv) { 413 | 414 | // this doesn't work for the values of 0 and 360 415 | // here's the hacky fix 416 | var h = hsv[0]; 417 | if (h === 0) {h = 1;} 418 | if (h === 360) {h = 359;} 419 | 420 | // Rebase the h,s,v values 421 | h = h/360; 422 | var s = hsv[1]/100, 423 | v = hsv[2]/100; 424 | 425 | var h_i = Math.floor(h*6), 426 | f = h * 6 - h_i, 427 | p = v * (1 - s), 428 | q = v * (1 - f*s), 429 | t = v * (1 - (1 - f)*s), 430 | r = 256, 431 | g = 256, 432 | b = 256; 433 | 434 | switch(h_i) { 435 | case 0: r = v; g = t; b = p; break; 436 | case 1: r = q; g = v; b = p; break; 437 | case 2: r = p; g = v; b = t; break; 438 | case 3: r = p; g = q; b = v; break; 439 | case 4: r = t; g = p; b = v; break; 440 | case 5: r = v; g = p; b = q; break; 441 | } 442 | 443 | var result = [Math.floor(r*255), Math.floor(g*255), Math.floor(b*255)]; 444 | return result; 445 | } 446 | 447 | function HexToHSB (hex) { 448 | hex = hex.replace(/^#/, ''); 449 | hex = hex.length === 3 ? hex.replace(/(.)/g, '$1$1') : hex; 450 | 451 | var red = parseInt(hex.substr(0, 2), 16) / 255, 452 | green = parseInt(hex.substr(2, 2), 16) / 255, 453 | blue = parseInt(hex.substr(4, 2), 16) / 255; 454 | 455 | var cMax = Math.max(red, green, blue), 456 | delta = cMax - Math.min(red, green, blue), 457 | saturation = cMax ? (delta / cMax) : 0; 458 | 459 | switch (cMax) { 460 | case red: return [ 60 * (((green - blue) / delta) % 6) || 0, saturation, cMax ]; 461 | case green: return [ 60 * (((blue - red) / delta) + 2) || 0, saturation, cMax ]; 462 | case blue: return [ 60 * (((red - green) / delta) + 4) || 0, saturation, cMax ]; 463 | } 464 | } 465 | 466 | function HSVtoHSL (hsv) { 467 | var h = hsv[0], 468 | s = hsv[1]/100, 469 | v = hsv[2]/100, 470 | k = (2-s)*v; 471 | 472 | return [ 473 | h, 474 | Math.round(s*v / (k<1 ? k : 2-k) * 10000) / 100, 475 | k/2 * 100 476 | ]; 477 | } 478 | 479 | function stringToInteger (string) { 480 | var total = 0; 481 | for (var i = 0; i !== string.length; i++) { 482 | if (total >= Number.MAX_SAFE_INTEGER) break; 483 | total += string.charCodeAt(i); 484 | } 485 | return total 486 | } 487 | 488 | // get The range of given hue when options.count!=0 489 | function getRealHueRange(colorHue) 490 | { if (!isNaN(colorHue)) { 491 | var number = parseInt(colorHue); 492 | 493 | if (number < 360 && number > 0) { 494 | return getColorInfo(colorHue).hueRange 495 | } 496 | } 497 | else if (typeof colorHue === 'string') { 498 | 499 | if (colorDictionary[colorHue]) { 500 | var color = colorDictionary[colorHue]; 501 | 502 | if (color.hueRange) { 503 | return color.hueRange 504 | } 505 | } else if (colorHue.match(/^#?([0-9A-F]{3}|[0-9A-F]{6})$/i)) { 506 | var hue = HexToHSB(colorHue)[0]; 507 | return getColorInfo(hue).hueRange 508 | } 509 | } 510 | 511 | return [0,360] 512 | } 513 | return randomColor; 514 | })); 515 | }); 516 | 517 | export default randomColor; 518 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/react-dom.js: -------------------------------------------------------------------------------- 1 | import { r as reactDom } from './common/index-8dbeb7e4.js'; 2 | export { r as default } from './common/index-8dbeb7e4.js'; 3 | import './common/_commonjsHelpers-8c19dec8.js'; 4 | import './common/index-57a74e37.js'; 5 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/react.js: -------------------------------------------------------------------------------- 1 | import { r as react } from './common/index-57a74e37.js'; 2 | export { r as default } from './common/index-57a74e37.js'; 3 | import './common/_commonjsHelpers-8c19dec8.js'; 4 | 5 | 6 | 7 | var useCallback = react.useCallback; 8 | var useEffect = react.useEffect; 9 | var useMemo = react.useMemo; 10 | var useState = react.useState; 11 | export { useCallback, useEffect, useMemo, useState }; 12 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/slate-history.js: -------------------------------------------------------------------------------- 1 | import { E as Editor, O as Operation, P as Path } from './common/index.es-71fa96c1.js'; 2 | import './common/_commonjsHelpers-8c19dec8.js'; 3 | import './common/process-2545f00a.js'; 4 | 5 | /*! 6 | * isobject 7 | * 8 | * Copyright (c) 2014-2017, Jon Schlinkert. 9 | * Released under the MIT License. 10 | */ 11 | 12 | function isObject(val) { 13 | return val != null && typeof val === 'object' && Array.isArray(val) === false; 14 | } 15 | 16 | /*! 17 | * is-plain-object 18 | * 19 | * Copyright (c) 2014-2017, Jon Schlinkert. 20 | * Released under the MIT License. 21 | */ 22 | 23 | function isObjectObject(o) { 24 | return isObject(o) === true 25 | && Object.prototype.toString.call(o) === '[object Object]'; 26 | } 27 | 28 | function isPlainObject(o) { 29 | var ctor,prot; 30 | 31 | if (isObjectObject(o) === false) return false; 32 | 33 | // If has modified constructor 34 | ctor = o.constructor; 35 | if (typeof ctor !== 'function') return false; 36 | 37 | // If has modified prototype 38 | prot = ctor.prototype; 39 | if (isObjectObject(prot) === false) return false; 40 | 41 | // If constructor does not have an Object-specific method 42 | if (prot.hasOwnProperty('isPrototypeOf') === false) { 43 | return false; 44 | } 45 | 46 | // Most likely a plain Object 47 | return true; 48 | } 49 | 50 | var History = { 51 | /** 52 | * Check if a value is a `History` object. 53 | */ 54 | isHistory(value) { 55 | return isPlainObject(value) && Array.isArray(value.redos) && Array.isArray(value.undos) && (value.redos.length === 0 || Operation.isOperationList(value.redos[0])) && (value.undos.length === 0 || Operation.isOperationList(value.undos[0])); 56 | } 57 | 58 | }; 59 | var SAVING = new WeakMap(); 60 | var MERGING = new WeakMap(); 61 | var HistoryEditor = { 62 | /** 63 | * Check if a value is a `HistoryEditor` object. 64 | */ 65 | isHistoryEditor(value) { 66 | return Editor.isEditor(value) && History.isHistory(value.history); 67 | }, 68 | 69 | /** 70 | * Get the merge flag's current value. 71 | */ 72 | isMerging(editor) { 73 | return MERGING.get(editor); 74 | }, 75 | 76 | /** 77 | * Get the saving flag's current value. 78 | */ 79 | isSaving(editor) { 80 | return SAVING.get(editor); 81 | }, 82 | 83 | /** 84 | * Redo to the previous saved state. 85 | */ 86 | redo(editor) { 87 | editor.redo(); 88 | }, 89 | 90 | /** 91 | * Undo to the previous saved state. 92 | */ 93 | undo(editor) { 94 | editor.undo(); 95 | }, 96 | 97 | /** 98 | * Apply a series of changes inside a synchronous `fn`, without merging any of 99 | * the new operations into previous save point in the history. 100 | */ 101 | withoutMerging(editor, fn) { 102 | var prev = HistoryEditor.isMerging(editor); 103 | MERGING.set(editor, false); 104 | fn(); 105 | MERGING.set(editor, prev); 106 | }, 107 | 108 | /** 109 | * Apply a series of changes inside a synchronous `fn`, without saving any of 110 | * their operations into the history. 111 | */ 112 | withoutSaving(editor, fn) { 113 | var prev = HistoryEditor.isSaving(editor); 114 | SAVING.set(editor, false); 115 | fn(); 116 | SAVING.set(editor, prev); 117 | } 118 | 119 | }; 120 | 121 | /** 122 | * The `withHistory` plugin keeps track of the operation history of a Slate 123 | * editor as operations are applied to it, using undo and redo stacks. 124 | */ 125 | 126 | var withHistory = editor => { 127 | var e = editor; 128 | var { 129 | apply 130 | } = e; 131 | e.history = { 132 | undos: [], 133 | redos: [] 134 | }; 135 | 136 | e.redo = () => { 137 | var { 138 | history 139 | } = e; 140 | var { 141 | redos 142 | } = history; 143 | 144 | if (redos.length > 0) { 145 | var batch = redos[redos.length - 1]; 146 | HistoryEditor.withoutSaving(e, () => { 147 | Editor.withoutNormalizing(e, () => { 148 | for (var op of batch) { 149 | e.apply(op); 150 | } 151 | }); 152 | }); 153 | history.redos.pop(); 154 | history.undos.push(batch); 155 | } 156 | }; 157 | 158 | e.undo = () => { 159 | var { 160 | history 161 | } = e; 162 | var { 163 | undos 164 | } = history; 165 | 166 | if (undos.length > 0) { 167 | var batch = undos[undos.length - 1]; 168 | HistoryEditor.withoutSaving(e, () => { 169 | Editor.withoutNormalizing(e, () => { 170 | var inverseOps = batch.map(Operation.inverse).reverse(); 171 | 172 | for (var op of inverseOps) { 173 | // If the final operation is deselecting the editor, skip it. This is 174 | if (op === inverseOps[inverseOps.length - 1] && op.type === 'set_selection' && op.newProperties == null) { 175 | continue; 176 | } else { 177 | e.apply(op); 178 | } 179 | } 180 | }); 181 | }); 182 | history.redos.push(batch); 183 | history.undos.pop(); 184 | } 185 | }; 186 | 187 | e.apply = op => { 188 | var { 189 | operations, 190 | history 191 | } = e; 192 | var { 193 | undos 194 | } = history; 195 | var lastBatch = undos[undos.length - 1]; 196 | var lastOp = lastBatch && lastBatch[lastBatch.length - 1]; 197 | var overwrite = shouldOverwrite(op, lastOp); 198 | var save = HistoryEditor.isSaving(e); 199 | var merge = HistoryEditor.isMerging(e); 200 | 201 | if (save == null) { 202 | save = shouldSave(op); 203 | } 204 | 205 | if (save) { 206 | if (merge == null) { 207 | if (lastBatch == null) { 208 | merge = false; 209 | } else if (operations.length !== 0) { 210 | merge = true; 211 | } else { 212 | merge = shouldMerge(op, lastOp) || overwrite; 213 | } 214 | } 215 | 216 | if (lastBatch && merge) { 217 | if (overwrite) { 218 | lastBatch.pop(); 219 | } 220 | 221 | lastBatch.push(op); 222 | } else { 223 | var batch = [op]; 224 | undos.push(batch); 225 | } 226 | 227 | while (undos.length > 100) { 228 | undos.shift(); 229 | } 230 | 231 | if (shouldClear(op)) { 232 | history.redos = []; 233 | } 234 | } 235 | 236 | apply(op); 237 | }; 238 | 239 | return e; 240 | }; 241 | /** 242 | * Check whether to merge an operation into the previous operation. 243 | */ 244 | 245 | var shouldMerge = (op, prev) => { 246 | if (op.type === 'set_selection') { 247 | return true; 248 | } 249 | 250 | if (prev && op.type === 'insert_text' && prev.type === 'insert_text' && op.offset === prev.offset + prev.text.length && Path.equals(op.path, prev.path)) { 251 | return true; 252 | } 253 | 254 | if (prev && op.type === 'remove_text' && prev.type === 'remove_text' && op.offset + op.text.length === prev.offset && Path.equals(op.path, prev.path)) { 255 | return true; 256 | } 257 | 258 | return false; 259 | }; 260 | /** 261 | * Check whether an operation needs to be saved to the history. 262 | */ 263 | 264 | 265 | var shouldSave = (op, prev) => { 266 | if (op.type === 'set_selection' && op.newProperties == null) { 267 | return false; 268 | } 269 | 270 | return true; 271 | }; 272 | /** 273 | * Check whether an operation should overwrite the previous one. 274 | */ 275 | 276 | 277 | var shouldOverwrite = (op, prev) => { 278 | if (prev && op.type === 'set_selection' && prev.type === 'set_selection') { 279 | return true; 280 | } 281 | 282 | return false; 283 | }; 284 | /** 285 | * Check whether an operation should clear the redos stack. 286 | */ 287 | 288 | 289 | var shouldClear = op => { 290 | if (op.type === 'set_selection') { 291 | return false; 292 | } 293 | 294 | return true; 295 | }; 296 | 297 | export { withHistory }; 298 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/slate.js: -------------------------------------------------------------------------------- 1 | export { E as Editor, a as Element, P as Path, R as Range, b as Text, T as Transforms, c as createEditor } from './common/index.es-71fa96c1.js'; 2 | import './common/_commonjsHelpers-8c19dec8.js'; 3 | import './common/process-2545f00a.js'; 4 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/y-protocols/auth.js: -------------------------------------------------------------------------------- 1 | import { i as writeVarUint, k as writeVarString } from '../common/encoding-7fdf95b6.js'; 2 | import { l as readVarUint, q as readVarString } from '../common/decoding-6e54b617.js'; 3 | import '../common/buffer-551584fe.js'; 4 | import '../common/process-2545f00a.js'; 5 | import '../common/map-c5ea9815.js'; 6 | import '../common/math-91bb74dc.js'; 7 | import '../common/binary-e1a1f68b.js'; 8 | 9 | const messagePermissionDenied = 0; 10 | 11 | /** 12 | * @param {encoding.Encoder} encoder 13 | * @param {string} reason 14 | */ 15 | const writePermissionDenied = (encoder, reason) => { 16 | writeVarUint(encoder, messagePermissionDenied); 17 | writeVarString(encoder, reason); 18 | }; 19 | 20 | /** 21 | * @callback PermissionDeniedHandler 22 | * @param {any} y 23 | * @param {string} reason 24 | */ 25 | 26 | /** 27 | * 28 | * @param {decoding.Decoder} decoder 29 | * @param {Y.Doc} y 30 | * @param {PermissionDeniedHandler} permissionDeniedHandler 31 | */ 32 | const readAuthMessage = (decoder, y, permissionDeniedHandler) => { 33 | switch (readVarUint(decoder)) { 34 | case messagePermissionDenied: permissionDeniedHandler(y, readVarString(decoder)); 35 | } 36 | }; 37 | 38 | export { messagePermissionDenied, readAuthMessage, writePermissionDenied }; 39 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/y-protocols/awareness.js: -------------------------------------------------------------------------------- 1 | import { i as writeVarUint, k as writeVarString, t as toUint8Array, c as createEncoder } from '../common/encoding-7fdf95b6.js'; 2 | import { l as readVarUint, q as readVarString, c as createDecoder } from '../common/decoding-6e54b617.js'; 3 | import { a as getUnixTime } from '../common/time-c2bb43f3.js'; 4 | import { f as floor } from '../common/math-91bb74dc.js'; 5 | import { O as Observable } from '../common/observable-363df4ab.js'; 6 | import { e as equalityDeep } from '../common/function-debeb549.js'; 7 | import '../common/buffer-551584fe.js'; 8 | import '../common/process-2545f00a.js'; 9 | import '../common/map-c5ea9815.js'; 10 | import '../common/binary-e1a1f68b.js'; 11 | import '../common/object-034d355c.js'; 12 | 13 | /** 14 | * @module awareness-protocol 15 | */ 16 | 17 | const outdatedTimeout = 30000; 18 | 19 | /** 20 | * @typedef {Object} MetaClientState 21 | * @property {number} MetaClientState.clock 22 | * @property {number} MetaClientState.lastUpdated unix timestamp 23 | */ 24 | 25 | /** 26 | * The Awareness class implements a simple shared state protocol that can be used for non-persistent data like awareness information 27 | * (cursor, username, status, ..). Each client can update its own local state and listen to state changes of 28 | * remote clients. Every client may set a state of a remote peer to `null` to mark the client as offline. 29 | * 30 | * Each client is identified by a unique client id (something we borrow from `doc.clientID`). A client can override 31 | * its own state by propagating a message with an increasing timestamp (`clock`). If such a message is received, it is 32 | * applied if the known state of that client is older than the new state (`clock < newClock`). If a client thinks that 33 | * a remote client is offline, it may propagate a message with 34 | * `{ clock: currentClientClock, state: null, client: remoteClient }`. If such a 35 | * message is received, and the known clock of that client equals the received clock, it will override the state with `null`. 36 | * 37 | * Before a client disconnects, it should propagate a `null` state with an updated clock. 38 | * 39 | * Awareness states must be updated every 30 seconds. Otherwise the Awareness instance will delete the client state. 40 | * 41 | * @extends {Observable} 42 | */ 43 | class Awareness extends Observable { 44 | /** 45 | * @param {Y.Doc} doc 46 | */ 47 | constructor (doc) { 48 | super(); 49 | this.doc = doc; 50 | /** 51 | * @type {number} 52 | */ 53 | this.clientID = doc.clientID; 54 | /** 55 | * Maps from client id to client state 56 | * @type {Map>} 57 | */ 58 | this.states = new Map(); 59 | /** 60 | * @type {Map} 61 | */ 62 | this.meta = new Map(); 63 | this._checkInterval = setInterval(() => { 64 | const now = getUnixTime(); 65 | if (this.getLocalState() !== null && (outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(this.clientID)).lastUpdated)) { 66 | // renew local clock 67 | this.setLocalState(this.getLocalState()); 68 | } 69 | /** 70 | * @type {Array} 71 | */ 72 | const remove = []; 73 | this.meta.forEach((meta, clientid) => { 74 | if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) { 75 | remove.push(clientid); 76 | } 77 | }); 78 | if (remove.length > 0) { 79 | removeAwarenessStates(this, remove, 'timeout'); 80 | } 81 | }, floor(outdatedTimeout / 10)); 82 | doc.on('destroy', () => { 83 | this.destroy(); 84 | }); 85 | this.setLocalState({}); 86 | } 87 | 88 | destroy () { 89 | this.emit('destroy', [this]); 90 | this.setLocalState(null); 91 | super.destroy(); 92 | clearInterval(this._checkInterval); 93 | } 94 | 95 | /** 96 | * @return {Object|null} 97 | */ 98 | getLocalState () { 99 | return this.states.get(this.clientID) || null 100 | } 101 | 102 | /** 103 | * @param {Object|null} state 104 | */ 105 | setLocalState (state) { 106 | const clientID = this.clientID; 107 | const currLocalMeta = this.meta.get(clientID); 108 | const clock = currLocalMeta === undefined ? 0 : currLocalMeta.clock + 1; 109 | const prevState = this.states.get(clientID); 110 | if (state === null) { 111 | this.states.delete(clientID); 112 | } else { 113 | this.states.set(clientID, state); 114 | } 115 | this.meta.set(clientID, { 116 | clock, 117 | lastUpdated: getUnixTime() 118 | }); 119 | const added = []; 120 | const updated = []; 121 | const filteredUpdated = []; 122 | const removed = []; 123 | if (state === null) { 124 | removed.push(clientID); 125 | } else if (prevState == null) { 126 | if (state != null) { 127 | added.push(clientID); 128 | } 129 | } else { 130 | updated.push(clientID); 131 | if (!equalityDeep(prevState, state)) { 132 | filteredUpdated.push(clientID); 133 | } 134 | } 135 | if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { 136 | this.emit('change', [{ added, updated: filteredUpdated, removed }, 'local']); 137 | } 138 | this.emit('update', [{ added, updated, removed }, 'local']); 139 | } 140 | 141 | /** 142 | * @param {string} field 143 | * @param {any} value 144 | */ 145 | setLocalStateField (field, value) { 146 | const state = this.getLocalState(); 147 | if (state !== null) { 148 | state[field] = value; 149 | this.setLocalState(state); 150 | } 151 | } 152 | 153 | /** 154 | * @return {Map>} 155 | */ 156 | getStates () { 157 | return this.states 158 | } 159 | } 160 | 161 | /** 162 | * Mark (remote) clients as inactive and remove them from the list of active peers. 163 | * This change will be propagated to remote clients. 164 | * 165 | * @param {Awareness} awareness 166 | * @param {Array} clients 167 | * @param {any} origin 168 | */ 169 | const removeAwarenessStates = (awareness, clients, origin) => { 170 | const removed = []; 171 | for (let i = 0; i < clients.length; i++) { 172 | const clientID = clients[i]; 173 | if (awareness.states.has(clientID)) { 174 | awareness.states.delete(clientID); 175 | if (clientID === awareness.clientID) { 176 | const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID)); 177 | awareness.meta.set(clientID, { 178 | clock: curMeta.clock + 1, 179 | lastUpdated: getUnixTime() 180 | }); 181 | } 182 | removed.push(clientID); 183 | } 184 | } 185 | if (removed.length > 0) { 186 | awareness.emit('change', [{ added: [], updated: [], removed }, origin]); 187 | awareness.emit('update', [{ added: [], updated: [], removed }, origin]); 188 | } 189 | }; 190 | 191 | /** 192 | * @param {Awareness} awareness 193 | * @param {Array} clients 194 | * @return {Uint8Array} 195 | */ 196 | const encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => { 197 | const len = clients.length; 198 | const encoder = createEncoder(); 199 | writeVarUint(encoder, len); 200 | for (let i = 0; i < len; i++) { 201 | const clientID = clients[i]; 202 | const state = states.get(clientID) || null; 203 | const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock; 204 | writeVarUint(encoder, clientID); 205 | writeVarUint(encoder, clock); 206 | writeVarString(encoder, JSON.stringify(state)); 207 | } 208 | return toUint8Array(encoder) 209 | }; 210 | 211 | /** 212 | * Modify the content of an awareness update before re-encoding it to an awareness update. 213 | * 214 | * This might be useful when you have a central server that wants to ensure that clients 215 | * cant hijack somebody elses identity. 216 | * 217 | * @param {Uint8Array} update 218 | * @param {function(any):any} modify 219 | * @return {Uint8Array} 220 | */ 221 | const modifyAwarenessUpdate = (update, modify) => { 222 | const decoder = createDecoder(update); 223 | const encoder = createEncoder(); 224 | const len = readVarUint(decoder); 225 | writeVarUint(encoder, len); 226 | for (let i = 0; i < len; i++) { 227 | const clientID = readVarUint(decoder); 228 | const clock = readVarUint(decoder); 229 | const state = JSON.parse(readVarString(decoder)); 230 | const modifiedState = modify(state); 231 | writeVarUint(encoder, clientID); 232 | writeVarUint(encoder, clock); 233 | writeVarString(encoder, JSON.stringify(modifiedState)); 234 | } 235 | return toUint8Array(encoder) 236 | }; 237 | 238 | /** 239 | * @param {Awareness} awareness 240 | * @param {Uint8Array} update 241 | * @param {any} origin This will be added to the emitted change event 242 | */ 243 | const applyAwarenessUpdate = (awareness, update, origin) => { 244 | const decoder = createDecoder(update); 245 | const timestamp = getUnixTime(); 246 | const added = []; 247 | const updated = []; 248 | const filteredUpdated = []; 249 | const removed = []; 250 | const len = readVarUint(decoder); 251 | for (let i = 0; i < len; i++) { 252 | const clientID = readVarUint(decoder); 253 | let clock = readVarUint(decoder); 254 | const state = JSON.parse(readVarString(decoder)); 255 | const clientMeta = awareness.meta.get(clientID); 256 | const prevState = awareness.states.get(clientID); 257 | const currClock = clientMeta === undefined ? 0 : clientMeta.clock; 258 | if (currClock < clock || (currClock === clock && state === null && awareness.states.has(clientID))) { 259 | if (state === null) { 260 | // never let a remote client remove this local state 261 | if (clientID === awareness.clientID && awareness.getLocalState() != null) { 262 | // remote client removed the local state. Do not remote state. Broadcast a message indicating 263 | // that this client still exists by increasing the clock 264 | clock++; 265 | } else { 266 | awareness.states.delete(clientID); 267 | } 268 | } else { 269 | awareness.states.set(clientID, state); 270 | } 271 | awareness.meta.set(clientID, { 272 | clock, 273 | lastUpdated: timestamp 274 | }); 275 | if (clientMeta === undefined && state !== null) { 276 | added.push(clientID); 277 | } else if (clientMeta !== undefined && state === null) { 278 | removed.push(clientID); 279 | } else if (state !== null) { 280 | if (!equalityDeep(state, prevState)) { 281 | filteredUpdated.push(clientID); 282 | } 283 | updated.push(clientID); 284 | } 285 | } 286 | } 287 | if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) { 288 | awareness.emit('change', [{ 289 | added, updated: filteredUpdated, removed 290 | }, origin]); 291 | } 292 | if (added.length > 0 || updated.length > 0 || removed.length > 0) { 293 | awareness.emit('update', [{ 294 | added, updated, removed 295 | }, origin]); 296 | } 297 | }; 298 | 299 | export { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, outdatedTimeout, removeAwarenessStates }; 300 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/y-protocols/sync.js: -------------------------------------------------------------------------------- 1 | import { i as writeVarUint, o as writeVarUint8Array } from '../common/encoding-7fdf95b6.js'; 2 | import { b as readVarUint8Array, l as readVarUint } from '../common/decoding-6e54b617.js'; 3 | import { f as encodeStateVector, g as encodeStateAsUpdate, h as applyUpdate } from '../common/yjs-95ac26e6.js'; 4 | import '../common/buffer-551584fe.js'; 5 | import '../common/process-2545f00a.js'; 6 | import '../common/map-c5ea9815.js'; 7 | import '../common/math-91bb74dc.js'; 8 | import '../common/binary-e1a1f68b.js'; 9 | import '../common/observable-363df4ab.js'; 10 | import '../common/function-debeb549.js'; 11 | import '../common/object-034d355c.js'; 12 | import '../common/time-c2bb43f3.js'; 13 | 14 | /** 15 | * @module sync-protocol 16 | */ 17 | 18 | /** 19 | * @typedef {Map} StateMap 20 | */ 21 | 22 | /** 23 | * Core Yjs defines two message types: 24 | * • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2. 25 | * • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it 26 | * received all information from the remote client. 27 | * 28 | * In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection 29 | * with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both 30 | * SyncStep2 and SyncDone, it is assured that it is synced to the remote client. 31 | * 32 | * In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1. 33 | * When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies 34 | * with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the 35 | * client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can 36 | * easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them. 37 | * Therefore it is necesarry that the client initiates the sync. 38 | * 39 | * Construction of a message: 40 | * [messageType : varUint, message definition..] 41 | * 42 | * Note: A message does not include information about the room name. This must to be handled by the upper layer protocol! 43 | * 44 | * stringify[messageType] stringifies a message definition (messageType is already read from the bufffer) 45 | */ 46 | 47 | const messageYjsSyncStep1 = 0; 48 | const messageYjsSyncStep2 = 1; 49 | const messageYjsUpdate = 2; 50 | 51 | /** 52 | * Create a sync step 1 message based on the state of the current shared document. 53 | * 54 | * @param {encoding.Encoder} encoder 55 | * @param {Y.Doc} doc 56 | */ 57 | const writeSyncStep1 = (encoder, doc) => { 58 | writeVarUint(encoder, messageYjsSyncStep1); 59 | const sv = encodeStateVector(doc); 60 | writeVarUint8Array(encoder, sv); 61 | }; 62 | 63 | /** 64 | * @param {encoding.Encoder} encoder 65 | * @param {Y.Doc} doc 66 | * @param {Uint8Array} [encodedStateVector] 67 | */ 68 | const writeSyncStep2 = (encoder, doc, encodedStateVector) => { 69 | writeVarUint(encoder, messageYjsSyncStep2); 70 | writeVarUint8Array(encoder, encodeStateAsUpdate(doc, encodedStateVector)); 71 | }; 72 | 73 | /** 74 | * Read SyncStep1 message and reply with SyncStep2. 75 | * 76 | * @param {decoding.Decoder} decoder The reply to the received message 77 | * @param {encoding.Encoder} encoder The received message 78 | * @param {Y.Doc} doc 79 | */ 80 | const readSyncStep1 = (decoder, encoder, doc) => 81 | writeSyncStep2(encoder, doc, readVarUint8Array(decoder)); 82 | 83 | /** 84 | * Read and apply Structs and then DeleteStore to a y instance. 85 | * 86 | * @param {decoding.Decoder} decoder 87 | * @param {Y.Doc} doc 88 | * @param {any} transactionOrigin 89 | */ 90 | const readSyncStep2 = (decoder, doc, transactionOrigin) => { 91 | applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin); 92 | }; 93 | 94 | /** 95 | * @param {encoding.Encoder} encoder 96 | * @param {Uint8Array} update 97 | */ 98 | const writeUpdate = (encoder, update) => { 99 | writeVarUint(encoder, messageYjsUpdate); 100 | writeVarUint8Array(encoder, update); 101 | }; 102 | 103 | /** 104 | * Read and apply Structs and then DeleteStore to a y instance. 105 | * 106 | * @param {decoding.Decoder} decoder 107 | * @param {Y.Doc} doc 108 | * @param {any} transactionOrigin 109 | */ 110 | const readUpdate = readSyncStep2; 111 | 112 | /** 113 | * @param {decoding.Decoder} decoder A message received from another client 114 | * @param {encoding.Encoder} encoder The reply message. Will not be sent if empty. 115 | * @param {Y.Doc} doc 116 | * @param {any} transactionOrigin 117 | */ 118 | const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => { 119 | const messageType = readVarUint(decoder); 120 | switch (messageType) { 121 | case messageYjsSyncStep1: 122 | readSyncStep1(decoder, encoder, doc); 123 | break 124 | case messageYjsSyncStep2: 125 | readSyncStep2(decoder, doc, transactionOrigin); 126 | break 127 | case messageYjsUpdate: 128 | readUpdate(decoder, doc, transactionOrigin); 129 | break 130 | default: 131 | throw new Error('Unknown message type') 132 | } 133 | return messageType 134 | }; 135 | 136 | export { messageYjsSyncStep1, messageYjsSyncStep2, messageYjsUpdate, readSyncMessage, readSyncStep1, readSyncStep2, readUpdate, writeSyncStep1, writeSyncStep2, writeUpdate }; 137 | -------------------------------------------------------------------------------- /docs/_snowpack/pkg/yjs.js: -------------------------------------------------------------------------------- 1 | export { A as AbstractConnector, i as AbstractStruct, j as AbstractType, Y as Array, k as ContentAny, l as ContentBinary, m as ContentDeleted, n as ContentEmbed, o as ContentFormat, p as ContentJSON, C as ContentString, q as ContentType, D as Doc, G as GC, I as ID, r as Item, a as Map, P as PermanentUserData, R as RelativePosition, S as Snapshot, b as Text, T as Transaction, U as UndoManager, s as XmlElement, t as XmlFragment, u as XmlHook, v as XmlText, c as YArrayEvent, w as YEvent, d as YMapEvent, e as YTextEvent, x as YXmlEvent, h as applyUpdate, y as applyUpdateV2, z as compareIDs, B as compareRelativePositions, E as createAbsolutePositionFromRelativePosition, F as createDeleteSet, H as createDeleteSetFromStructStore, J as createDocFromSnapshot, K as createID, L as createRelativePositionFromJSON, M as createRelativePositionFromTypeIndex, N as createSnapshot, O as decodeSnapshot, Q as decodeSnapshotV2, V as decodeStateVector, W as decodeStateVectorV2, X as emptySnapshot, Z as encodeSnapshot, _ as encodeSnapshotV2, g as encodeStateAsUpdate, $ as encodeStateAsUpdateV2, f as encodeStateVector, a0 as encodeStateVectorV2, a1 as equalSnapshots, a2 as findRootTypeKey, a3 as getItem, a4 as getState, a5 as getTypeChildren, a6 as isDeleted, a7 as isParentOf, a8 as iterateDeletedStructs, a9 as logType, aa as readRelativePosition, ab as readUpdate, ac as readUpdateV2, ad as snapshot, ae as transact, af as tryGc, ag as typeListToArraySnapshot, ah as typeMapGetSnapshot, ai as writeRelativePosition } from './common/yjs-95ac26e6.js'; 2 | import './common/observable-363df4ab.js'; 3 | import './common/map-c5ea9815.js'; 4 | import './common/math-91bb74dc.js'; 5 | import './common/encoding-7fdf95b6.js'; 6 | import './common/buffer-551584fe.js'; 7 | import './common/process-2545f00a.js'; 8 | import './common/binary-e1a1f68b.js'; 9 | import './common/decoding-6e54b617.js'; 10 | import './common/function-debeb549.js'; 11 | import './common/object-034d355c.js'; 12 | import './common/time-c2bb43f3.js'; 13 | -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | input, 3 | textarea { 4 | font-family: 'Roboto', sans-serif; 5 | line-height: 1.4; 6 | background: #eee; 7 | } 8 | 9 | body { 10 | margin: 0; 11 | } 12 | 13 | p { 14 | margin: 0; 15 | } 16 | 17 | pre { 18 | padding: 10px; 19 | background-color: #eee; 20 | white-space: pre-wrap; 21 | } 22 | 23 | :not(pre) > code { 24 | font-family: monospace; 25 | background-color: #eee; 26 | padding: 3px; 27 | } 28 | 29 | img { 30 | max-width: 100%; 31 | max-height: 20em; 32 | } 33 | 34 | blockquote { 35 | border-left: 2px solid #ddd; 36 | margin-left: 0; 37 | margin-right: 0; 38 | padding-left: 10px; 39 | color: #aaa; 40 | font-style: italic; 41 | } 42 | 43 | blockquote[dir='rtl'] { 44 | border-left: none; 45 | padding-left: 0; 46 | padding-right: 10px; 47 | border-right: 2px solid #ddd; 48 | } 49 | 50 | table { 51 | border-collapse: collapse; 52 | } 53 | 54 | td { 55 | padding: 10px; 56 | border: 2px solid #ddd; 57 | } 58 | 59 | input { 60 | box-sizing: border-box; 61 | font-size: 0.85em; 62 | width: 100%; 63 | padding: 0.5em; 64 | border: 2px solid #ddd; 65 | background: #fafafa; 66 | } 67 | 68 | input:focus { 69 | outline: 0; 70 | border-color: blue; 71 | } 72 | 73 | [data-slate-editor] > * + * { 74 | margin-top: 1em; 75 | } 76 | 77 | /* Quill Background */ 78 | .ql-toolbar, #editor-container { 79 | background: white; 80 | } 81 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | y-websocket-api demo 11 | 12 | 13 |
    14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "snowpack dev", 4 | "build": "snowpack build", 5 | "test": "echo \"This template does not include a test runner by default.\" && exit 1" 6 | }, 7 | "devDependencies": { 8 | "eslint": "^7.17.0", 9 | "eslint-plugin-react": "^7.22.0", 10 | "eslint-plugin-react-hooks": "^4.2.0", 11 | "snowpack": "^3.0.11" 12 | }, 13 | "dependencies": { 14 | "@emotion/css": "^11.1.3", 15 | "is-hotkey": "^0.2.0", 16 | "lib0": "^0.2.35", 17 | "quill": "^1.3.7", 18 | "quill-cursors": "^3.0.1", 19 | "randomcolor": "^0.6.2", 20 | "react": "^17.0.1", 21 | "react-dom": "^17.0.1", 22 | "slate": "^0.59.0", 23 | "slate-history": "^0.59.0", 24 | "slate-react": "^0.59.0", 25 | "slate-yjs": "^1.0.0", 26 | "y-protocols": "^1.0.2", 27 | "y-quill": "^0.1.4", 28 | "yjs": "^13.4.9" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/snowpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | packageOptions: { 3 | // source: 'remote', // TODO y-protocols fails :( submit PR? 4 | }, 5 | buildOptions: { 6 | out: '../docs', 7 | baseUrl: '/y-websocket-api' 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /docs/src/components/ConnectionForm.js: -------------------------------------------------------------------------------- 1 | import React from "../../_snowpack/pkg/react.js"; 2 | import {css} from "../../_snowpack/pkg/@emotion/css.js"; 3 | import useLocalStorage from "../services/useLocalStorage.js"; 4 | const inputGroup = css` 5 | display: flex; 6 | gap: 10px; 7 | `; 8 | const submitBtn = css` 9 | background: #2185d0; 10 | white-space: nowrap; 11 | color: white; 12 | font-weight: bold; 13 | border: none; 14 | padding: 10px 15px; 15 | font-size: 14px; 16 | cursor: pointer; 17 | `; 18 | export default () => { 19 | const [storedValue, setStoredValue] = useLocalStorage("document", `doc-${Math.round(Math.random() * 1e4)}`); 20 | return /* @__PURE__ */ React.createElement("div", { 21 | className: inputGroup 22 | }, /* @__PURE__ */ React.createElement("input", { 23 | type: "text", 24 | name: "Document", 25 | value: storedValue, 26 | onChange: (e) => setStoredValue(e.target.value) 27 | }), /* @__PURE__ */ React.createElement("div", { 28 | className: submitBtn, 29 | onClick: () => location.reload() 30 | }, "Update Document")); 31 | }; 32 | -------------------------------------------------------------------------------- /docs/src/components/SlateEditor/components.js: -------------------------------------------------------------------------------- 1 | import React from "../../../_snowpack/pkg/react.js"; 2 | import ReactDOM from "../../../_snowpack/pkg/react-dom.js"; 3 | import {cx, css} from "../../../_snowpack/pkg/@emotion/css.js"; 4 | export const Button = React.forwardRef(({className, active, reversed, ...props}, ref) => /* @__PURE__ */ React.createElement("span", { 5 | ...props, 6 | ref, 7 | className: cx(className, css` 8 | cursor: pointer; 9 | color: ${reversed ? active ? "white" : "#aaa" : active ? "black" : "#ccc"}; 10 | `) 11 | })); 12 | export const EditorValue = React.forwardRef(({className, value, ...props}, ref) => { 13 | const textLines = value.document.nodes.map((node) => node.text).toArray().join("\n"); 14 | return /* @__PURE__ */ React.createElement("div", { 15 | ref, 16 | ...props, 17 | className: cx(className, css` 18 | margin: 30px -20px 0; 19 | `) 20 | }, /* @__PURE__ */ React.createElement("div", { 21 | className: css` 22 | font-size: 14px; 23 | padding: 5px 20px; 24 | color: #404040; 25 | border-top: 2px solid #eeeeee; 26 | background: #f8f8f8; 27 | ` 28 | }, "Slate's value as text"), /* @__PURE__ */ React.createElement("div", { 29 | className: css` 30 | color: #404040; 31 | font: 12px monospace; 32 | white-space: pre-wrap; 33 | padding: 10px 20px; 34 | div { 35 | margin: 0 0 0.5em; 36 | } 37 | ` 38 | }, textLines)); 39 | }); 40 | export const Icon = React.forwardRef(({className, ...props}, ref) => /* @__PURE__ */ React.createElement("span", { 41 | ...props, 42 | ref, 43 | className: cx("material-icons", className, css` 44 | font-size: 18px; 45 | vertical-align: text-bottom; 46 | `) 47 | })); 48 | export const Instruction = React.forwardRef(({className, ...props}, ref) => /* @__PURE__ */ React.createElement("div", { 49 | ...props, 50 | ref, 51 | className: cx(className, css` 52 | white-space: pre-wrap; 53 | margin: 0 -20px 10px; 54 | padding: 10px 20px; 55 | font-size: 14px; 56 | background: #f8f8e8; 57 | `) 58 | })); 59 | export const Menu = React.forwardRef(({className, ...props}, ref) => /* @__PURE__ */ React.createElement("div", { 60 | ...props, 61 | ref, 62 | className: cx(className, css` 63 | & > * { 64 | display: inline-block; 65 | } 66 | 67 | & > * + * { 68 | margin-left: 15px; 69 | } 70 | `) 71 | })); 72 | export const Portal = ({children}) => { 73 | return ReactDOM.createPortal(children, document.body); 74 | }; 75 | export const Toolbar = React.forwardRef(({className, ...props}, ref) => /* @__PURE__ */ React.createElement(Menu, { 76 | ...props, 77 | ref, 78 | className: cx(className, css` 79 | position: relative; 80 | padding: 1px 18px 17px; 81 | margin: 0 -20px; 82 | border-bottom: 2px solid #eee; 83 | margin-bottom: 20px; 84 | `) 85 | })); 86 | -------------------------------------------------------------------------------- /docs/src/components/SlateEditor/index.js: -------------------------------------------------------------------------------- 1 | import React, {useCallback, useMemo, useState, useEffect} from "../../../_snowpack/pkg/react.js"; 2 | import isHotkey from "../../../_snowpack/pkg/is-hotkey.js"; 3 | import {Editable, withReact, useSlate, Slate} from "../../../_snowpack/pkg/slate-react.js"; 4 | import { 5 | Editor, 6 | Transforms, 7 | createEditor, 8 | Element as SlateElement 9 | } from "../../../_snowpack/pkg/slate.js"; 10 | import {withHistory} from "../../../_snowpack/pkg/slate-history.js"; 11 | import * as Y from "../../../_snowpack/pkg/yjs.js"; 12 | import {withYjs, toSharedType} from "../../../_snowpack/pkg/slate-yjs.js"; 13 | import randomColor from "../../../_snowpack/pkg/randomcolor.js"; 14 | import {WebsocketProvider} from "../../services/y-websocket.js"; 15 | import {cx, css} from "../../../_snowpack/pkg/@emotion/css.js"; 16 | import {Button, Icon, Toolbar} from "./components.js"; 17 | import {YJS_ENDPOINT} from "../../services/state.js"; 18 | import useCursor from "../../services/useCursor.js"; 19 | import useLocalStorage from "../../services/useLocalStorage.js"; 20 | const HOTKEYS = { 21 | "mod+b": "bold", 22 | "mod+i": "italic", 23 | "mod+u": "underline", 24 | "mod+`": "code" 25 | }; 26 | const LIST_TYPES = ["numbered-list", "bulleted-list"]; 27 | const SlateEditor = () => { 28 | const [value, setValue] = useState([]); 29 | const [editable, setEditable] = useState(false); 30 | const [storedValue] = useLocalStorage("document", `doc-${Math.round(Math.random() * 1e4)}-fallback`); 31 | const [sharedType, provider] = useMemo(() => { 32 | const doc = new Y.Doc(); 33 | const sharedType2 = doc.getArray("content"); 34 | const provider2 = new WebsocketProvider(YJS_ENDPOINT, `?doc=${storedValue}`, doc); 35 | return [sharedType2, provider2]; 36 | }, []); 37 | const editor = useMemo(() => { 38 | const editor2 = withYjs(withReact(withHistory(createEditor())), sharedType); 39 | return editor2; 40 | }, []); 41 | const color = useMemo(() => randomColor({ 42 | luminosity: "dark", 43 | format: "rgba", 44 | alpha: 1 45 | }), []); 46 | const cursorOptions = { 47 | name: `User ${Math.round(Math.random() * 1e3)}`, 48 | color, 49 | alphaColor: color.slice(0, -2) + "0.2)" 50 | }; 51 | const {decorate} = useCursor(editor, provider.awareness, cursorOptions); 52 | const renderElement = useCallback((props) => /* @__PURE__ */ React.createElement(Element, { 53 | ...props 54 | }), []); 55 | const renderLeaf = useCallback((props) => /* @__PURE__ */ React.createElement(Leaf, { 56 | ...props 57 | }), [decorate]); 58 | useEffect(() => { 59 | provider.on("status", ({status}) => { 60 | setEditable(true); 61 | }); 62 | provider.on("sync", (isSynced) => { 63 | if (isSynced && sharedType.length === 0) { 64 | toSharedType(sharedType, [ 65 | {type: "paragraph", children: [{text: ""}]} 66 | ]); 67 | } 68 | }); 69 | return () => { 70 | provider.disconnect(); 71 | }; 72 | }, []); 73 | return /* @__PURE__ */ React.createElement(ExampleContent, null, /* @__PURE__ */ React.createElement(Slate, { 74 | editor, 75 | value, 76 | onChange: (value2) => setValue(value2) 77 | }, /* @__PURE__ */ React.createElement(Toolbar, null, /* @__PURE__ */ React.createElement(MarkButton, { 78 | format: "bold", 79 | icon: "format_bold" 80 | }), /* @__PURE__ */ React.createElement(MarkButton, { 81 | format: "italic", 82 | icon: "format_italic" 83 | }), /* @__PURE__ */ React.createElement(MarkButton, { 84 | format: "underline", 85 | icon: "format_underlined" 86 | }), /* @__PURE__ */ React.createElement(MarkButton, { 87 | format: "code", 88 | icon: "code" 89 | }), /* @__PURE__ */ React.createElement(BlockButton, { 90 | format: "heading-one", 91 | icon: "looks_one" 92 | }), /* @__PURE__ */ React.createElement(BlockButton, { 93 | format: "heading-two", 94 | icon: "looks_two" 95 | }), /* @__PURE__ */ React.createElement(BlockButton, { 96 | format: "block-quote", 97 | icon: "format_quote" 98 | }), /* @__PURE__ */ React.createElement(BlockButton, { 99 | format: "numbered-list", 100 | icon: "format_list_numbered" 101 | }), /* @__PURE__ */ React.createElement(BlockButton, { 102 | format: "bulleted-list", 103 | icon: "format_list_bulleted" 104 | })), !editable && /* @__PURE__ */ React.createElement("div", null, "Loading..."), editable && /* @__PURE__ */ React.createElement(Editable, { 105 | renderElement, 106 | renderLeaf, 107 | decorate, 108 | placeholder: "Enter some rich text\u2026", 109 | spellCheck: true, 110 | onKeyDown: (event) => { 111 | for (const hotkey in HOTKEYS) { 112 | if (isHotkey(hotkey, event)) { 113 | event.preventDefault(); 114 | const mark = HOTKEYS[hotkey]; 115 | toggleMark(editor, mark); 116 | } 117 | } 118 | } 119 | }))); 120 | }; 121 | const toggleBlock = (editor, format) => { 122 | const isActive = isBlockActive(editor, format); 123 | const isList = LIST_TYPES.includes(format); 124 | Transforms.unwrapNodes(editor, { 125 | match: (n) => LIST_TYPES.includes(!Editor.isEditor(n) && SlateElement.isElement(n) && n.type), 126 | split: true 127 | }); 128 | const newProperties = { 129 | type: isActive ? "paragraph" : isList ? "list-item" : format 130 | }; 131 | Transforms.setNodes(editor, newProperties); 132 | if (!isActive && isList) { 133 | const block = {type: format, children: []}; 134 | Transforms.wrapNodes(editor, block); 135 | } 136 | }; 137 | const toggleMark = (editor, format) => { 138 | const isActive = isMarkActive(editor, format); 139 | if (isActive) { 140 | Editor.removeMark(editor, format); 141 | } else { 142 | Editor.addMark(editor, format, true); 143 | } 144 | }; 145 | const isBlockActive = (editor, format) => { 146 | const [match] = Editor.nodes(editor, { 147 | match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === format 148 | }); 149 | return !!match; 150 | }; 151 | const isMarkActive = (editor, format) => { 152 | const marks = Editor.marks(editor); 153 | return marks ? marks[format] === true : false; 154 | }; 155 | const Element = ({attributes, children, element}) => { 156 | switch (element.type) { 157 | case "block-quote": 158 | return /* @__PURE__ */ React.createElement("blockquote", { 159 | ...attributes 160 | }, children); 161 | case "bulleted-list": 162 | return /* @__PURE__ */ React.createElement("ul", { 163 | ...attributes 164 | }, children); 165 | case "heading-one": 166 | return /* @__PURE__ */ React.createElement("h1", { 167 | ...attributes 168 | }, children); 169 | case "heading-two": 170 | return /* @__PURE__ */ React.createElement("h2", { 171 | ...attributes 172 | }, children); 173 | case "list-item": 174 | return /* @__PURE__ */ React.createElement("li", { 175 | ...attributes 176 | }, children); 177 | case "numbered-list": 178 | return /* @__PURE__ */ React.createElement("ol", { 179 | ...attributes 180 | }, children); 181 | default: 182 | return /* @__PURE__ */ React.createElement("p", { 183 | ...attributes 184 | }, children); 185 | } 186 | }; 187 | const Leaf = ({attributes, children, leaf}) => { 188 | if (leaf.bold) { 189 | children = /* @__PURE__ */ React.createElement("strong", null, children); 190 | } 191 | if (leaf.code) { 192 | children = /* @__PURE__ */ React.createElement("code", null, children); 193 | } 194 | if (leaf.italic) { 195 | children = /* @__PURE__ */ React.createElement("em", null, children); 196 | } 197 | if (leaf.underline) { 198 | children = /* @__PURE__ */ React.createElement("u", null, children); 199 | } 200 | return /* @__PURE__ */ React.createElement("span", { 201 | ...attributes, 202 | style: { 203 | position: "relative", 204 | backgroundColor: leaf.alphaColor 205 | } 206 | }, leaf.isCaret ? /* @__PURE__ */ React.createElement(Caret, { 207 | ...leaf 208 | }) : null, children); 209 | }; 210 | const BlockButton = ({format, icon}) => { 211 | const editor = useSlate(); 212 | return /* @__PURE__ */ React.createElement(Button, { 213 | active: isBlockActive(editor, format), 214 | onMouseDown: (event) => { 215 | event.preventDefault(); 216 | toggleBlock(editor, format); 217 | } 218 | }, /* @__PURE__ */ React.createElement(Icon, null, icon)); 219 | }; 220 | const MarkButton = ({format, icon}) => { 221 | const editor = useSlate(); 222 | return /* @__PURE__ */ React.createElement(Button, { 223 | active: isMarkActive(editor, format), 224 | onMouseDown: (event) => { 225 | event.preventDefault(); 226 | toggleMark(editor, format); 227 | } 228 | }, /* @__PURE__ */ React.createElement(Icon, null, icon)); 229 | }; 230 | const Wrapper = ({className, ...props}) => /* @__PURE__ */ React.createElement("div", { 231 | ...props, 232 | className: cx(className, css` 233 | margin: 20px auto; 234 | padding: 20px; 235 | `) 236 | }); 237 | const ExampleContent = (props) => /* @__PURE__ */ React.createElement(Wrapper, { 238 | ...props, 239 | className: css` 240 | background: #fff; 241 | ` 242 | }); 243 | const Caret = ({color, isForward, name}) => { 244 | const cursorStyles = { 245 | ...cursorStyleBase, 246 | background: color, 247 | left: isForward ? "100%" : "0%" 248 | }; 249 | const caretStyles = { 250 | ...caretStyleBase, 251 | background: color, 252 | left: isForward ? "100%" : "0%" 253 | }; 254 | caretStyles[isForward ? "bottom" : "top"] = 0; 255 | return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { 256 | contentEditable: false, 257 | style: caretStyles 258 | }, /* @__PURE__ */ React.createElement("span", { 259 | style: {position: "relative"} 260 | }, /* @__PURE__ */ React.createElement("span", { 261 | contentEditable: false, 262 | style: cursorStyles 263 | }, name)))); 264 | }; 265 | const cursorStyleBase = { 266 | position: "absolute", 267 | top: -2, 268 | pointerEvents: "none", 269 | userSelect: "none", 270 | transform: "translateY(-100%)", 271 | fontSize: 10, 272 | color: "white", 273 | background: "palevioletred", 274 | whiteSpace: "nowrap" 275 | }; 276 | const caretStyleBase = { 277 | position: "absolute", 278 | pointerEvents: "none", 279 | userSelect: "none", 280 | height: "1.2em", 281 | width: 2, 282 | background: "palevioletred" 283 | }; 284 | export default SlateEditor; 285 | -------------------------------------------------------------------------------- /docs/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "../_snowpack/pkg/react.js"; 2 | import ReactDOM from "../_snowpack/pkg/react-dom.js"; 3 | import {css} from "../_snowpack/pkg/@emotion/css.js"; 4 | import SlateEditor from "./components/SlateEditor/index.js"; 5 | import ConnectionForm from "./components/ConnectionForm.js"; 6 | const pageStyle = css` 7 | margin: 24px; 8 | `; 9 | const App = () => /* @__PURE__ */ React.createElement("div", { 10 | className: pageStyle 11 | }, /* @__PURE__ */ React.createElement(ConnectionForm, null), /* @__PURE__ */ React.createElement(SlateEditor, null)); 12 | ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root")); 13 | -------------------------------------------------------------------------------- /docs/src/services/state.js: -------------------------------------------------------------------------------- 1 | export const YJS_ENDPOINT = `wss://yrk3e12ayj.execute-api.us-east-1.amazonaws.com/dev` 2 | -------------------------------------------------------------------------------- /docs/src/services/useCursor.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback, useEffect } from '../../_snowpack/pkg/react.js' 2 | import { Text, Range, Path } from '../../_snowpack/pkg/slate.js' 3 | 4 | // Apply slate cursor to YJS 5 | export const applySlateCursor = (editor, awareness, cursorOptions) => { 6 | const selection = editor.selection 7 | const localCursor = awareness.getLocalState().cursor 8 | 9 | if (selection) { 10 | const updatedCursor = Object.assign( 11 | {}, 12 | localCursor, 13 | selection, 14 | cursorOptions, 15 | { 16 | isForward: Range.isForward(selection) 17 | } 18 | ) 19 | 20 | // Broadcast cursor 21 | if (JSON.stringify(updatedCursor) !== JSON.stringify(localCursor)) { 22 | awareness.setLocalStateField('cursor', updatedCursor) 23 | } 24 | } else { 25 | // Broadcast remove cursor 26 | awareness.setLocalStateField('cursor', null) 27 | } 28 | } 29 | 30 | const useCursor = (editor, awareness, cursorOptions) => { 31 | const [cursors, setCursors] = useState([]) 32 | 33 | useEffect(() => { 34 | const oldOnChange = editor.onChange 35 | 36 | editor.onChange = () => { 37 | if (!editor.isRemote) { 38 | applySlateCursor(editor, awareness, cursorOptions) 39 | } 40 | 41 | if (oldOnChange) { 42 | oldOnChange() 43 | } 44 | } 45 | 46 | awareness.on('change', () => { 47 | const localState = awareness.getLocalState() 48 | if (!localState) return // page is closing 49 | // Pull cursors from awareness 50 | setCursors( 51 | [...awareness.getStates().values()] 52 | .filter(_ => _ !== localState) 53 | .map(_ => _.cursor) 54 | .filter(_ => _) 55 | ) 56 | }) 57 | }, []) 58 | 59 | // Supply decorations to slate leaves 60 | const decorate = useCallback( 61 | ([node, path]) => { 62 | const ranges = [] 63 | 64 | if (Text.isText(node) && cursors?.length) { 65 | cursors.forEach(cursor => { 66 | if (Range.includes(cursor, path)) { 67 | const { focus, anchor, isForward } = cursor 68 | 69 | const isFocusNode = Path.equals(focus.path, path) 70 | const isAnchorNode = Path.equals(anchor.path, path) 71 | 72 | ranges.push({ 73 | ...cursor, 74 | isCaret: isFocusNode, 75 | anchor: { 76 | path, 77 | offset: isAnchorNode 78 | ? anchor.offset 79 | : isForward 80 | ? 0 81 | : node.text.length 82 | }, 83 | focus: { 84 | path, 85 | offset: isFocusNode 86 | ? focus.offset 87 | : isForward 88 | ? node.text.length 89 | : 0 90 | } 91 | }) 92 | } 93 | }) 94 | } 95 | 96 | return ranges 97 | }, 98 | [cursors] 99 | ) 100 | 101 | return { 102 | cursors, 103 | decorate 104 | } 105 | } 106 | 107 | export default useCursor 108 | -------------------------------------------------------------------------------- /docs/src/services/useLocalStorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from '../../_snowpack/pkg/react.js' 2 | 3 | /** 4 | * 5 | * @param {*} key 6 | * @param {*} initialValue 7 | * @returns {*} 8 | */ 9 | export default function useLocalStorage(key, initialValue) { 10 | // Init synchronously 11 | if (!window.localStorage.getItem(key)) { 12 | window.localStorage.setItem(key, initialValue) 13 | } 14 | 15 | const [value, setValue] = useState(window.localStorage.getItem(key)) 16 | 17 | const setValueWrapper = (value) => { 18 | window.localStorage.setItem(key, value) 19 | setValue(value) 20 | } 21 | 22 | return [value, setValueWrapper]; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/services/y-websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | Unlike stated in the LICENSE file, it is not necessary to include the copyright notice and permission notice when you copy code from this file. 3 | */ 4 | 5 | /** 6 | * @module provider/websocket 7 | */ 8 | 9 | /* eslint-env browser */ 10 | 11 | import * as Y from '../../_snowpack/pkg/yjs.js' // eslint-disable-line 12 | import * as bc from '../../_snowpack/pkg/lib0/broadcastchannel.js' 13 | import * as time from '../../_snowpack/pkg/lib0/time.js' 14 | import * as encoding from '../../_snowpack/pkg/lib0/encoding.js' 15 | import * as decoding from '../../_snowpack/pkg/lib0/decoding.js' 16 | import * as syncProtocol from '../../_snowpack/pkg/y-protocols/sync.js' 17 | import * as authProtocol from '../../_snowpack/pkg/y-protocols/auth.js' 18 | import * as awarenessProtocol from '../../_snowpack/pkg/y-protocols/awareness.js' 19 | import * as mutex from '../../_snowpack/pkg/lib0/mutex.js' 20 | import { Observable } from '../../_snowpack/pkg/lib0/observable.js' 21 | import * as math from '../../_snowpack/pkg/lib0/math.js' 22 | import * as url from '../../_snowpack/pkg/lib0/url.js' 23 | import { toBase64, fromBase64 } from '../../_snowpack/pkg/lib0/buffer.js' 24 | 25 | const messageSync = 0 26 | const messageQueryAwareness = 3 27 | const messageAwareness = 1 28 | const messageAuth = 2 29 | 30 | const reconnectTimeoutBase = 1200 31 | const maxReconnectTimeout = 2500 32 | // @todo - this should depend on awareness.outdatedTime 33 | const messageReconnectTimeout = 30000 34 | 35 | /** 36 | * @param {WebsocketProvider} provider 37 | * @param {string} reason 38 | */ 39 | const permissionDeniedHandler = (provider, reason) => console.warn(`Permission denied to access ${provider.url}.\n${reason}`) 40 | 41 | /** 42 | * @param {WebsocketProvider} provider 43 | * @param {Uint8Array} buf 44 | * @param {boolean} emitSynced 45 | * @return {encoding.Encoder} 46 | */ 47 | const readMessage = (provider, buf, emitSynced) => { 48 | const decoder = decoding.createDecoder(buf) 49 | const encoder = encoding.createEncoder() 50 | const messageType = decoding.readVarUint(decoder) 51 | switch (messageType) { 52 | case messageSync: { 53 | encoding.writeVarUint(encoder, messageSync) 54 | const syncMessageType = syncProtocol.readSyncMessage(decoder, encoder, provider.doc, provider) 55 | if (emitSynced && syncMessageType === syncProtocol.messageYjsSyncStep2 && !provider.synced) { 56 | provider.synced = true 57 | } 58 | break 59 | } 60 | case messageQueryAwareness: 61 | encoding.writeVarUint(encoder, messageAwareness) 62 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, Array.from(provider.awareness.getStates().keys()))) 63 | break 64 | case messageAwareness: 65 | awarenessProtocol.applyAwarenessUpdate(provider.awareness, decoding.readVarUint8Array(decoder), provider) 66 | break 67 | case messageAuth: 68 | authProtocol.readAuthMessage(decoder, provider.doc, permissionDeniedHandler) 69 | break 70 | default: 71 | console.error('Unable to compute message') 72 | return encoder 73 | } 74 | return encoder 75 | } 76 | 77 | /** 78 | * @param {WebsocketProvider} provider 79 | */ 80 | const setupWS = provider => { 81 | if (provider.shouldConnect && provider.ws === null) { 82 | const websocket = new provider._WS(provider.url) 83 | websocket.binaryType = 'arraybuffer' 84 | provider.ws = websocket 85 | provider.wsconnecting = true 86 | provider.wsconnected = false 87 | provider.synced = false 88 | 89 | websocket.onmessage = event => { 90 | provider.wsLastMessageReceived = time.getUnixTime() 91 | const encoder = readMessage(provider, new Uint8Array(fromBase64(event.data)), true) 92 | if (encoding.length(encoder) > 1) { 93 | websocket.send(toBase64(encoding.toUint8Array(encoder))) 94 | } 95 | } 96 | websocket.onclose = () => { 97 | provider.ws = null 98 | provider.wsconnecting = false 99 | if (provider.wsconnected) { 100 | provider.wsconnected = false 101 | provider.synced = false 102 | // update awareness (all users except local left) 103 | awarenessProtocol.removeAwarenessStates(provider.awareness, Array.from(provider.awareness.getStates().keys()).filter(client => client !== provider.doc.clientID), provider) 104 | provider.emit('status', [{ 105 | status: 'disconnected' 106 | }]) 107 | } else { 108 | provider.wsUnsuccessfulReconnects++ 109 | } 110 | // Start with no reconnect timeout and increase timeout by 111 | // log10(wsUnsuccessfulReconnects). 112 | // The idea is to increase reconnect timeout slowly and have no reconnect 113 | // timeout at the beginning (log(1) = 0) 114 | setTimeout(setupWS, math.min(math.log10(provider.wsUnsuccessfulReconnects + 1) * reconnectTimeoutBase, maxReconnectTimeout), provider) 115 | } 116 | websocket.onopen = () => { 117 | provider.wsLastMessageReceived = time.getUnixTime() 118 | provider.wsconnecting = false 119 | provider.wsconnected = true 120 | provider.wsUnsuccessfulReconnects = 0 121 | provider.emit('status', [{ 122 | status: 'connected' 123 | }]) 124 | // always send sync step 1 when connected 125 | const encoder = encoding.createEncoder() 126 | encoding.writeVarUint(encoder, messageSync) 127 | syncProtocol.writeSyncStep1(encoder, provider.doc) 128 | websocket.send(toBase64(encoding.toUint8Array(encoder))) 129 | // broadcast local awareness state 130 | if (provider.awareness.getLocalState() !== null) { 131 | const encoderAwarenessState = encoding.createEncoder() 132 | encoding.writeVarUint(encoderAwarenessState, messageAwareness) 133 | encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(provider.awareness, [provider.doc.clientID])) 134 | websocket.send(toBase64(encoding.toUint8Array(encoderAwarenessState))) 135 | } 136 | } 137 | 138 | provider.emit('status', [{ 139 | status: 'connecting' 140 | }]) 141 | } 142 | } 143 | 144 | /** 145 | * @param {WebsocketProvider} provider 146 | * @param {ArrayBuffer} buf 147 | */ 148 | const broadcastMessage = (provider, buf) => { 149 | if (provider.wsconnected) { 150 | // @ts-ignore We know that wsconnected = true 151 | provider.ws.send(toBase64(buf)) 152 | } 153 | if (provider.bcconnected) { 154 | provider.mux(() => { 155 | bc.publish(provider.bcChannel, buf) 156 | }) 157 | } 158 | } 159 | 160 | /** 161 | * Websocket Provider for Yjs. Creates a websocket connection to sync the shared document. 162 | * The document name is attached to the provided url. I.e. the following example 163 | * creates a websocket connection to http://localhost:1234/my-document-name 164 | * 165 | * @example 166 | * import * as Y from 'yjs' 167 | * import { WebsocketProvider } from 'y-websocket' 168 | * const doc = new Y.Doc() 169 | * const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc) 170 | * 171 | * @extends {Observable} 172 | */ 173 | export class WebsocketProvider extends Observable { 174 | /** 175 | * @param {string} serverUrl 176 | * @param {string} roomname 177 | * @param {Y.Doc} doc 178 | * @param {object} [opts] 179 | * @param {boolean} [opts.connect] 180 | * @param {awarenessProtocol.Awareness} [opts.awareness] 181 | * @param {Object} [opts.params] 182 | * @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill 183 | * @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds 184 | */ 185 | constructor (serverUrl, roomname, doc, { connect = true, awareness = new awarenessProtocol.Awareness(doc), params = {}, WebSocketPolyfill = WebSocket, resyncInterval = -1 } = {}) { 186 | super() 187 | // ensure that url is always ends with / 188 | while (serverUrl[serverUrl.length - 1] === '/') { 189 | serverUrl = serverUrl.slice(0, serverUrl.length - 1) 190 | } 191 | const encodedParams = url.encodeQueryParams(params) 192 | this.bcChannel = serverUrl + '/' + roomname 193 | this.url = serverUrl + '/' + roomname + (encodedParams.length === 0 ? '' : '?' + encodedParams) 194 | this.roomname = roomname 195 | this.doc = doc 196 | this._WS = WebSocketPolyfill 197 | this.awareness = awareness 198 | this.wsconnected = false 199 | this.wsconnecting = false 200 | this.bcconnected = false 201 | this.wsUnsuccessfulReconnects = 0 202 | this.mux = mutex.createMutex() 203 | /** 204 | * @type {boolean} 205 | */ 206 | this._synced = false 207 | /** 208 | * @type {WebSocket?} 209 | */ 210 | this.ws = null 211 | this.wsLastMessageReceived = 0 212 | /** 213 | * Whether to connect to other peers or not 214 | * @type {boolean} 215 | */ 216 | this.shouldConnect = connect 217 | 218 | /** 219 | * @type {NodeJS.Timeout | number} 220 | */ 221 | this._resyncInterval = 0 222 | if (resyncInterval > 0) { 223 | this._resyncInterval = setInterval(() => { 224 | if (this.ws) { 225 | // resend sync step 1 226 | const encoder = encoding.createEncoder() 227 | encoding.writeVarUint(encoder, messageSync) 228 | syncProtocol.writeSyncStep1(encoder, doc) 229 | this.ws.send(toBase64(encoding.toUint8Array(encoder))) 230 | } 231 | }, resyncInterval) 232 | } 233 | 234 | /** 235 | * @param {ArrayBuffer} data 236 | */ 237 | this._bcSubscriber = data => { 238 | this.mux(() => { 239 | const encoder = readMessage(this, new Uint8Array(data), false) 240 | if (encoding.length(encoder) > 1) { 241 | bc.publish(this.bcChannel, encoding.toUint8Array(encoder)) 242 | } 243 | }) 244 | } 245 | /** 246 | * Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel) 247 | * @param {Uint8Array} update 248 | * @param {any} origin 249 | */ 250 | this._updateHandler = (update, origin) => { 251 | if (origin !== this || origin === null) { 252 | const encoder = encoding.createEncoder() 253 | encoding.writeVarUint(encoder, messageSync) 254 | syncProtocol.writeUpdate(encoder, update) 255 | broadcastMessage(this, encoding.toUint8Array(encoder)) 256 | } 257 | } 258 | this.doc.on('update', this._updateHandler) 259 | /** 260 | * @param {any} changed 261 | * @param {any} origin 262 | */ 263 | this._awarenessUpdateHandler = ({ added, updated, removed }, origin) => { 264 | const changedClients = added.concat(updated).concat(removed) 265 | const encoder = encoding.createEncoder() 266 | encoding.writeVarUint(encoder, messageAwareness) 267 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(awareness, changedClients)) 268 | broadcastMessage(this, encoding.toUint8Array(encoder)) 269 | } 270 | window.addEventListener('beforeunload', () => { 271 | awarenessProtocol.removeAwarenessStates(this.awareness, [doc.clientID], 'window unload') 272 | }) 273 | awareness.on('update', this._awarenessUpdateHandler) 274 | this._checkInterval = setInterval(() => { 275 | if (this.wsconnected && messageReconnectTimeout < time.getUnixTime() - this.wsLastMessageReceived) { 276 | // no message received in a long time - not even your own awareness 277 | // updates (which are updated every 15 seconds) 278 | /** @type {WebSocket} */ (this.ws).close() 279 | } 280 | }, messageReconnectTimeout / 10) 281 | if (connect) { 282 | this.connect() 283 | } 284 | } 285 | 286 | /** 287 | * @type {boolean} 288 | */ 289 | get synced () { 290 | return this._synced 291 | } 292 | 293 | set synced (state) { 294 | if (this._synced !== state) { 295 | this._synced = state 296 | this.emit('synced', [state]) 297 | this.emit('sync', [state]) 298 | } 299 | } 300 | 301 | destroy () { 302 | if (this._resyncInterval !== 0) { 303 | clearInterval(/** @type {NodeJS.Timeout} */ (this._resyncInterval)) 304 | } 305 | clearInterval(this._checkInterval) 306 | this.disconnect() 307 | this.awareness.off('update', this._awarenessUpdateHandler) 308 | this.doc.off('update', this._updateHandler) 309 | super.destroy() 310 | } 311 | 312 | connectBc () { 313 | if (!this.bcconnected) { 314 | bc.subscribe(this.bcChannel, this._bcSubscriber) 315 | this.bcconnected = true 316 | } 317 | // send sync step1 to bc 318 | this.mux(() => { 319 | // write sync step 1 320 | const encoderSync = encoding.createEncoder() 321 | encoding.writeVarUint(encoderSync, messageSync) 322 | syncProtocol.writeSyncStep1(encoderSync, this.doc) 323 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync)) 324 | // broadcast local state 325 | const encoderState = encoding.createEncoder() 326 | encoding.writeVarUint(encoderState, messageSync) 327 | syncProtocol.writeSyncStep2(encoderState, this.doc) 328 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderState)) 329 | // write queryAwareness 330 | const encoderAwarenessQuery = encoding.createEncoder() 331 | encoding.writeVarUint(encoderAwarenessQuery, messageQueryAwareness) 332 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery)) 333 | // broadcast local awareness state 334 | const encoderAwarenessState = encoding.createEncoder() 335 | encoding.writeVarUint(encoderAwarenessState, messageAwareness) 336 | encoding.writeVarUint8Array(encoderAwarenessState, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID])) 337 | bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState)) 338 | }) 339 | } 340 | 341 | disconnectBc () { 342 | // broadcast message with local awareness state set to null (indicating disconnect) 343 | const encoder = encoding.createEncoder() 344 | encoding.writeVarUint(encoder, messageAwareness) 345 | encoding.writeVarUint8Array(encoder, awarenessProtocol.encodeAwarenessUpdate(this.awareness, [this.doc.clientID], new Map())) 346 | broadcastMessage(this, encoding.toUint8Array(encoder)) 347 | if (this.bcconnected) { 348 | bc.unsubscribe(this.bcChannel, this._bcSubscriber) 349 | this.bcconnected = false 350 | } 351 | } 352 | 353 | disconnect () { 354 | this.shouldConnect = false 355 | this.disconnectBc() 356 | if (this.ws !== null) { 357 | this.ws.close() 358 | } 359 | } 360 | 361 | connect () { 362 | this.shouldConnect = true 363 | if (!this.wsconnected && this.ws === null) { 364 | setupWS(this) 365 | this.connectBc() 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "esnext", 5 | "checkJs": true, 6 | "jsx": "react", 7 | "resolveJsonModule": true 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | "build" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /local-db/cleanup.sh: -------------------------------------------------------------------------------- 1 | aws dynamodb delete-table --table-name docs --endpoint-url http://localhost:8000 2 | aws dynamodb delete-table --table-name connections --endpoint-url http://localhost:8000 3 | -------------------------------------------------------------------------------- /local-db/connections_table.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeySchema": [ 3 | { 4 | "AttributeName": "PartitionKey", 5 | "KeyType": "HASH" 6 | } 7 | ], 8 | "AttributeDefinitions": [ 9 | { 10 | "AttributeName": "PartitionKey", 11 | "AttributeType": "S" 12 | }, 13 | { 14 | "AttributeName": "DocName", 15 | "AttributeType": "S" 16 | } 17 | ], 18 | "BillingMode": "PAY_PER_REQUEST", 19 | "GlobalSecondaryIndexes": [ 20 | { 21 | "IndexName": "DocNameIndex", 22 | "KeySchema": [ 23 | { 24 | "AttributeName": "DocName", 25 | "KeyType": "HASH" 26 | } 27 | ], 28 | "Projection": { 29 | "ProjectionType": "ALL" 30 | } 31 | } 32 | ], 33 | "TableName": "connections" 34 | } 35 | -------------------------------------------------------------------------------- /local-db/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | dynamodb-local: 4 | image: amazon/dynamodb-local:latest 5 | container_name: dynamodb-local 6 | ports: 7 | - "8000:8000" 8 | -------------------------------------------------------------------------------- /local-db/docs_table.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeySchema": [ 3 | { 4 | "AttributeName": "PartitionKey", 5 | "KeyType": "HASH" 6 | } 7 | ], 8 | "AttributeDefinitions": [ 9 | { 10 | "AttributeName": "PartitionKey", 11 | "AttributeType": "S" 12 | } 13 | ], 14 | "BillingMode": "PAY_PER_REQUEST", 15 | "TableName": "docs" 16 | } 17 | -------------------------------------------------------------------------------- /local-db/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "docker-compose up --no-recreate", 4 | "setup": "bash ./setup.sh", 5 | "cleanup": "bash ./cleanup.sh" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /local-db/setup.sh: -------------------------------------------------------------------------------- 1 | aws dynamodb create-table --cli-input-json file://docs_table.json --endpoint-url http://localhost:8000 2 | aws dynamodb create-table --cli-input-json file://connections_table.json --endpoint-url http://localhost:8000 3 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "esbuild": { 6 | "version": "0.8.42", 7 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.8.42.tgz", 8 | "integrity": "sha512-zUtj5RMqROCCCH0vV/a7cd8YQg8I0GWBhV3A3PklWRT+oM/YwVbnrtFnITzE1otGdnXplWHWdZ4OcYiV0PN+JQ==", 9 | "dev": true 10 | }, 11 | "fsevents": { 12 | "version": "2.3.2", 13 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 14 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 15 | "dev": true, 16 | "optional": true 17 | }, 18 | "is-docker": { 19 | "version": "2.1.1", 20 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", 21 | "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", 22 | "dev": true 23 | }, 24 | "is-wsl": { 25 | "version": "2.2.0", 26 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", 27 | "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", 28 | "dev": true, 29 | "requires": { 30 | "is-docker": "^2.0.0" 31 | } 32 | }, 33 | "open": { 34 | "version": "7.4.0", 35 | "resolved": "https://registry.npmjs.org/open/-/open-7.4.0.tgz", 36 | "integrity": "sha512-PGoBCX/lclIWlpS/R2PQuIR4NJoXh6X5AwVzE7WXnWRGvHg7+4TBCgsujUgiPpm0K1y4qvQeWnCWVTpTKZBtvA==", 37 | "dev": true, 38 | "requires": { 39 | "is-docker": "^2.0.0", 40 | "is-wsl": "^2.1.1" 41 | } 42 | }, 43 | "rollup": { 44 | "version": "2.38.5", 45 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.38.5.tgz", 46 | "integrity": "sha512-VoWt8DysFGDVRGWuHTqZzT02J0ASgjVq/hPs9QcBOGMd7B+jfTr/iqMVEyOi901rE3xq+Deq66GzIT1yt7sGwQ==", 47 | "dev": true, 48 | "requires": { 49 | "fsevents": "~2.3.1" 50 | } 51 | }, 52 | "snowpack": { 53 | "version": "3.0.11", 54 | "resolved": "https://registry.npmjs.org/snowpack/-/snowpack-3.0.11.tgz", 55 | "integrity": "sha512-lBxgkvWTgdg0szE31JUt01wQkA9Lnmm+6lxqeV9rxDfflpx7ASnldVHFvu7Se70QJmPTQB0UJjfKI+xmYGwiiQ==", 56 | "dev": true, 57 | "requires": { 58 | "esbuild": "^0.8.7", 59 | "fsevents": "^2.2.0", 60 | "open": "^7.0.4", 61 | "rollup": "^2.34.0" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'node': true, 4 | 'es2021': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'parserOptions': { 8 | 'ecmaVersion': 12, 9 | 'sourceType': 'module', 10 | 'ecmaFeatures': { 11 | 'jsx': true 12 | }, 13 | }, 14 | 'rules': { 15 | 'indent': [ 16 | 'error', 17 | 2 18 | ], 19 | 'linebreak-style': [ 20 | 'error', 21 | 'unix' 22 | ], 23 | 'quotes': [ 24 | 'error', 25 | 'single' 26 | ], 27 | 'semi': [ 28 | 'error', 29 | 'never' 30 | ] 31 | }, 32 | 'ignorePatterns': ['build'] 33 | } 34 | -------------------------------------------------------------------------------- /server/babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "12" 7 | } 8 | } 9 | ] 10 | ], 11 | "ignore": [ 12 | "node_modules", 13 | "build" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /server/db/aws.js: -------------------------------------------------------------------------------- 1 | import { DynamoDBClient, PutItemCommand, QueryCommand, DeleteItemCommand, UpdateItemCommand } from '@aws-sdk/client-dynamodb' 2 | import * as Y from 'yjs' 3 | 4 | const ddb = new DynamoDBClient({ 5 | apiVersion: '2012-08-10', 6 | region: process.env.REGION, 7 | endpoint: process.env.DYNAMODB_ENDPOINT, 8 | }) 9 | 10 | export async function addConnection (id, docName) { 11 | await ddb.send(new PutItemCommand({ 12 | TableName: process.env.CONNECTIONS_TABLE_NAME, 13 | Item: { 14 | PartitionKey: { 15 | S: id, 16 | }, 17 | DocName: { 18 | S: docName, 19 | }, 20 | }, 21 | })) 22 | } 23 | 24 | export async function getConnection (id) { 25 | const { Items } = await ddb.send(new QueryCommand({ 26 | TableName: process.env.CONNECTIONS_TABLE_NAME, 27 | KeyConditionExpression: 'PartitionKey = :partitionkeyval', 28 | ExpressionAttributeValues: { 29 | ':partitionkeyval': { 30 | S: id, 31 | }, 32 | }, 33 | })) 34 | 35 | const connection = Items[0] 36 | 37 | if (!connection) { 38 | await removeConnection(id) 39 | throw new Error(`Connection not found: ${id}`) 40 | } 41 | 42 | return connection 43 | } 44 | 45 | export async function getConnectionIds (docName) { 46 | const { Items } = await ddb.send(new QueryCommand({ 47 | TableName: process.env.CONNECTIONS_TABLE_NAME, 48 | IndexName: 'DocNameIndex', 49 | KeyConditionExpression: 'DocName = :docnameval', 50 | ExpressionAttributeValues: { 51 | ':docnameval': { 52 | S: docName, 53 | }, 54 | }, 55 | })) 56 | return Items.map(item => item.PartitionKey.S) 57 | } 58 | 59 | export async function removeConnection (id) { 60 | await ddb.send(new DeleteItemCommand({ 61 | TableName: process.env.CONNECTIONS_TABLE_NAME, 62 | Key: { 63 | PartitionKey: { 64 | S: id, 65 | }, 66 | }, 67 | })) 68 | } 69 | 70 | export async function getOrCreateDoc (docName) { 71 | const { Items } = await ddb.send(new QueryCommand({ 72 | TableName: process.env.DOCS_TABLE_NAME, 73 | KeyConditionExpression: 'PartitionKey = :partitionkeyval', 74 | ExpressionAttributeValues: { 75 | ':partitionkeyval': { 76 | S: docName, 77 | }, 78 | }, 79 | })) 80 | 81 | let dbDoc = Items[0] 82 | 83 | // Doc not found, create doc 84 | if (!dbDoc) { 85 | await ddb.send(new PutItemCommand({ 86 | TableName: process.env.DOCS_TABLE_NAME, 87 | Item: { 88 | PartitionKey: { 89 | S: docName, 90 | }, 91 | Updates: { 92 | L: [], 93 | }, 94 | }, 95 | })) 96 | dbDoc = { 97 | Updates: { L: [] } 98 | } 99 | } 100 | 101 | // @ts-ignore 102 | const updates = dbDoc.Updates.L.map(_ => new Uint8Array(Buffer.from(_.B, 'base64'))) 103 | 104 | const ydoc = new Y.Doc() 105 | 106 | for (let i = 0; i < updates.length; i++) { 107 | Y.applyUpdate(ydoc, updates[i]) 108 | } 109 | 110 | return ydoc 111 | } 112 | 113 | export async function updateDoc (docName, update) { 114 | await ddb.send(new UpdateItemCommand({ 115 | TableName: process.env.DOCS_TABLE_NAME, 116 | UpdateExpression: 'SET Updates = list_append(Updates, :attrValue)', 117 | Key: { 118 | PartitionKey: { 119 | S: docName, 120 | }, 121 | }, 122 | ExpressionAttributeValues: { 123 | ':attrValue': { 124 | L: [{ B: update }], 125 | }, 126 | }, 127 | })) 128 | } 129 | -------------------------------------------------------------------------------- /server/handler/aws.js: -------------------------------------------------------------------------------- 1 | import * as Y from 'yjs' 2 | // @ts-ignore 3 | import syncProtocol from 'y-protocols/dist/sync.cjs' 4 | // @ts-ignore 5 | import encoding from 'lib0/dist/encoding.cjs' 6 | // @ts-ignore 7 | import decoding from 'lib0/dist/decoding.cjs' 8 | import { addConnection, getConnection, getConnectionIds, removeConnection, getOrCreateDoc, updateDoc } from '../db/aws.js' 9 | import ws from 'aws-lambda-ws-server' 10 | import { toBase64, fromBase64 } from 'lib0/buffer.js' 11 | 12 | const messageSync = 0 13 | const messageAwareness = 1 14 | 15 | const getDocName = (event) => { 16 | const qs = event.multiValueQueryStringParameters 17 | 18 | if (!qs || !qs.doc) { 19 | throw new Error('must specify ?doc=DOC_NAME') 20 | } 21 | 22 | return qs.doc[0] 23 | } 24 | 25 | const send = ({ context, message, id }) => { 26 | return context.postToConnection(toBase64(message), id) 27 | .catch((err) => { 28 | console.error(`Error during postToConnection: ${err}`) 29 | return removeConnection(id) 30 | }) 31 | } 32 | 33 | export const handler = ws( 34 | ws.handler({ 35 | // Connect 36 | async connect ({ id, event, context }) { 37 | console.log(['connect', id, event]) 38 | 39 | const docName = getDocName(event) 40 | 41 | await addConnection(id, docName) 42 | 43 | // get doc from db 44 | // create new doc with no updates if no doc exists 45 | const doc = await getOrCreateDoc(docName) 46 | 47 | // writeSyncStep1 (send sv) 48 | const encoder = encoding.createEncoder() 49 | encoding.writeVarUint(encoder, messageSync) 50 | syncProtocol.writeSyncStep1(encoder, doc) 51 | 52 | // TODO cannot send message during connection!!!!! 53 | // await send({ context, message: encoding.toUint8Array(encoder), id }) 54 | 55 | console.log('done connect') 56 | return { statusCode: 200, body: 'Connected.' } 57 | }, 58 | 59 | // Disconnect 60 | async disconnect ({ id, event }) { 61 | console.log(['disconnect', id, event]) 62 | 63 | await removeConnection(id) 64 | 65 | return { statusCode: 200, body: 'Disconnected.' } 66 | }, 67 | 68 | // Message 69 | async default ({ message, id, event, context }) { 70 | console.log(['message', id, message, event]) 71 | 72 | message = fromBase64(message) 73 | 74 | const docName = (await getConnection(id)).DocName.S 75 | const connectionIds = await getConnectionIds(docName) 76 | const otherConnectionIds = connectionIds.filter(_ => _ !== id) 77 | const broadcast = (message) => { 78 | return Promise.all(otherConnectionIds.map(id => { 79 | return send({ context, message, id }) 80 | })) 81 | } 82 | 83 | const doc = await getOrCreateDoc(docName) 84 | 85 | const encoder = encoding.createEncoder() 86 | const decoder = decoding.createDecoder(message) 87 | const messageType = decoding.readVarUint(decoder) 88 | 89 | switch (messageType) { 90 | // Case sync1: Read SyncStep1 message and reply with SyncStep2 (send doc to client wrt state vector input) 91 | // Case sync2 or yjsUpdate: Read and apply Structs and then DeleteStore to a y instance (append to db, send to all clients) 92 | case messageSync: 93 | encoding.writeVarUint(encoder, messageSync) 94 | 95 | // syncProtocol.readSyncMessage 96 | const messageType = decoding.readVarUint(decoder) 97 | switch (messageType) { 98 | case syncProtocol.messageYjsSyncStep1: 99 | syncProtocol.writeSyncStep2(encoder, doc, decoding.readVarUint8Array(decoder)) 100 | break 101 | case syncProtocol.messageYjsSyncStep2: 102 | case syncProtocol.messageYjsUpdate: 103 | const update = decoding.readVarUint8Array(decoder) 104 | Y.applyUpdate(doc, update) 105 | await updateDoc(docName, update) 106 | await broadcast(message) 107 | break 108 | default: 109 | throw new Error('Unknown message type') 110 | } 111 | 112 | // Reply with our state 113 | if (encoding.length(encoder) > 1) { 114 | await send({ context, message: encoding.toUint8Array(encoder), id }) 115 | } 116 | 117 | break 118 | case messageAwareness: { 119 | await broadcast(message) 120 | break 121 | } 122 | } 123 | 124 | return { statusCode: 200, body: 'Data sent.' } 125 | }, 126 | }) 127 | ) 128 | -------------------------------------------------------------------------------- /server/local-env.cjs: -------------------------------------------------------------------------------- 1 | process.env.REGION = 'us-east-1' // same as aws cli config 2 | process.env.DYNAMODB_ENDPOINT = 'http://localhost:8000' 3 | process.env.DOCS_TABLE_NAME = 'docs' 4 | process.env.CONNECTIONS_TABLE_NAME = 'connections' 5 | process.env.PORT = 9000 6 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yjs-server", 3 | "version": "1.0.0", 4 | "description": "server for yjs", 5 | "private": true, 6 | "type": "module", 7 | "scripts": { 8 | "start": "nodemon -e js,cjs -r ./local-env.cjs handler/aws", 9 | "build": "rimraf build && rollup --config rollup.config.js", 10 | "postinstall": "patch-package" 11 | }, 12 | "exports": { 13 | "./": "./" 14 | }, 15 | "nodemonConfig": { 16 | "delay": "250" 17 | }, 18 | "dependencies": { 19 | "@aws-sdk/client-dynamodb": "^3.3.0", 20 | "aws-lambda-ws-server": "^0.1.21", 21 | "bufferutil": "^4.0.3", 22 | "lib0": "^0.2.35", 23 | "lodash.debounce": "^4.0.8", 24 | "patch-package": "^6.2.2", 25 | "utf-8-validate": "^5.0.4", 26 | "y-protocols": "^1.0.2", 27 | "yjs": "^13.4.9" 28 | }, 29 | "devDependencies": { 30 | "@babel/cli": "^7.12.10", 31 | "@babel/core": "^7.12.10", 32 | "@babel/preset-env": "^7.12.11", 33 | "@rollup/plugin-commonjs": "^17.0.0", 34 | "@rollup/plugin-json": "^4.1.0", 35 | "@rollup/plugin-node-resolve": "^11.0.1", 36 | "dotenv": "^8.2.0", 37 | "eslint": "^7.17.0", 38 | "nodemon": "^2.0.6", 39 | "rimraf": "^3.0.2", 40 | "rollup": "^2.36.1", 41 | "ws": "^7.4.2", 42 | "y-leveldb": "^0.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/patches/aws-lambda-ws-server+0.1.21.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/aws-lambda-ws-server/src/handler.js b/node_modules/aws-lambda-ws-server/src/handler.js 2 | index 77eadab..0cfd7a2 100644 3 | --- a/node_modules/aws-lambda-ws-server/src/handler.js 4 | +++ b/node_modules/aws-lambda-ws-server/src/handler.js 5 | @@ -14,12 +14,7 @@ module.exports = routes => async (event, context) => { 6 | } else if (eventType === 'DISCONNECT' && routes.disconnect) { 7 | return routes.disconnect(connectionArgs) 8 | } else if (eventType === 'MESSAGE') { 9 | - const body = JSON.parse( 10 | - Buffer.from( 11 | - event.body || '{}', 12 | - event.isBase64Encoded ? 'base64' : undefined 13 | - ) 14 | - ) 15 | + const body = event.body 16 | const messageArgs = { ...connectionArgs, message: body } 17 | if (routes[routeKey]) { 18 | return routes[routeKey](messageArgs) 19 | diff --git a/node_modules/aws-lambda-ws-server/src/local.js b/node_modules/aws-lambda-ws-server/src/local.js 20 | index 3d3cf12..f4c58ec 100644 21 | --- a/node_modules/aws-lambda-ws-server/src/local.js 22 | +++ b/node_modules/aws-lambda-ws-server/src/local.js 23 | @@ -89,7 +89,7 @@ const context = () => ({ 24 | err.statusCode = 410 25 | return reject(err) 26 | } 27 | - ws.send(JSON.stringify(payload), err => { 28 | + ws.send(payload, err => { 29 | if (err) return reject(err) 30 | resolve() 31 | }) 32 | @@ -123,7 +123,7 @@ module.exports = handler => { 33 | }) 34 | ws.on('message', async message => { 35 | try { 36 | - const body = JSON.parse(message || '{}') 37 | + const body = message 38 | await handler( 39 | event(body[mappingKey] || '$default', 'MESSAGE', req, message), 40 | context() 41 | -------------------------------------------------------------------------------- /server/patches/aws-post-to-connection+0.1.21.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/aws-post-to-connection/src/index.js b/node_modules/aws-post-to-connection/src/index.js 2 | index 7dc9631..e69262b 100644 3 | --- a/node_modules/aws-post-to-connection/src/index.js 4 | +++ b/node_modules/aws-post-to-connection/src/index.js 5 | @@ -12,7 +12,7 @@ module.exports = event => async (message, connectionId) => { 6 | headers: { 'content-type': 'application/json' }, 7 | host: domainName, 8 | path: `/${stage}/%40connections/${encodeURIComponent(connectionId)}`, 9 | - body: JSON.stringify(message) 10 | + body: message 11 | }) 12 | 13 | return new Promise((resolve, reject) => { 14 | -------------------------------------------------------------------------------- /server/rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import json from '@rollup/plugin-json' 4 | 5 | export default { 6 | input: 'handler/aws.js', 7 | output: { 8 | file: 'build/index.js', 9 | format: 'cjs' 10 | }, 11 | plugins: [ 12 | nodeResolve(), 13 | commonjs(), 14 | json(), 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /stack/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'node': true, 4 | 'es2021': true 5 | }, 6 | 'extends': 'eslint:recommended', 7 | 'parserOptions': { 8 | 'ecmaVersion': 12, 9 | 'sourceType': 'module', 10 | 'ecmaFeatures': { 11 | 'jsx': true 12 | }, 13 | }, 14 | 'rules': { 15 | 'indent': [ 16 | 'error', 17 | 2 18 | ], 19 | 'linebreak-style': [ 20 | 'error', 21 | 'unix' 22 | ], 23 | 'quotes': [ 24 | 'error', 25 | 'single' 26 | ], 27 | 'semi': [ 28 | 'error', 29 | 'never' 30 | ] 31 | }, 32 | 'ignorePatterns': ['build'] 33 | } 34 | -------------------------------------------------------------------------------- /stack/README.md: -------------------------------------------------------------------------------- 1 | # Api GatewayV2 websocket with lambda and dynamodb 2 | 3 | --- 4 | 5 | ![Stability: Stable](https://img.shields.io/badge/stability-Stable-success.svg?style=for-the-badge) 6 | 7 | > **This is a stable example. It should successfully build out of the box** 8 | > 9 | > This examples does is built on Construct Libraries marked "Stable" and does not have any infrastructure prerequisites to build. 10 | 11 | --- 12 | 13 | 14 | 15 | This is the code for the simple-websocket-chat-app which is oringial from [simple-websockets-chat-app](https://github.com/aws-samples/simple-websockets-chat-app). There are three functions contained within the directories and wires them up to a DynamoDB table and provides the minimal set of permissions needed to run the app: 16 | ``` 17 | . 18 | ├── onconnect <-- Source code onconnect 19 | ├── ondisconnect <-- Source code ondisconnect 20 | └── sendmessage <-- Source code sendmessage 21 | ``` 22 | ```"resolveJsonModule": true "esModuleInterop": true``` is added to `tsconfig.json` to support import jsonfile as header 23 | 24 | ## Build 25 | 26 | To build this app, you need to be in this example's root folder. Then run the following: 27 | 28 | ```bash 29 | npm install -g aws-cdk 30 | npm install 31 | npm run build 32 | ``` 33 | 34 | This will install the necessary CDK, then this example's dependencies, and then build your TypeScript files and your CloudFormation template. 35 | 36 | ## Deploy 37 | 38 | Change `account_id` to your own aws account id in `config.json` file. 39 | 40 | Run `cdk deploy`. This will deploy / redeploy your Stack to your AWS Account. 41 | 42 | After the deployment you will see the API's URL, which represents the url you can then use. 43 | 44 | ## Synthesize Cloudformation Template 45 | 46 | To see the Cloudformation template generated by the CDK, run `cdk synth`, then check the output file in the "cdk.out" directory. 47 | 48 | 49 | ## Testing the chat API 50 | 51 | To test the WebSocket API, you can use [wscat](https://github.com/websockets/wscat), an open-source command line tool. 52 | 53 | 1. [Install NPM](https://www.npmjs.com/get-npm). 54 | 2. Install wscat: 55 | ``` bash 56 | $ npm install -g wscat 57 | ``` 58 | 3. On the console, connect to your published API endpoint by executing the following command: 59 | ``` bash 60 | $ wscat -c wss://{YOUR-API-ID}.execute-api.{YOUR-REGION}.amazonaws.com/{STAGE} 61 | ``` 62 | 4. To test the sendMessage function, send a JSON message like the following example. The Lambda function sends it back using the callback URL: 63 | ``` bash 64 | $ wscat -c wss://{YOUR-API-ID}.execute-api.{YOUR-REGION}.amazonaws.com/prod 65 | connected (press CTRL+C to quit) 66 | > {"action":"sendmessage", "data":"hello world"} 67 | < hello world 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /stack/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "node index" 3 | } 4 | -------------------------------------------------------------------------------- /stack/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "stage": "dev", 3 | "region": "us-east-1", 4 | "account_id": "YOUR_ACCOUNT_ID" 5 | } 6 | -------------------------------------------------------------------------------- /stack/index.js: -------------------------------------------------------------------------------- 1 | const {AssetCode, Function, Runtime} = require('@aws-cdk/aws-lambda') 2 | const {CfnApi, CfnDeployment, CfnIntegration, CfnRoute, CfnStage} = require('@aws-cdk/aws-apigatewayv2') 3 | const {App, ConcreteDependable, Duration, RemovalPolicy, Stack} = require('@aws-cdk/core') 4 | const {Effect, PolicyStatement, Role, ServicePrincipal} = require('@aws-cdk/aws-iam') 5 | const {AttributeType, Table, BillingMode} = require('@aws-cdk/aws-dynamodb') 6 | const config = require('./config.json') 7 | 8 | class WebsocketDynamoDBStack extends Stack { 9 | constructor(scope, id, props) { 10 | super(scope, id, props) 11 | 12 | // Initialize API 13 | 14 | const name = id + '-api' 15 | const api = new CfnApi(this, name, { 16 | name: 'WebsocketApi', 17 | protocolType: 'WEBSOCKET', 18 | routeSelectionExpression: '$request.body.action', 19 | }) 20 | 21 | // Create tables 22 | 23 | const docsTable = new Table(this, `${name}-docs-table`, { 24 | partitionKey: { // docName 25 | name: 'PartitionKey', 26 | type: AttributeType.STRING, 27 | }, 28 | tableName: 'docs', 29 | billingMode: BillingMode.PAY_PER_REQUEST, 30 | removalPolicy: RemovalPolicy.DESTROY, // TODO for prod use RETAIN 31 | }) 32 | 33 | const connectionsTable = new Table(this, `${name}-connections-table`, { 34 | partitionKey: { // connectionId 35 | name: 'PartitionKey', 36 | type: AttributeType.STRING, 37 | }, 38 | tableName: 'connections', 39 | billingMode: BillingMode.PAY_PER_REQUEST, 40 | removalPolicy: RemovalPolicy.DESTROY, // TODO for prod use RETAIN 41 | }) 42 | 43 | connectionsTable.addGlobalSecondaryIndex({ 44 | indexName: 'DocNameIndex', 45 | partitionKey: { // docName 46 | name: 'DocName', 47 | type: AttributeType.STRING, 48 | }, 49 | }) 50 | 51 | // Initialize lambda 52 | 53 | const messageFunc = new Function(this, `${name}-message-lambda`, { 54 | code: new AssetCode('../server/build'), 55 | handler: 'index.handler', 56 | runtime: Runtime.NODEJS_12_X, 57 | timeout: Duration.seconds(30), 58 | memorySize: 256, 59 | currentVersionOptions: { 60 | // CloudFormation tries to delete lambda before moving alias, this fixes the error: 61 | removalPolicy: RemovalPolicy.RETAIN 62 | }, 63 | initialPolicy: [ 64 | new PolicyStatement({ 65 | actions: [ 66 | 'execute-api:ManageConnections' 67 | ], 68 | resources: [ 69 | 'arn:aws:execute-api:' + config['region'] + ':' + config['account_id'] + ':' + api.ref + '/*' 70 | ], 71 | effect: Effect.ALLOW, 72 | }) 73 | ], 74 | environment: { 75 | DOCS_TABLE_NAME: docsTable.tableName, 76 | CONNECTIONS_TABLE_NAME: connectionsTable.tableName, 77 | REGION: config['region'], 78 | } 79 | }) 80 | 81 | // Add lambda permissions 82 | 83 | docsTable.grantReadWriteData(messageFunc) 84 | connectionsTable.grantReadWriteData(messageFunc) 85 | 86 | // Lambda autoscaling (destroy cold starts) 87 | 88 | messageFunc.currentVersion.addAlias(id+'-message-lambda-alias') 89 | 90 | // const autoScale = alias.addAutoScaling({ 91 | // maxCapacity: 10, 92 | // minCapacity: 2, 93 | // }) 94 | 95 | // autoScale.scaleOnUtilization({ 96 | // utilizationTarget: 0.5, 97 | // policyName: id+'-lambda-scaler', 98 | // }) 99 | 100 | // Access role for the socket api to access the socket lambda 101 | 102 | const policy = new PolicyStatement({ 103 | effect: Effect.ALLOW, 104 | resources: [ 105 | messageFunc.functionArn, 106 | ], 107 | actions: ['lambda:InvokeFunction'], 108 | }) 109 | 110 | const role = new Role(this, `${name}-iam-role`, { 111 | assumedBy: new ServicePrincipal('apigateway.amazonaws.com') 112 | }) 113 | role.addToPolicy(policy) 114 | 115 | // Integrate lambda with Websocket API 116 | 117 | const messageIntegration = new CfnIntegration(this, `${name}-message-route-lambda-integration`, { 118 | apiId: api.ref, 119 | integrationType: 'AWS_PROXY', 120 | integrationUri: 'arn:aws:apigateway:' + config['region'] + ':lambda:path/2015-03-31/functions/' + messageFunc.functionArn + '/invocations', 121 | credentialsArn: role.roleArn, 122 | contentHandlingStrategy: 'CONVERT_TO_BINARY', // see http://amzn.to/39DkYP4 123 | }) 124 | 125 | const messageRoute = new CfnRoute(this, `${name}-message-route`, { 126 | apiId: api.ref, 127 | routeKey: '$default', 128 | authorizationType: 'NONE', 129 | target: 'integrations/' + messageIntegration.ref, 130 | }) 131 | 132 | const connectIntegration = new CfnIntegration(this, `${name}-connect-route-lambda-integration`, { 133 | apiId: api.ref, 134 | integrationType: 'AWS_PROXY', 135 | integrationUri: 'arn:aws:apigateway:' + config['region'] + ':lambda:path/2015-03-31/functions/' + messageFunc.functionArn + '/invocations', 136 | credentialsArn: role.roleArn, 137 | contentHandlingStrategy: 'CONVERT_TO_BINARY', // see http://amzn.to/39DkYP4 138 | }) 139 | 140 | const connectRoute = new CfnRoute(this, `${name}-connect-route`, { 141 | apiId: api.ref, 142 | routeKey: '$connect', 143 | authorizationType: 'NONE', 144 | target: 'integrations/' + connectIntegration.ref, 145 | }) 146 | 147 | const disconnectIntegration = new CfnIntegration(this, `${name}-disconnect-route-lambda-integration`, { 148 | apiId: api.ref, 149 | integrationType: 'AWS_PROXY', 150 | integrationUri: 'arn:aws:apigateway:' + config['region'] + ':lambda:path/2015-03-31/functions/' + messageFunc.functionArn + '/invocations', 151 | credentialsArn: role.roleArn, 152 | contentHandlingStrategy: 'CONVERT_TO_BINARY', // see http://amzn.to/39DkYP4 153 | }) 154 | 155 | const disconnectRoute = new CfnRoute(this, `${name}-disconnect-route`, { 156 | apiId: api.ref, 157 | routeKey: '$disconnect', 158 | authorizationType: 'NONE', 159 | target: 'integrations/' + disconnectIntegration.ref, 160 | }) 161 | 162 | const deployment = new CfnDeployment(this, `${name}-deployment`, { 163 | apiId: api.ref, 164 | }) 165 | 166 | new CfnStage(this, `${name}-stage`, { 167 | apiId: api.ref, 168 | autoDeploy: true, 169 | deploymentId: deployment.ref, 170 | stageName: config['stage'], 171 | }) 172 | 173 | const dependencies = new ConcreteDependable() 174 | dependencies.add(messageRoute) 175 | dependencies.add(connectRoute) 176 | dependencies.add(disconnectRoute) 177 | deployment.node.addDependency(dependencies) 178 | } 179 | } 180 | 181 | const app = new App() 182 | 183 | new WebsocketDynamoDBStack(app, 'yjs') 184 | -------------------------------------------------------------------------------- /stack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-websocket-lambda-dynamodb", 3 | "version": "1.0.0", 4 | "description": "Use of an Application Load Balancer with an AutoScaling Group", 5 | "private": true, 6 | "scripts": { 7 | "deploy": "npm run build --prefix ../server && cdk deploy" 8 | }, 9 | "author": { 10 | "name": "Amazon Web Services", 11 | "url": "https://aws.amazon.com", 12 | "organization": true 13 | }, 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "@aws-cdk/aws-apigatewayv2": "*", 17 | "@aws-cdk/aws-dynamodb": "*", 18 | "@aws-cdk/aws-lambda": "*", 19 | "@aws-cdk/core": "*", 20 | "@types/node": "^10.17.0", 21 | "aws-cdk": "^1.85.0", 22 | "eslint": "^7.17.0" 23 | } 24 | } 25 | --------------------------------------------------------------------------------