├── .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 | * {
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
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 | 
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 |
--------------------------------------------------------------------------------