mirror of
https://github.com/outline/outline.git
synced 2026-06-13 19:35:02 +03:00
Compare commits
43 Commits
tom/api-scopes
...
yjs
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bcf5b0292 | |||
| 3b4aa02c67 | |||
| 375d658231 | |||
| 70ea77ce01 | |||
| cea1d808d1 | |||
| bea8b85cf9 | |||
| abeccb8a4c | |||
| c8d3d26044 | |||
| ec5a7d79f5 | |||
| 30d31b35ac | |||
| 8abf2436dd | |||
| 4df75bda7b | |||
| 220546c40a | |||
| b96ffe59db | |||
| 2676a7e8cf | |||
| 5e9e4fb028 | |||
| 551b1620e0 | |||
| b8569ed8de | |||
| 8d1a707dd0 | |||
| 9877cf1f4e | |||
| 50fbcd8d85 | |||
| 359d228771 | |||
| 0347620c75 | |||
| cb362511a5 | |||
| 700db463fc | |||
| a28dfa77ee | |||
| d8bc6515dd | |||
| 0776b78e25 | |||
| 4256e7ec87 | |||
| e723124f8f | |||
| c2fbd78622 | |||
| 17cbeab409 | |||
| 37d456a0fb | |||
| 48a0ba0dec | |||
| f454467bf1 | |||
| f21f660543 | |||
| 50637bc7ce | |||
| acb61d5e0c | |||
| 7dcbaa9c5c | |||
| ae1761e517 | |||
| 7166378c32 | |||
| 2719321430 | |||
| a7f2c7edb3 |
@@ -11,6 +11,10 @@
|
||||
.*/node_modules/react-side-effect/.*
|
||||
.*/node_modules/fbjs/.*
|
||||
.*/node_modules/config-chain/.*
|
||||
.*/node_modules/yjs/.*
|
||||
.*/node_modules/@tommoor/y-prosemirror/.*
|
||||
.*/node_modules/y-protocols/.*
|
||||
.*/node_modules/lib0/.*
|
||||
.*/server/scripts/.*
|
||||
*.test.js
|
||||
|
||||
|
||||
@@ -97,17 +97,55 @@ const StyledEditor = styled(RichMarkdownEditor)`
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.heading-name {
|
||||
pointer-events: none;
|
||||
.ProseMirror {
|
||||
.ProseMirror-yjs-cursor {
|
||||
position: relative;
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
border-left: 1px solid black;
|
||||
border-right: 1px solid black;
|
||||
height: 1em;
|
||||
word-break: normal;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
right: -8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: -1.8em;
|
||||
font-size: 13px;
|
||||
background-color: rgb(250, 129, 0);
|
||||
font-style: normal;
|
||||
line-height: normal;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
font-weight: 500;
|
||||
border-radius: 4px;
|
||||
pointer-events: none;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
> div {
|
||||
opacity: 1;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* pseudo element allows us to add spacing for fixed header */
|
||||
/* ref: https://stackoverflow.com/a/28824157 */
|
||||
.heading-name::before {
|
||||
content: "";
|
||||
display: ${(props) => (props.readOnly ? "block" : "none")};
|
||||
height: 72px;
|
||||
margin: -72px 0 0;
|
||||
.heading-name {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.heading-name:first-child {
|
||||
@@ -149,3 +187,17 @@ const EditorWithRouterAndTheme = withRouter(withTheme(Editor));
|
||||
export default React.forwardRef<Props, typeof Editor>((props, ref) => (
|
||||
<EditorWithRouterAndTheme {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
// > .ProseMirror-yjs-cursor:first-child {
|
||||
// margin-top: 16px;
|
||||
// }
|
||||
|
||||
// p:first-child,
|
||||
// h1:first-child,
|
||||
// h2:first-child,
|
||||
// h3:first-child,
|
||||
// h4:first-child,
|
||||
// h5:first-child,
|
||||
// h6:first-child {
|
||||
// margin-top: 16px;
|
||||
// }
|
||||
|
||||
@@ -10,6 +10,7 @@ class Team extends BaseModel {
|
||||
googleConnected: boolean;
|
||||
sharing: boolean;
|
||||
documentEmbeds: boolean;
|
||||
multiplayerEditor: boolean;
|
||||
guestSignin: boolean;
|
||||
subdomain: ?string;
|
||||
domain: ?string;
|
||||
|
||||
@@ -7,6 +7,7 @@ class User extends BaseModel {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
color: string;
|
||||
isAdmin: boolean;
|
||||
lastActiveAt: string;
|
||||
isSuspended: boolean;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// @flow
|
||||
import {
|
||||
ySyncPlugin,
|
||||
yCursorPlugin,
|
||||
yUndoPlugin,
|
||||
undo,
|
||||
redo,
|
||||
} from "@tommoor/y-prosemirror";
|
||||
import { keymap } from "prosemirror-keymap";
|
||||
import { Extension } from "rich-markdown-editor";
|
||||
// import { IndexeddbPersistence } from "y-indexeddb";
|
||||
import * as Y from "yjs";
|
||||
|
||||
export default class MultiplayerExtension extends Extension {
|
||||
get name() {
|
||||
return "multiplayer";
|
||||
}
|
||||
|
||||
get plugins() {
|
||||
const { user, provider, doc } = this.options;
|
||||
const type = doc.get("prosemirror", Y.XmlFragment);
|
||||
|
||||
provider.on("status", ({ status }) => {
|
||||
if (status === "connected") {
|
||||
provider.awareness.setLocalStateField("user", {
|
||||
color: user.color,
|
||||
name: user.name,
|
||||
id: user.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
provider.once("sync", () => {
|
||||
const assignUser = (tr) => {
|
||||
const clientIds = Array.from(doc.store.clients.keys());
|
||||
|
||||
if (
|
||||
tr.local &&
|
||||
tr.changed.size > 0 &&
|
||||
!clientIds.includes(doc.clientID)
|
||||
) {
|
||||
const permanentUserData = new Y.PermanentUserData(doc);
|
||||
permanentUserData.setUserMapping(doc, doc.clientID, user.id);
|
||||
doc.off("afterTransaction", assignUser);
|
||||
}
|
||||
};
|
||||
|
||||
doc.on("afterTransaction", assignUser);
|
||||
});
|
||||
|
||||
// const dbProvider = new IndexeddbPersistence(doc.documentId, doc);
|
||||
// dbProvider.whenSynced.then(() => {
|
||||
// console.log("loaded data from indexed db");
|
||||
// });
|
||||
|
||||
return [
|
||||
ySyncPlugin(type),
|
||||
yCursorPlugin(provider.awareness),
|
||||
yUndoPlugin(),
|
||||
keymap({
|
||||
"Mod-z": undo,
|
||||
"Mod-y": redo,
|
||||
"Mod-Shift-z": redo,
|
||||
}),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
// Based on example implementation, modified to work with existing sockets
|
||||
// https://github.com/yjs/y-websocket/blob/master/src/y-websocket.js
|
||||
|
||||
// @flow
|
||||
import * as bc from "lib0/broadcastchannel.js";
|
||||
import * as decoding from "lib0/decoding.js";
|
||||
import * as encoding from "lib0/encoding.js";
|
||||
import * as mutex from "lib0/mutex.js";
|
||||
import { Observable } from "lib0/observable.js";
|
||||
import { Socket } from "socket.io-client";
|
||||
import * as awarenessProtocol from "y-protocols/awareness.js";
|
||||
import * as syncProtocol from "y-protocols/sync.js";
|
||||
import * as Y from "yjs";
|
||||
import {
|
||||
MESSAGE_SYNC,
|
||||
MESSAGE_AWARENESS,
|
||||
MESSAGE_QUERY_AWARENESS,
|
||||
} from "shared/constants";
|
||||
|
||||
const readMessage = (
|
||||
provider: WebsocketProvider,
|
||||
buff: Uint8Array,
|
||||
emitSynced: boolean
|
||||
): encoding.Encoder => {
|
||||
const decoder = decoding.createDecoder(buff);
|
||||
const encoder = encoding.createEncoder();
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
|
||||
switch (messageType) {
|
||||
case MESSAGE_SYNC: {
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
const syncMessageType = syncProtocol.readSyncMessage(
|
||||
decoder,
|
||||
encoder,
|
||||
provider.doc,
|
||||
provider
|
||||
);
|
||||
if (
|
||||
emitSynced &&
|
||||
syncMessageType === syncProtocol.messageYjsSyncStep2 &&
|
||||
!provider.synced
|
||||
) {
|
||||
provider.synced = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MESSAGE_QUERY_AWARENESS:
|
||||
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(
|
||||
provider.awareness,
|
||||
Array.from(provider.awareness.getStates().keys())
|
||||
)
|
||||
);
|
||||
break;
|
||||
case MESSAGE_AWARENESS:
|
||||
awarenessProtocol.applyAwarenessUpdate(
|
||||
provider.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
provider
|
||||
);
|
||||
break;
|
||||
default:
|
||||
console.error("Unable to compute message");
|
||||
return encoder;
|
||||
}
|
||||
return encoder;
|
||||
};
|
||||
|
||||
const broadcastMessage = (provider: WebsocketProvider, buff: ArrayBuffer) => {
|
||||
if (provider.wsconnected) {
|
||||
provider.wsPublish(buff);
|
||||
}
|
||||
if (provider.bcconnected) {
|
||||
provider.mux(() => {
|
||||
bc.publish(provider.documentId, buff);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Websocket Provider for Yjs. Syncs the shared document using socket.io socket
|
||||
*/
|
||||
export class WebsocketProvider extends Observable {
|
||||
constructor(
|
||||
socket: Socket,
|
||||
documentId: string,
|
||||
userId: string,
|
||||
doc: Y.Doc,
|
||||
{
|
||||
awareness = new awarenessProtocol.Awareness(doc),
|
||||
resyncInterval = 0,
|
||||
}: {
|
||||
awareness: awarenessProtocol.Awareness,
|
||||
resyncInterval: number,
|
||||
} = {}
|
||||
) {
|
||||
super();
|
||||
this.socket = socket;
|
||||
this.bcChannel = documentId;
|
||||
this.documentId = documentId;
|
||||
this.userId = userId;
|
||||
this.doc = doc;
|
||||
this.awareness = awareness;
|
||||
this.wsconnected = false;
|
||||
this.bcconnected = false;
|
||||
this.shouldConnect = true;
|
||||
this.mux = mutex.createMutex();
|
||||
this._synced = false;
|
||||
this._resyncInterval = 0;
|
||||
|
||||
if (resyncInterval > 0) {
|
||||
this._resyncInterval = setInterval(() => {
|
||||
if (this.ws) {
|
||||
// resend sync step 1
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.writeSyncStep1(encoder, doc);
|
||||
this.wsPublish(encoding.toUint8Array(encoder));
|
||||
}
|
||||
}, resyncInterval);
|
||||
}
|
||||
|
||||
this.doc.on("update", this._updateHandler);
|
||||
|
||||
window.addEventListener("beforeunload", this._unloadHandler);
|
||||
awareness.on("update", this._awarenessUpdateHandler);
|
||||
|
||||
this.connect();
|
||||
}
|
||||
|
||||
_unloadHandler = () => {
|
||||
awarenessProtocol.removeAwarenessStates(
|
||||
this.awareness,
|
||||
[this.doc.clientID],
|
||||
"window unload"
|
||||
);
|
||||
};
|
||||
|
||||
_bcSubscriber = (data: ArrayBuffer) => {
|
||||
this.mux(() => {
|
||||
const encoder = readMessage(this, new Uint8Array(data), false);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoder));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
|
||||
*/
|
||||
_updateHandler = (update: Uint8Array, origin: any) => {
|
||||
if (origin !== this || origin === null) {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.writeUpdate(encoder, update);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
}
|
||||
};
|
||||
|
||||
_awarenessUpdateHandler = ({ added, updated, removed }: any, origin: any) => {
|
||||
const changedClients = added.concat(updated).concat(removed);
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)
|
||||
);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
};
|
||||
|
||||
get synced() {
|
||||
return this._synced;
|
||||
}
|
||||
|
||||
set synced(state: boolean) {
|
||||
if (this._synced !== state) {
|
||||
this._synced = state;
|
||||
this.emit("sync", [state]);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this._resyncInterval !== 0) {
|
||||
clearInterval(this._resyncInterval);
|
||||
}
|
||||
this.disconnect();
|
||||
this.awareness.off("update", this._awarenessUpdateHandler);
|
||||
this.doc.off("update", this._updateHandler);
|
||||
this.awareness.destroy();
|
||||
window.removeEventListener("beforeunload", this._unloadHandler);
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
connectBc() {
|
||||
if (!this.bcconnected) {
|
||||
bc.subscribe(this.bcChannel, this._bcSubscriber);
|
||||
this.bcconnected = true;
|
||||
}
|
||||
|
||||
// send sync step1 to bc
|
||||
this.mux(() => {
|
||||
// write sync step 1
|
||||
const encoderSync = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderSync, MESSAGE_SYNC);
|
||||
syncProtocol.writeSyncStep1(encoderSync, this.doc);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderSync));
|
||||
|
||||
// broadcast local state
|
||||
const encoderState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderState, MESSAGE_SYNC);
|
||||
syncProtocol.writeSyncStep2(encoderState, this.doc);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderState));
|
||||
|
||||
// write queryAwareness
|
||||
const encoderAwarenessQuery = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessQuery, MESSAGE_QUERY_AWARENESS);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessQuery));
|
||||
|
||||
// broadcast local awareness state
|
||||
const encoderAwarenessState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessState, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoderAwarenessState,
|
||||
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
||||
this.doc.clientID,
|
||||
])
|
||||
);
|
||||
bc.publish(this.bcChannel, encoding.toUint8Array(encoderAwarenessState));
|
||||
});
|
||||
}
|
||||
|
||||
disconnectBc() {
|
||||
// broadcast message with local awareness state set to null (indicating disconnect)
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(
|
||||
this.awareness,
|
||||
[this.doc.clientID],
|
||||
new Map()
|
||||
)
|
||||
);
|
||||
broadcastMessage(this, encoding.toUint8Array(encoder));
|
||||
if (this.bcconnected) {
|
||||
bc.unsubscribe(this.bcChannel, this._bcSubscriber);
|
||||
this.bcconnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
wsPublish(data: ArrayBuffer) {
|
||||
if (!data) return;
|
||||
|
||||
this.socket.binary(true).emit("sync", {
|
||||
documentId: this.documentId,
|
||||
userId: this.userId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
_wsMessageHandler = (event: {
|
||||
documentId: string,
|
||||
userId: string,
|
||||
data: ArrayBuffer,
|
||||
}) => {
|
||||
if (event.documentId === this.documentId) {
|
||||
const encoder = readMessage(this, new Uint8Array(event.data), true);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
this.wsPublish(encoding.toUint8Array(encoder));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_wsCloseHandler = () => {
|
||||
awarenessProtocol.removeAwarenessStates(
|
||||
this.awareness,
|
||||
Array.from(this.awareness.getStates().keys()),
|
||||
this
|
||||
);
|
||||
|
||||
this.emit("status", [
|
||||
{
|
||||
status: "disconnected",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
_wsJoinHandler = (event: { documentId: string, userId: string }) => {
|
||||
if (event.userId !== this.userId || event.documentId !== this.documentId) {
|
||||
return;
|
||||
}
|
||||
console.log("user.join");
|
||||
|
||||
this.awareness.setLocalState({});
|
||||
|
||||
this.emit("status", [
|
||||
{
|
||||
status: "connected",
|
||||
},
|
||||
]);
|
||||
|
||||
console.log("writing sync step 1");
|
||||
|
||||
// always send sync step 1 when connected
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.writeSyncStep1(encoder, this.doc);
|
||||
this.wsPublish(encoding.toUint8Array(encoder));
|
||||
|
||||
// broadcast local awareness state
|
||||
if (this.awareness.getLocalState() !== null) {
|
||||
console.log("broadcast awareness state");
|
||||
|
||||
const encoderAwarenessState = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoderAwarenessState, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoderAwarenessState,
|
||||
awarenessProtocol.encodeAwarenessUpdate(this.awareness, [
|
||||
this.doc.clientID,
|
||||
])
|
||||
);
|
||||
|
||||
this.wsPublish(encoding.toUint8Array(encoderAwarenessState));
|
||||
}
|
||||
};
|
||||
|
||||
connectWs() {
|
||||
this.socket.on("document.sync", this._wsMessageHandler);
|
||||
this.socket.on("disconnect", this._wsCloseHandler);
|
||||
this.socket.on("user.join", this._wsJoinHandler);
|
||||
}
|
||||
|
||||
disconnectWs() {
|
||||
this.socket.off("document.sync", this._wsMessageHandler);
|
||||
this.socket.off("disconnect", this._wsCloseHandler);
|
||||
this.socket.off("user.join", this._wsJoinHandler);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.shouldConnect = false;
|
||||
this.disconnectWs();
|
||||
this.disconnectBc();
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.shouldConnect = true;
|
||||
|
||||
if (!this.wsconnected) {
|
||||
this.wsconnected = true;
|
||||
this.connectWs();
|
||||
this.connectBc();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { Switch, Route } from "react-router-dom";
|
||||
import Features from "scenes/Settings/Features";
|
||||
import Settings from "scenes/Settings";
|
||||
import Details from "scenes/Settings/Details";
|
||||
import Export from "scenes/Settings/Export";
|
||||
@@ -18,6 +19,7 @@ export default function SettingsRoutes() {
|
||||
<Switch>
|
||||
<Route exact path="/settings" component={Settings} />
|
||||
<Route exact path="/settings/details" component={Details} />
|
||||
<Route exact path="/settings/features" component={Features} />
|
||||
<Route exact path="/settings/security" component={Security} />
|
||||
<Route exact path="/settings/people" component={People} />
|
||||
<Route exact path="/settings/people/:filter" component={People} />
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { RouterHistory, Match } from "react-router-dom";
|
||||
import { withRouter } from "react-router-dom";
|
||||
import { withTheme } from "styled-components";
|
||||
import parseDocumentSlug from "shared/utils/parseDocumentSlug";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import DocumentsStore from "stores/DocumentsStore";
|
||||
import PoliciesStore from "stores/PoliciesStore";
|
||||
import RevisionsStore from "stores/RevisionsStore";
|
||||
@@ -30,6 +31,7 @@ import { matchDocumentEdit, updateDocumentUrl } from "utils/routeHelpers";
|
||||
type Props = {|
|
||||
match: Match,
|
||||
location: LocationWithState,
|
||||
auth: AuthStore,
|
||||
shares: SharesStore,
|
||||
documents: DocumentsStore,
|
||||
policies: PoliciesStore,
|
||||
@@ -217,7 +219,7 @@ class DataLoader extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { location, policies, ui } = this.props;
|
||||
const { location, policies, auth, ui } = this.props;
|
||||
|
||||
if (this.error) {
|
||||
return this.error instanceof OfflineError ? (
|
||||
@@ -227,10 +229,11 @@ class DataLoader extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
const team = auth.team;
|
||||
const document = this.document;
|
||||
const revision = this.revision;
|
||||
|
||||
if (!document) {
|
||||
if (!document || !team) {
|
||||
return (
|
||||
<>
|
||||
<Loading location={location} />
|
||||
@@ -240,21 +243,36 @@ class DataLoader extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const abilities = policies.abilities(document.id);
|
||||
const key = this.isEditing ? "editing" : "read-only";
|
||||
const key = team.multiplayerEditor
|
||||
? ""
|
||||
: this.isEditing
|
||||
? "editing"
|
||||
: "read-only";
|
||||
|
||||
return (
|
||||
<SocketPresence documentId={document.id} isEditing={this.isEditing}>
|
||||
{this.isEditing && <HideSidebar ui={ui} />}
|
||||
<DocumentComponent
|
||||
key={key}
|
||||
document={document}
|
||||
revision={revision}
|
||||
abilities={abilities}
|
||||
location={location}
|
||||
readOnly={!this.isEditing || !abilities.update || document.isArchived}
|
||||
onSearchLink={this.onSearchLink}
|
||||
onCreateLink={this.onCreateLink}
|
||||
/>
|
||||
<SocketPresence
|
||||
documentId={document.id}
|
||||
userId={auth.user ? auth.user.id : undefined}
|
||||
isMultiplayer={team.multiplayerEditor}
|
||||
>
|
||||
{(multiplayer) => (
|
||||
<>
|
||||
{this.isEditing && <HideSidebar ui={ui} />}
|
||||
<DocumentComponent
|
||||
key={key}
|
||||
document={document}
|
||||
revision={revision}
|
||||
abilities={abilities}
|
||||
location={location}
|
||||
multiplayer={multiplayer}
|
||||
readOnly={
|
||||
!this.isEditing || !abilities.update || document.isArchived
|
||||
}
|
||||
onSearchLink={this.onSearchLink}
|
||||
onCreateLink={this.onCreateLink}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SocketPresence>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Prompt, Route, withRouter } from "react-router-dom";
|
||||
import type { RouterHistory, Match } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
|
||||
import * as Y from "yjs";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import Document from "models/Document";
|
||||
@@ -30,6 +30,8 @@ import Header from "./Header";
|
||||
import KeyboardShortcutsButton from "./KeyboardShortcutsButton";
|
||||
import MarkAsViewed from "./MarkAsViewed";
|
||||
import References from "./References";
|
||||
import MultiplayerExtension from "multiplayer/MultiplayerExtension";
|
||||
import { WebsocketProvider } from "multiplayer/WebsocketProvider";
|
||||
import { type LocationWithState, type Theme } from "types";
|
||||
import { isCustomDomain } from "utils/domains";
|
||||
import { emojiToUrl } from "utils/emoji";
|
||||
@@ -60,6 +62,12 @@ type Props = {
|
||||
document: Document,
|
||||
revision: Revision,
|
||||
readOnly: boolean,
|
||||
multiplayer: {
|
||||
isConnected: boolean,
|
||||
isReconnecting: boolean,
|
||||
provider: ?WebsocketProvider,
|
||||
doc: Y.Doc,
|
||||
},
|
||||
onCreateLink: (title: string) => string,
|
||||
onSearchLink: (term: string) => any,
|
||||
theme: Theme,
|
||||
@@ -195,7 +203,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
autosave?: boolean,
|
||||
} = {}
|
||||
) => {
|
||||
const { document } = this.props;
|
||||
const { document, auth } = this.props;
|
||||
|
||||
// prevent saves when we are already saving
|
||||
if (document.isSaving) return;
|
||||
@@ -223,10 +231,14 @@ class DocumentScene extends React.Component<Props> {
|
||||
this.isPublishing = !!options.publish;
|
||||
|
||||
try {
|
||||
const savedDocument = await document.save({
|
||||
...options,
|
||||
lastRevision: this.lastRevision,
|
||||
});
|
||||
let savedDocument = document;
|
||||
if (!auth.team || !auth.team.multiplayerEditor) {
|
||||
savedDocument = await document.save({
|
||||
...options,
|
||||
lastRevision: this.lastRevision,
|
||||
});
|
||||
}
|
||||
|
||||
this.isDirty = false;
|
||||
this.lastRevision = savedDocument.revision;
|
||||
|
||||
@@ -311,8 +323,10 @@ class DocumentScene extends React.Component<Props> {
|
||||
auth,
|
||||
ui,
|
||||
match,
|
||||
multiplayer,
|
||||
} = this.props;
|
||||
const team = auth.team;
|
||||
const user = auth.user;
|
||||
const isShare = !!match.params.shareId;
|
||||
|
||||
const value = revision ? revision.text : document.text;
|
||||
@@ -350,7 +364,12 @@ class DocumentScene extends React.Component<Props> {
|
||||
{!readOnly && (
|
||||
<>
|
||||
<Prompt
|
||||
when={this.isDirty && !this.isUploading}
|
||||
when={
|
||||
this.isDirty &&
|
||||
!this.isUploading &&
|
||||
!!team &&
|
||||
!team.multiplayerEditor
|
||||
}
|
||||
message={DISCARD_CHANGES}
|
||||
/>
|
||||
<Prompt
|
||||
@@ -409,6 +428,16 @@ class DocumentScene extends React.Component<Props> {
|
||||
)}
|
||||
</Notice>
|
||||
)}
|
||||
{team &&
|
||||
multiplayer &&
|
||||
!multiplayer.isConnected &&
|
||||
team.multiplayerEditor && (
|
||||
<Notice muted>
|
||||
Connection lost. Any edits will sync once you’re back
|
||||
online.{" "}
|
||||
{multiplayer.isReconnecting && "Trying to reconnect…"}
|
||||
</Notice>
|
||||
)}
|
||||
<React.Suspense fallback={<LoadingPlaceholder />}>
|
||||
<Flex auto={!readOnly}>
|
||||
{showContents && <Contents headings={headings} />}
|
||||
@@ -422,7 +451,7 @@ class DocumentScene extends React.Component<Props> {
|
||||
title={revision ? revision.title : this.title}
|
||||
document={document}
|
||||
value={readOnly ? value : undefined}
|
||||
defaultValue={value}
|
||||
defaultValue={team && team.multiplayerEditor ? "" : value}
|
||||
disableEmbeds={disableEmbeds}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
@@ -436,6 +465,16 @@ class DocumentScene extends React.Component<Props> {
|
||||
readOnly={readOnly}
|
||||
readOnlyWriteCheckboxes={readOnly && abilities.update}
|
||||
ui={this.props.ui}
|
||||
extensions={
|
||||
team && team.multiplayerEditor && !isShare && !revision
|
||||
? [
|
||||
new MultiplayerExtension({
|
||||
user,
|
||||
...multiplayer,
|
||||
}),
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Flex>
|
||||
{readOnly && !isShare && !revision && (
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "outline-icons";
|
||||
import { transparentize, darken } from "polished";
|
||||
import * as React from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import { withRouter, type RouterHistory } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import breakpoint from "styled-components-breakpoint";
|
||||
import AuthStore from "stores/AuthStore";
|
||||
@@ -50,6 +50,7 @@ type Props = {
|
||||
publishingIsDisabled: boolean,
|
||||
savingIsDisabled: boolean,
|
||||
onDiscard: () => void,
|
||||
history: RouterHistory,
|
||||
onSave: ({
|
||||
done?: boolean,
|
||||
publish?: boolean,
|
||||
@@ -61,7 +62,6 @@ type Props = {
|
||||
class Header extends React.Component<Props> {
|
||||
@observable isScrolled = false;
|
||||
@observable showShareModal = false;
|
||||
@observable redirectTo: ?string;
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener("scroll", this.handleScroll);
|
||||
@@ -78,15 +78,17 @@ class Header extends React.Component<Props> {
|
||||
handleScroll = throttle(this.updateIsScrolled, 50);
|
||||
|
||||
handleEdit = () => {
|
||||
this.redirectTo = editDocumentUrl(this.props.document);
|
||||
this.props.history.push(editDocumentUrl(this.props.document));
|
||||
};
|
||||
|
||||
handleNewFromTemplate = () => {
|
||||
const { document } = this.props;
|
||||
const { document, history } = this.props;
|
||||
|
||||
this.redirectTo = newDocumentUrl(document.collectionId, {
|
||||
templateId: document.id,
|
||||
});
|
||||
history.push(
|
||||
newDocumentUrl(document.collectionId, {
|
||||
templateId: document.id,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
handleSave = () => {
|
||||
@@ -116,8 +118,6 @@ class Header extends React.Component<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} push />;
|
||||
|
||||
const {
|
||||
shares,
|
||||
document,
|
||||
@@ -425,4 +425,4 @@ const Title = styled.div`
|
||||
`};
|
||||
`;
|
||||
|
||||
export default inject("auth", "ui", "policies", "shares")(Header);
|
||||
export default inject("auth", "ui", "policies", "shares")(withRouter(Header));
|
||||
|
||||
@@ -1,77 +1,106 @@
|
||||
// @flow
|
||||
import * as React from "react";
|
||||
import { USER_PRESENCE_INTERVAL } from "shared/constants";
|
||||
import * as Y from "yjs";
|
||||
import { SocketContext } from "components/SocketProvider";
|
||||
import useStores from "hooks/useStores";
|
||||
import { WebsocketProvider } from "multiplayer/WebsocketProvider";
|
||||
|
||||
type Props = {
|
||||
children?: React.Node,
|
||||
children: ({
|
||||
provider: ?WebsocketProvider,
|
||||
isReconnecting: boolean,
|
||||
isConnected: boolean,
|
||||
doc: Y.Doc,
|
||||
}) => React.Node,
|
||||
isMultiplayer: boolean,
|
||||
documentId: string,
|
||||
isEditing: boolean,
|
||||
userId?: string,
|
||||
};
|
||||
|
||||
export default class SocketPresence extends React.Component<Props> {
|
||||
static contextType = SocketContext;
|
||||
previousContext: any;
|
||||
editingInterval: IntervalID;
|
||||
export default function SocketPresence(props: Props) {
|
||||
const { presence } = useStores();
|
||||
const context = React.useContext(SocketContext);
|
||||
const [isConnected, setConnected] = React.useState(
|
||||
context ? context.connected : false
|
||||
);
|
||||
const [isReconnecting, setReconnecting] = React.useState(false);
|
||||
const [doc] = React.useState(() =>
|
||||
props.isMultiplayer ? new Y.Doc() : undefined
|
||||
);
|
||||
const [provider] = React.useState(() =>
|
||||
props.isMultiplayer && props.userId
|
||||
? new WebsocketProvider(context, props.documentId, props.userId, doc)
|
||||
: undefined
|
||||
);
|
||||
|
||||
componentDidMount() {
|
||||
this.editingInterval = setInterval(() => {
|
||||
if (this.props.isEditing) {
|
||||
this.emitPresence();
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
console.log("destroy");
|
||||
provider.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const awareness = provider && provider.awareness;
|
||||
React.useEffect(() => {
|
||||
const onUpdate = () => {
|
||||
presence.updateFromAwareness(props.documentId, awareness);
|
||||
};
|
||||
|
||||
if (awareness) {
|
||||
awareness.on("update", onUpdate);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (awareness) {
|
||||
awareness.off("update", onUpdate);
|
||||
}
|
||||
}, USER_PRESENCE_INTERVAL);
|
||||
this.setupOnce();
|
||||
}
|
||||
};
|
||||
}, [presence, props.documentId, awareness]);
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
this.setupOnce();
|
||||
React.useEffect(() => {
|
||||
console.log("useEffect", context);
|
||||
if (!context) return;
|
||||
|
||||
if (prevProps.isEditing !== this.props.isEditing) {
|
||||
this.emitPresence();
|
||||
}
|
||||
}
|
||||
const emitJoin = () => {
|
||||
if (!context) return;
|
||||
context.emit("join", { documentId: props.documentId });
|
||||
};
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.context) {
|
||||
this.context.emit("leave", { documentId: this.props.documentId });
|
||||
this.context.off("authenticated", this.emitJoin);
|
||||
const updateStatus = () => {
|
||||
setConnected(context.connected);
|
||||
};
|
||||
|
||||
const reconnectingStopped = () => {
|
||||
setReconnecting(false);
|
||||
};
|
||||
|
||||
context.on("connect", updateStatus);
|
||||
context.on("disconnect", updateStatus);
|
||||
context.on("reconnect", reconnectingStopped);
|
||||
context.on("reconnect_attempt", setReconnecting);
|
||||
context.on("reconnect_failed", reconnectingStopped);
|
||||
context.on("authenticated", emitJoin);
|
||||
|
||||
if (context.authenticated) {
|
||||
emitJoin();
|
||||
}
|
||||
|
||||
clearInterval(this.editingInterval);
|
||||
}
|
||||
return () => {
|
||||
if (!context) return;
|
||||
context.emit("leave", { documentId: props.documentId });
|
||||
context.off("authenticated", emitJoin);
|
||||
context.off("connect", updateStatus);
|
||||
context.off("disconnect", updateStatus);
|
||||
context.off("reconnect", reconnectingStopped);
|
||||
context.off("reconnect_attempt", setReconnecting);
|
||||
context.off("reconnect_failed", reconnectingStopped);
|
||||
};
|
||||
}, [context, props.documentId, props.userId]);
|
||||
|
||||
setupOnce = () => {
|
||||
if (this.context && this.context !== this.previousContext) {
|
||||
this.previousContext = this.context;
|
||||
|
||||
if (this.context.authenticated) {
|
||||
this.emitJoin();
|
||||
}
|
||||
this.context.on("authenticated", () => {
|
||||
this.emitJoin();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
emitJoin = () => {
|
||||
if (!this.context) return;
|
||||
|
||||
this.context.emit("join", {
|
||||
documentId: this.props.documentId,
|
||||
isEditing: this.props.isEditing,
|
||||
});
|
||||
};
|
||||
|
||||
emitPresence = () => {
|
||||
if (!this.context) return;
|
||||
|
||||
this.context.emit("presence", {
|
||||
documentId: this.props.documentId,
|
||||
isEditing: this.props.isEditing,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return this.props.children || null;
|
||||
}
|
||||
return props.children({
|
||||
isConnected,
|
||||
isReconnecting,
|
||||
provider,
|
||||
doc,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// @flow
|
||||
import { debounce } from "lodash";
|
||||
import { observable } from "mobx";
|
||||
import { observer, inject } from "mobx-react";
|
||||
import * as React from "react";
|
||||
|
||||
import AuthStore from "stores/AuthStore";
|
||||
import UiStore from "stores/UiStore";
|
||||
import CenteredContent from "components/CenteredContent";
|
||||
import Checkbox from "components/Checkbox";
|
||||
import HelpText from "components/HelpText";
|
||||
import PageTitle from "components/PageTitle";
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
@observer
|
||||
class Features extends React.Component<Props> {
|
||||
form: ?HTMLFormElement;
|
||||
@observable multiplayerEditor: boolean;
|
||||
|
||||
componentDidMount() {
|
||||
const { auth } = this.props;
|
||||
if (auth.team) {
|
||||
this.multiplayerEditor = auth.team.multiplayerEditor;
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = async (ev: SyntheticInputEvent<*>) => {
|
||||
switch (ev.target.name) {
|
||||
case "multiplayerEditor":
|
||||
this.multiplayerEditor = ev.target.checked;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
|
||||
await this.props.auth.updateTeam({
|
||||
multiplayerEditor: this.multiplayerEditor,
|
||||
});
|
||||
this.showSuccessMessage();
|
||||
};
|
||||
|
||||
showSuccessMessage = debounce(() => {
|
||||
this.props.ui.showToast("Settings saved");
|
||||
}, 500);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title="Labs" />
|
||||
<h1>Labs</h1>
|
||||
<HelpText>
|
||||
Enable experimental features that are still under development.
|
||||
</HelpText>
|
||||
|
||||
<Checkbox
|
||||
label="Multiplayer editor"
|
||||
name="multiplayerEditor"
|
||||
checked={this.multiplayerEditor}
|
||||
onChange={this.handleChange}
|
||||
note="Allow multiple team members to edit documents at the same time"
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default inject("auth", "ui")(Features);
|
||||
@@ -34,6 +34,32 @@ export default class PresenceStore {
|
||||
this.data.set(documentId, existing);
|
||||
}
|
||||
|
||||
@action updateFromAwareness(documentId: string, awareness: any) {
|
||||
const existing = this.data.get(documentId) || new Map();
|
||||
const clients = Array.from(awareness.states.values());
|
||||
const userIds = clients.map((client) => client.user && client.user.id);
|
||||
|
||||
existing.forEach((value, key) => {
|
||||
if (!userIds.includes(key)) {
|
||||
existing.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
clients.forEach((client) => {
|
||||
if (!client.user) {
|
||||
return;
|
||||
}
|
||||
const userId = client.user.id;
|
||||
|
||||
existing.set(userId, {
|
||||
isEditing: !!client.cursor,
|
||||
userId,
|
||||
});
|
||||
});
|
||||
|
||||
this.data.set(documentId, existing);
|
||||
}
|
||||
|
||||
// called when a user presence message is received – user.presence websocket
|
||||
// message.
|
||||
// While in edit mode a message is sent every USER_PRESENCE_INTERVAL, if
|
||||
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// flow-typed signature: 2db53ec5dbb577a4e27bc465cd4670f3
|
||||
// flow-typed version: <<STUB>>/y-prosemirror_v^0.3.7/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* '@tommoor/y-prosemirror'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
// @flow
|
||||
declare module "@tommoor/y-prosemirror" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module "@tommoor/y-prosemirror/dist/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "@tommoor/y-prosemirror/src/lib" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/cursor-plugin" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/sync-plugin" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/undo-plugin" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "@tommoor/y-prosemirror/src/y-prosemirror" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module "@tommoor/y-prosemirror/dist/test.js" {
|
||||
declare module.exports: $Exports<"@tommoor/y-prosemirror/dist/test">;
|
||||
}
|
||||
declare module "@tommoor/y-prosemirror/src/lib.js" {
|
||||
declare module.exports: $Exports<"@tommoor/y-prosemirror/src/lib">;
|
||||
}
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/cursor-plugin.js" {
|
||||
declare module.exports: $Exports<
|
||||
"@tommoor/y-prosemirror/src/plugins/cursor-plugin"
|
||||
>;
|
||||
}
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/sync-plugin.js" {
|
||||
declare module.exports: $Exports<
|
||||
"@tommoor/y-prosemirror/src/plugins/sync-plugin"
|
||||
>;
|
||||
}
|
||||
declare module "@tommoor/y-prosemirror/src/plugins/undo-plugin.js" {
|
||||
declare module.exports: $Exports<
|
||||
"@tommoor/y-prosemirror/src/plugins/undo-plugin"
|
||||
>;
|
||||
}
|
||||
declare module "@tommoor/y-prosemirror/src/y-prosemirror.js" {
|
||||
declare module.exports: $Exports<"@tommoor/y-prosemirror/src/y-prosemirror">;
|
||||
}
|
||||
Vendored
+377
@@ -0,0 +1,377 @@
|
||||
// flow-typed signature: 97da878aea98698d6c06f8a696bb62af
|
||||
// flow-typed version: <<STUB>>/lib0_v0.2.34/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'lib0'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
// @flow
|
||||
declare module "lib0" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module "lib0/array" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/bin/gendocs" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/binary" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/broadcastchannel" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/buffer" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/component" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/conditions" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/decoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/diff" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dist/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/dom" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/encoding" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/environment" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/error" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/eventloop" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/function" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/indexeddb" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/isomorphic" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/iterator" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/json" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/logging" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/map" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/math" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/metric" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/mutex" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/number" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/object" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/observable" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/pair" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Mt19937" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xoroshiro128plus" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/prng/Xorshift32" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/promise" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/queue" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/random" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/set" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/sort" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/statistics" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/storage" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/string" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/symbol" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/testing" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/time" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/tree" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/url" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "lib0/websocket" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module "lib0/array.js" {
|
||||
declare module.exports: $Exports<"lib0/array">;
|
||||
}
|
||||
declare module "lib0/bin/gendocs.js" {
|
||||
declare module.exports: $Exports<"lib0/bin/gendocs">;
|
||||
}
|
||||
declare module "lib0/binary.js" {
|
||||
declare module.exports: $Exports<"lib0/binary">;
|
||||
}
|
||||
declare module "lib0/broadcastchannel.js" {
|
||||
declare module.exports: $Exports<"lib0/broadcastchannel">;
|
||||
}
|
||||
declare module "lib0/buffer.js" {
|
||||
declare module.exports: $Exports<"lib0/buffer">;
|
||||
}
|
||||
declare module "lib0/component.js" {
|
||||
declare module.exports: $Exports<"lib0/component">;
|
||||
}
|
||||
declare module "lib0/conditions.js" {
|
||||
declare module.exports: $Exports<"lib0/conditions">;
|
||||
}
|
||||
declare module "lib0/decoding.js" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/dist/decoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/decoding">;
|
||||
}
|
||||
declare module "lib0/diff.js" {
|
||||
declare module.exports: $Exports<"lib0/diff">;
|
||||
}
|
||||
declare module "lib0/dist/test.js" {
|
||||
declare module.exports: $Exports<"lib0/dist/test">;
|
||||
}
|
||||
declare module "lib0/dom.js" {
|
||||
declare module.exports: $Exports<"lib0/dom">;
|
||||
}
|
||||
declare module "lib0/encoding.js" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/dist/encoding.cjs" {
|
||||
declare module.exports: $Exports<"lib0/encoding">;
|
||||
}
|
||||
declare module "lib0/environment.js" {
|
||||
declare module.exports: $Exports<"lib0/environment">;
|
||||
}
|
||||
declare module "lib0/error.js" {
|
||||
declare module.exports: $Exports<"lib0/error">;
|
||||
}
|
||||
declare module "lib0/eventloop.js" {
|
||||
declare module.exports: $Exports<"lib0/eventloop">;
|
||||
}
|
||||
declare module "lib0/function.js" {
|
||||
declare module.exports: $Exports<"lib0/function">;
|
||||
}
|
||||
declare module "lib0/index" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/index.js" {
|
||||
declare module.exports: $Exports<"lib0">;
|
||||
}
|
||||
declare module "lib0/indexeddb.js" {
|
||||
declare module.exports: $Exports<"lib0/indexeddb">;
|
||||
}
|
||||
declare module "lib0/isomorphic.js" {
|
||||
declare module.exports: $Exports<"lib0/isomorphic">;
|
||||
}
|
||||
declare module "lib0/iterator.js" {
|
||||
declare module.exports: $Exports<"lib0/iterator">;
|
||||
}
|
||||
declare module "lib0/json.js" {
|
||||
declare module.exports: $Exports<"lib0/json">;
|
||||
}
|
||||
declare module "lib0/logging.js" {
|
||||
declare module.exports: $Exports<"lib0/logging">;
|
||||
}
|
||||
declare module "lib0/map.js" {
|
||||
declare module.exports: $Exports<"lib0/map">;
|
||||
}
|
||||
declare module "lib0/math.js" {
|
||||
declare module.exports: $Exports<"lib0/math">;
|
||||
}
|
||||
declare module "lib0/metric.js" {
|
||||
declare module.exports: $Exports<"lib0/metric">;
|
||||
}
|
||||
declare module "lib0/mutex.js" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/dist/mutex.cjs" {
|
||||
declare module.exports: $Exports<"lib0/mutex">;
|
||||
}
|
||||
declare module "lib0/number.js" {
|
||||
declare module.exports: $Exports<"lib0/number">;
|
||||
}
|
||||
declare module "lib0/object.js" {
|
||||
declare module.exports: $Exports<"lib0/object">;
|
||||
}
|
||||
declare module "lib0/observable.js" {
|
||||
declare module.exports: $Exports<"lib0/observable">;
|
||||
}
|
||||
declare module "lib0/pair.js" {
|
||||
declare module.exports: $Exports<"lib0/pair">;
|
||||
}
|
||||
declare module "lib0/prng.js" {
|
||||
declare module.exports: $Exports<"lib0/prng">;
|
||||
}
|
||||
declare module "lib0/prng/Mt19937.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Mt19937">;
|
||||
}
|
||||
declare module "lib0/prng/Xoroshiro128plus.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xoroshiro128plus">;
|
||||
}
|
||||
declare module "lib0/prng/Xorshift32.js" {
|
||||
declare module.exports: $Exports<"lib0/prng/Xorshift32">;
|
||||
}
|
||||
declare module "lib0/promise.js" {
|
||||
declare module.exports: $Exports<"lib0/promise">;
|
||||
}
|
||||
declare module "lib0/queue.js" {
|
||||
declare module.exports: $Exports<"lib0/queue">;
|
||||
}
|
||||
declare module "lib0/random.js" {
|
||||
declare module.exports: $Exports<"lib0/random">;
|
||||
}
|
||||
declare module "lib0/set.js" {
|
||||
declare module.exports: $Exports<"lib0/set">;
|
||||
}
|
||||
declare module "lib0/sort.js" {
|
||||
declare module.exports: $Exports<"lib0/sort">;
|
||||
}
|
||||
declare module "lib0/statistics.js" {
|
||||
declare module.exports: $Exports<"lib0/statistics">;
|
||||
}
|
||||
declare module "lib0/storage.js" {
|
||||
declare module.exports: $Exports<"lib0/storage">;
|
||||
}
|
||||
declare module "lib0/string.js" {
|
||||
declare module.exports: $Exports<"lib0/string">;
|
||||
}
|
||||
declare module "lib0/symbol.js" {
|
||||
declare module.exports: $Exports<"lib0/symbol">;
|
||||
}
|
||||
declare module "lib0/test.js" {
|
||||
declare module.exports: $Exports<"lib0/test">;
|
||||
}
|
||||
declare module "lib0/testing.js" {
|
||||
declare module.exports: $Exports<"lib0/testing">;
|
||||
}
|
||||
declare module "lib0/time.js" {
|
||||
declare module.exports: $Exports<"lib0/time">;
|
||||
}
|
||||
declare module "lib0/tree.js" {
|
||||
declare module.exports: $Exports<"lib0/tree">;
|
||||
}
|
||||
declare module "lib0/url.js" {
|
||||
declare module.exports: $Exports<"lib0/url">;
|
||||
}
|
||||
declare module "lib0/websocket.js" {
|
||||
declare module.exports: $Exports<"lib0/websocket">;
|
||||
}
|
||||
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
// flow-typed signature: 71e55e30d387153cf804d226f95c0ad8
|
||||
// flow-typed version: <<STUB>>/y-indexeddb_v^9.0.5/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'y-indexeddb'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'y-indexeddb' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'y-indexeddb/dist/test' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'y-indexeddb/src/y-indexeddb' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'y-indexeddb/dist/test.js' {
|
||||
declare module.exports: $Exports<'y-indexeddb/dist/test'>;
|
||||
}
|
||||
declare module 'y-indexeddb/src/y-indexeddb.js' {
|
||||
declare module.exports: $Exports<'y-indexeddb/src/y-indexeddb'>;
|
||||
}
|
||||
Vendored
+67
@@ -0,0 +1,67 @@
|
||||
// flow-typed signature: 3ef5e4dd42591ff15af5f507abd6aa97
|
||||
// flow-typed version: <<STUB>>/y-protocols_v^1.0.1/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'y-protocols'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
// @flow
|
||||
declare module "y-protocols" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module "y-protocols/auth" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "y-protocols/awareness" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "y-protocols/awareness.test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "y-protocols/dist/test" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module "y-protocols/sync" {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module "y-protocols/auth.js" {
|
||||
declare module.exports: $Exports<"y-protocols/auth">;
|
||||
}
|
||||
declare module "y-protocols/awareness.js" {
|
||||
declare module.exports: $Exports<"y-protocols/awareness">;
|
||||
}
|
||||
declare module "y-protocols/dist/awareness.cjs" {
|
||||
declare module.exports: $Exports<"y-protocols/awareness">;
|
||||
}
|
||||
declare module "y-protocols/awareness.test.js" {
|
||||
declare module.exports: $Exports<"y-protocols/awareness.test">;
|
||||
}
|
||||
declare module "y-protocols/dist/test.js" {
|
||||
declare module.exports: $Exports<"y-protocols/dist/test">;
|
||||
}
|
||||
declare module "y-protocols/sync.js" {
|
||||
declare module.exports: $Exports<"y-protocols/sync">;
|
||||
}
|
||||
declare module "y-protocols/dist/sync.cjs" {
|
||||
declare module.exports: $Exports<"y-protocols/sync">;
|
||||
}
|
||||
Vendored
+430
@@ -0,0 +1,430 @@
|
||||
// flow-typed signature: ec89eac307897bef104c76ce1dd14a4d
|
||||
// flow-typed version: <<STUB>>/yjs_v^13.4.1/flow_v0.104.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'yjs'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'yjs' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'yjs/dist/tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/docs/scripts/jquery.min' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/docs/scripts/linenumber' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/docs/scripts/prettify/lang-css' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/docs/scripts/prettify/prettify' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/docs/scripts/tui-doc' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/internals' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/AbstractStruct' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentAny' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentBinary' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentDeleted' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentDoc' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentEmbed' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentFormat' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentJSON' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentString' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/ContentType' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/GC' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/structs/Item' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/AbstractType' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YArray' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YMap' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YText' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YXmlElement' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YXmlEvent' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YXmlFragment' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YXmlHook' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/types/YXmlText' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/AbstractConnector' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/DeleteSet' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/Doc' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/encoding' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/EventHandler' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/ID' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/isParentOf' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/logging' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/PermanentUserData' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/RelativePosition' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/Snapshot' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/StructStore' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/Transaction' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/UndoManager' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/UpdateDecoder' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/UpdateEncoder' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/src/utils/YEvent' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/compatibility.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/doc.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/encoding.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/snapshot.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/testHelper' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/undo-redo.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/y-array.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/y-map.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/y-text.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
declare module 'yjs/tests/y-xml.tests' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'yjs/dist/tests.js' {
|
||||
declare module.exports: $Exports<'yjs/dist/tests'>;
|
||||
}
|
||||
declare module 'yjs/docs/scripts/jquery.min.js' {
|
||||
declare module.exports: $Exports<'yjs/docs/scripts/jquery.min'>;
|
||||
}
|
||||
declare module 'yjs/docs/scripts/linenumber.js' {
|
||||
declare module.exports: $Exports<'yjs/docs/scripts/linenumber'>;
|
||||
}
|
||||
declare module 'yjs/docs/scripts/prettify/lang-css.js' {
|
||||
declare module.exports: $Exports<'yjs/docs/scripts/prettify/lang-css'>;
|
||||
}
|
||||
declare module 'yjs/docs/scripts/prettify/prettify.js' {
|
||||
declare module.exports: $Exports<'yjs/docs/scripts/prettify/prettify'>;
|
||||
}
|
||||
declare module 'yjs/docs/scripts/tui-doc.js' {
|
||||
declare module.exports: $Exports<'yjs/docs/scripts/tui-doc'>;
|
||||
}
|
||||
declare module 'yjs/src/index' {
|
||||
declare module.exports: $Exports<'yjs/src'>;
|
||||
}
|
||||
declare module 'yjs/src/index.js' {
|
||||
declare module.exports: $Exports<'yjs/src'>;
|
||||
}
|
||||
declare module 'yjs/src/internals.js' {
|
||||
declare module.exports: $Exports<'yjs/src/internals'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/AbstractStruct.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/AbstractStruct'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentAny.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentAny'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentBinary.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentBinary'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentDeleted.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentDeleted'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentDoc.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentDoc'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentEmbed.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentEmbed'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentFormat.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentFormat'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentJSON.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentJSON'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentString.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentString'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/ContentType.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/ContentType'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/GC.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/GC'>;
|
||||
}
|
||||
declare module 'yjs/src/structs/Item.js' {
|
||||
declare module.exports: $Exports<'yjs/src/structs/Item'>;
|
||||
}
|
||||
declare module 'yjs/src/types/AbstractType.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/AbstractType'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YArray.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YArray'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YMap.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YMap'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YText.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YText'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YXmlElement.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YXmlElement'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YXmlEvent.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YXmlEvent'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YXmlFragment.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YXmlFragment'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YXmlHook.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YXmlHook'>;
|
||||
}
|
||||
declare module 'yjs/src/types/YXmlText.js' {
|
||||
declare module.exports: $Exports<'yjs/src/types/YXmlText'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/AbstractConnector.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/AbstractConnector'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/DeleteSet.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/DeleteSet'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/Doc.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/Doc'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/encoding.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/encoding'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/EventHandler.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/EventHandler'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/ID.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/ID'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/isParentOf.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/isParentOf'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/logging.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/logging'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/PermanentUserData.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/PermanentUserData'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/RelativePosition.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/RelativePosition'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/Snapshot.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/Snapshot'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/StructStore.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/StructStore'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/Transaction.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/Transaction'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/UndoManager.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/UndoManager'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/UpdateDecoder.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/UpdateDecoder'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/UpdateEncoder.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/UpdateEncoder'>;
|
||||
}
|
||||
declare module 'yjs/src/utils/YEvent.js' {
|
||||
declare module.exports: $Exports<'yjs/src/utils/YEvent'>;
|
||||
}
|
||||
declare module 'yjs/tests/compatibility.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/compatibility.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/doc.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/doc.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/encoding.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/encoding.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/index' {
|
||||
declare module.exports: $Exports<'yjs/tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/index.js' {
|
||||
declare module.exports: $Exports<'yjs/tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/snapshot.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/snapshot.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/testHelper.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/testHelper'>;
|
||||
}
|
||||
declare module 'yjs/tests/undo-redo.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/undo-redo.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/y-array.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/y-array.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/y-map.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/y-map.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/y-text.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/y-text.tests'>;
|
||||
}
|
||||
declare module 'yjs/tests/y-xml.tests.js' {
|
||||
declare module.exports: $Exports<'yjs/tests/y-xml.tests'>;
|
||||
}
|
||||
+8
-3
@@ -9,7 +9,7 @@
|
||||
"build:webpack": "webpack --config webpack.config.prod.js",
|
||||
"build": "yarn clean && yarn build:webpack && yarn build:server",
|
||||
"start": "node ./build/server/index.js",
|
||||
"dev": "nodemon --exec \"yarn build:server && node build/server/index.js\" -e js --ignore build/ --ignore app/",
|
||||
"dev": "nodemon --exec \"yarn build:server && node --inspect build/server/index.js\" -e js --ignore build/ --ignore app/",
|
||||
"lint": "eslint app server shared",
|
||||
"flow": "flow",
|
||||
"deploy": "git push heroku master",
|
||||
@@ -69,6 +69,7 @@
|
||||
"@sentry/node": "^5.23.0",
|
||||
"@tippy.js/react": "^2.2.2",
|
||||
"@tommoor/remove-markdown": "0.3.1",
|
||||
"@tommoor/y-prosemirror": "^0.4.1",
|
||||
"autotrack": "^2.4.1",
|
||||
"aws-sdk": "^2.135.0",
|
||||
"babel-plugin-lodash": "^3.3.4",
|
||||
@@ -115,6 +116,7 @@
|
||||
"koa-sendfile": "2.0.0",
|
||||
"koa-sslify": "2.1.2",
|
||||
"koa-static": "^4.0.1",
|
||||
"lib0": "^0.2.34",
|
||||
"lodash": "^4.17.19",
|
||||
"mammoth": "^1.4.11",
|
||||
"mobx": "4.6.0",
|
||||
@@ -164,7 +166,10 @@
|
||||
"turndown": "^6.0.0",
|
||||
"utf8": "^2.1.0",
|
||||
"uuid": "2.0.2",
|
||||
"validator": "5.2.0"
|
||||
"validator": "5.2.0",
|
||||
"y-indexeddb": "^9.0.5",
|
||||
"y-protocols": "^1.0.1",
|
||||
"yjs": "^13.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.10.5",
|
||||
@@ -200,4 +205,4 @@
|
||||
"js-yaml": "^3.13.1"
|
||||
},
|
||||
"version": "0.50.0"
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ router.post("team.update", auth(), async (ctx) => {
|
||||
sharing,
|
||||
guestSignin,
|
||||
documentEmbeds,
|
||||
multiplayerEditor,
|
||||
} = ctx.body;
|
||||
const user = ctx.state.user;
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
@@ -30,6 +31,10 @@ router.post("team.update", auth(), async (ctx) => {
|
||||
if (sharing !== undefined) team.sharing = sharing;
|
||||
if (documentEmbeds !== undefined) team.documentEmbeds = documentEmbeds;
|
||||
if (guestSignin !== undefined) team.guestSignin = guestSignin;
|
||||
if (multiplayerEditor !== undefined) {
|
||||
team.multiplayerEditor = multiplayerEditor;
|
||||
}
|
||||
|
||||
if (avatarUrl !== undefined) team.avatarUrl = avatarUrl;
|
||||
|
||||
const changes = team.changed();
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// @flow
|
||||
import { yDocToProsemirror } from "@tommoor/y-prosemirror";
|
||||
import { uniq } from "lodash";
|
||||
import { schema, serializer } from "rich-markdown-editor";
|
||||
import * as Y from "yjs";
|
||||
import { Document, Event } from "../models";
|
||||
|
||||
export default async function documentUpdater({
|
||||
documentId,
|
||||
ydoc,
|
||||
userId,
|
||||
done,
|
||||
}: {
|
||||
documentId: string,
|
||||
ydoc: Y.Doc,
|
||||
userId: string,
|
||||
done?: boolean,
|
||||
}) {
|
||||
const document = await Document.findByPk(documentId);
|
||||
const state = Y.encodeStateAsUpdate(ydoc);
|
||||
const node = yDocToProsemirror(schema, ydoc);
|
||||
const text = serializer.serialize(node);
|
||||
|
||||
// extract collaborators from doc user data
|
||||
const pud = new Y.PermanentUserData(ydoc);
|
||||
const pudIds = Array.from(pud.clients.values());
|
||||
const existingIds = document.collaboratorIds;
|
||||
const collaboratorIds = uniq([...pudIds, ...existingIds]);
|
||||
|
||||
if (document.text === text) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Document.update(
|
||||
{
|
||||
text,
|
||||
state: Buffer.from(state),
|
||||
updatedAt: new Date(),
|
||||
lastModifiedById: userId,
|
||||
collaboratorIds,
|
||||
},
|
||||
{
|
||||
hooks: false,
|
||||
where: {
|
||||
id: document.id,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const event = {
|
||||
name: "documents.update",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: userId,
|
||||
data: {
|
||||
multiplayer: true,
|
||||
title: document.title,
|
||||
},
|
||||
};
|
||||
|
||||
if (done) {
|
||||
await Event.create(event);
|
||||
} else {
|
||||
await Event.add(event);
|
||||
}
|
||||
}
|
||||
+167
-64
@@ -1,15 +1,19 @@
|
||||
// @flow
|
||||
import http from "http";
|
||||
import debug from "debug";
|
||||
import IO from "socket.io";
|
||||
import socketRedisAdapter from "socket.io-redis";
|
||||
import SocketAuth from "socketio-auth";
|
||||
import app from "./app";
|
||||
import { Document, Collection, View } from "./models";
|
||||
import { Team, Document, Collection, View } from "./models";
|
||||
import * as multiplayer from "./multiplayer";
|
||||
import policy from "./policies";
|
||||
import { client, subscriber } from "./redis";
|
||||
import { getUserForJWT } from "./utils/jwt";
|
||||
|
||||
const server = http.createServer(app.callback());
|
||||
const log = debug("server");
|
||||
|
||||
let io;
|
||||
|
||||
const { can } = policy;
|
||||
@@ -54,6 +58,7 @@ SocketAuth(io, {
|
||||
}
|
||||
},
|
||||
postAuthenticate: async (socket, data) => {
|
||||
log(`postAuthenticate ${socket.id}`);
|
||||
const { user } = socket.client;
|
||||
|
||||
// the rooms associated with the current team
|
||||
@@ -70,32 +75,116 @@ SocketAuth(io, {
|
||||
|
||||
// join all of the rooms at once
|
||||
socket.join(rooms);
|
||||
},
|
||||
});
|
||||
|
||||
// allow the client to request to join rooms
|
||||
socket.on("join", async (event) => {
|
||||
// user is joining a collection channel, because their permissions have
|
||||
// changed, granting them access.
|
||||
if (event.collectionId) {
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(event.collectionId);
|
||||
// receive multiplayer "sync" messages from other nodes (awareness and doc updates),
|
||||
// applies data to doc if in memory otherwise the request is ignored
|
||||
io.of("/").adapter.customHook = (event, callback) => {
|
||||
io.of("/").clients((err, socketIds) => {
|
||||
if (!socketIds.includes(event.socketId)) {
|
||||
multiplayer.handleRemoteSync(
|
||||
event.socketId,
|
||||
event.documentId,
|
||||
event.userId,
|
||||
event.data
|
||||
);
|
||||
}
|
||||
});
|
||||
callback(true);
|
||||
};
|
||||
|
||||
if (can(user, "read", collection)) {
|
||||
socket.join(`collection-${event.collectionId}`);
|
||||
io.on("connection", (socket) => {
|
||||
socket.on("sync", (event) => {
|
||||
if (!socket.auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = socket.client.user.id;
|
||||
|
||||
// handleJoin must be called before handleSync to ensure authentication
|
||||
// to communicate changes for the document/socket combo. Messages received
|
||||
// before handleJoin will be logged and discarded.
|
||||
multiplayer.handleSync(
|
||||
socket,
|
||||
event.documentId,
|
||||
userId,
|
||||
new Uint8Array(event.data)
|
||||
);
|
||||
|
||||
// forward "sync" messages to all nodes (awareness and doc updates) so
|
||||
// that any docs held in memory can be kept up to date.
|
||||
// TODO: optimize by proactively keeping track of which nodes have doc in
|
||||
// memory? Perf gains for large horizontal scaling.
|
||||
io.of("/").adapter.customRequest(
|
||||
{
|
||||
socketId: socket.id,
|
||||
documentId: event.documentId,
|
||||
userId,
|
||||
data: event.data,
|
||||
},
|
||||
(err) => {
|
||||
if (err) {
|
||||
log(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// user is joining a document channel, because they have navigated to
|
||||
// view a document.
|
||||
if (event.documentId) {
|
||||
const document = await Document.findByPk(event.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
// allow the client to request to join rooms
|
||||
socket.on("join", async (event) => {
|
||||
if (!socket.auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (can(user, "read", document)) {
|
||||
const room = `document-${event.documentId}`;
|
||||
log("join", event.documentId, socket.id);
|
||||
const { user } = socket.client;
|
||||
|
||||
await View.touch(event.documentId, user.id, event.isEditing);
|
||||
// user is joining a collection channel, because their permissions have
|
||||
// changed, granting them access.
|
||||
if (event.collectionId) {
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(event.collectionId);
|
||||
|
||||
if (can(user, "read", collection)) {
|
||||
socket.join(`collection-${event.collectionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// user is joining a document channel, because they have navigated to
|
||||
// view a document.
|
||||
if (event.documentId) {
|
||||
const team = await Team.findByPk(user.teamId);
|
||||
const document = await Document.findByPk(event.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (can(user, "read", document)) {
|
||||
const room = `document-${event.documentId}`;
|
||||
|
||||
// new logic for multiplayer editing completely changes "presence"
|
||||
// detection and propagation, so split at a high-level here.
|
||||
if (team.multiplayerEditor) {
|
||||
log("joined multiplayer", socket.id);
|
||||
socket.join(room, () => {
|
||||
socket.emit("user.join", {
|
||||
userId: user.id,
|
||||
documentId: event.documentId,
|
||||
});
|
||||
|
||||
multiplayer.handleJoin({
|
||||
io,
|
||||
document,
|
||||
socket: socket,
|
||||
documentId: event.documentId,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// old deprecated logic to be removed in the future once multiplayer
|
||||
// has stabilized
|
||||
|
||||
await View.touch(event.documentId, user.id);
|
||||
const editing = await View.findRecentlyEditingByDocument(
|
||||
event.documentId
|
||||
);
|
||||
@@ -109,14 +198,14 @@ SocketAuth(io, {
|
||||
});
|
||||
|
||||
// let this user know who else is already present in the room
|
||||
io.in(room).clients(async (err, sockets) => {
|
||||
io.in(room).clients(async (err, socketIds) => {
|
||||
if (err) throw err;
|
||||
|
||||
// because a single user can have multiple socket connections we
|
||||
// need to make sure that only unique userIds are returned. A Map
|
||||
// makes this easy.
|
||||
let userIds = new Map();
|
||||
for (const socketId of sockets) {
|
||||
for (const socketId of socketIds) {
|
||||
const userId = await client.hget(socketId, "userId");
|
||||
userIds.set(userId, userId);
|
||||
}
|
||||
@@ -129,57 +218,71 @@ SocketAuth(io, {
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// allow the client to request to leave rooms
|
||||
socket.on("leave", (event) => {
|
||||
if (event.collectionId) {
|
||||
socket.leave(`collection-${event.collectionId}`);
|
||||
}
|
||||
if (event.documentId) {
|
||||
const room = `document-${event.documentId}`;
|
||||
socket.leave(room, () => {
|
||||
io.to(room).emit("user.leave", {
|
||||
userId: user.id,
|
||||
documentId: event.documentId,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
// allow the client to request to leave rooms
|
||||
socket.on("leave", (event) => {
|
||||
if (!socket.auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
socket.on("disconnecting", () => {
|
||||
const rooms = Object.keys(socket.rooms);
|
||||
if (event.collectionId) {
|
||||
socket.leave(`collection-${event.collectionId}`);
|
||||
}
|
||||
|
||||
rooms.forEach((room) => {
|
||||
if (room.startsWith("document-")) {
|
||||
const documentId = room.replace("document-", "");
|
||||
io.to(room).emit("user.leave", {
|
||||
userId: user.id,
|
||||
documentId,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("presence", async (event) => {
|
||||
if (event.documentId) {
|
||||
const room = `document-${event.documentId}`;
|
||||
const userId = socket.client.user.id;
|
||||
|
||||
if (event.documentId && socket.rooms[room]) {
|
||||
const view = await View.touch(
|
||||
event.documentId,
|
||||
user.id,
|
||||
event.isEditing
|
||||
);
|
||||
view.user = user;
|
||||
|
||||
io.to(room).emit("user.presence", {
|
||||
userId: user.id,
|
||||
socket.leave(room, () => {
|
||||
io.to(room).emit("user.leave", {
|
||||
userId,
|
||||
documentId: event.documentId,
|
||||
isEditing: event.isEditing,
|
||||
});
|
||||
});
|
||||
|
||||
multiplayer.handleLeave(socket.id, userId, event.documentId);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on("disconnecting", () => {
|
||||
if (!socket.auth) {
|
||||
return;
|
||||
}
|
||||
const rooms = Object.keys(socket.rooms);
|
||||
|
||||
rooms.forEach((room) => {
|
||||
if (room.startsWith("document-")) {
|
||||
const documentId = room.replace("document-", "");
|
||||
const userId = socket.client.user.id;
|
||||
io.to(room).emit("user.leave", {
|
||||
userId,
|
||||
documentId,
|
||||
});
|
||||
multiplayer.handleLeave(socket.id, userId, documentId);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
socket.on("presence", async (event) => {
|
||||
if (!socket.auth) {
|
||||
return;
|
||||
}
|
||||
const room = `document-${event.documentId}`;
|
||||
const { user } = socket.client;
|
||||
|
||||
if (event.documentId && socket.rooms[room]) {
|
||||
const view = await View.touch(event.documentId, user.id, event.isEditing);
|
||||
view.user = user;
|
||||
|
||||
io.to(room).emit("user.presence", {
|
||||
userId: user.id,
|
||||
documentId: event.documentId,
|
||||
isEditing: event.isEditing,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('documents', 'state', {
|
||||
type: Sequelize.BLOB
|
||||
});
|
||||
await queryInterface.addColumn('teams', 'multiplayerEditor', {
|
||||
type: Sequelize.BOOLEAN,
|
||||
defaultValue: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn('documents', 'state');
|
||||
await queryInterface.removeColumn('teams', 'multiplayerEditor');
|
||||
}
|
||||
};
|
||||
@@ -74,6 +74,7 @@ const Document = sequelize.define(
|
||||
template: DataTypes.BOOLEAN,
|
||||
editorVersion: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
state: DataTypes.BLOB,
|
||||
|
||||
// backup contains a record of text at the moment it was converted to v2
|
||||
// this is a safety measure during deployment of new editor and will be
|
||||
@@ -95,6 +96,9 @@ const Document = sequelize.define(
|
||||
},
|
||||
getterMethods: {
|
||||
url: function () {
|
||||
if (!this.title) {
|
||||
return;
|
||||
}
|
||||
const slugifiedTitle = slugify(this.title);
|
||||
return `/doc/${slugifiedTitle}-${this.urlId}`;
|
||||
},
|
||||
|
||||
@@ -66,6 +66,11 @@ const Team = sequelize.define(
|
||||
allowNull: false,
|
||||
defaultValue: true,
|
||||
},
|
||||
multiplayerEditor: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: false,
|
||||
},
|
||||
slackData: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ import uuid from "uuid";
|
||||
import { ValidationError } from "../errors";
|
||||
import { sendEmail } from "../mailer";
|
||||
import { DataTypes, sequelize, encryptedFields } from "../sequelize";
|
||||
import { palette } from "../utils/color";
|
||||
import { publicS3Endpoint, uploadToS3FromUrl } from "../utils/s3";
|
||||
import { Star, Team, Collection, NotificationSetting, ApiKey } from ".";
|
||||
|
||||
@@ -53,7 +54,14 @@ const User = sequelize.define(
|
||||
.createHash("md5")
|
||||
.update(this.email || "")
|
||||
.digest("hex");
|
||||
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${this.name[0]}.png`;
|
||||
return `${DEFAULT_AVATAR_HOST}/avatar/${hash}/${
|
||||
this.name[0]
|
||||
}.png?c=${this.color.replace("#", "")}`;
|
||||
},
|
||||
color() {
|
||||
const idAsHex = crypto.createHash("md5").update(this.id).digest("hex");
|
||||
const idAsNumber = parseInt(idAsHex, 16);
|
||||
return palette[idAsNumber % palette.length];
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// @flow
|
||||
import { prosemirrorToYDoc } from "@tommoor/y-prosemirror";
|
||||
import * as encoding from "lib0/dist/encoding.cjs";
|
||||
import * as mutex from "lib0/dist/mutex.cjs";
|
||||
import { parser } from "rich-markdown-editor";
|
||||
import * as awarenessProtocol from "y-protocols/dist/awareness.cjs";
|
||||
import * as syncProtocol from "y-protocols/dist/sync.cjs";
|
||||
import * as Y from "yjs";
|
||||
import { MESSAGE_AWARENESS, MESSAGE_SYNC } from "../../shared/constants";
|
||||
import { Document } from "../models";
|
||||
|
||||
export default class WSSharedDoc extends Y.Doc {
|
||||
constructor(document: Document, io: any) {
|
||||
super({ gc: true });
|
||||
this.io = io;
|
||||
this.documentId = document.id;
|
||||
this.mux = mutex.createMutex();
|
||||
this.conns = new Map();
|
||||
this.awareness = new awarenessProtocol.Awareness(this);
|
||||
this.awareness.setLocalState(null);
|
||||
|
||||
if (document.state) {
|
||||
Y.applyUpdate(this, document.state);
|
||||
} else {
|
||||
const node = parser.parse(document.text);
|
||||
const ydoc = prosemirrorToYDoc(node);
|
||||
Y.applyUpdate(this, Y.encodeStateAsUpdate(ydoc));
|
||||
}
|
||||
|
||||
this.awareness.on("update", this.awarenessHandler);
|
||||
this.on("update", this.updateHandler);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.off("update", this.updateHandler);
|
||||
this.awareness.off("update", this.awarenessHandler);
|
||||
this.awareness.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
awarenessHandler = (
|
||||
{
|
||||
added,
|
||||
updated,
|
||||
removed,
|
||||
}: { added: Array<number>, updated: Array<number>, removed: Array<number> },
|
||||
socketId: number
|
||||
) => {
|
||||
const changedClients = added.concat(updated, removed);
|
||||
|
||||
if (socketId !== null) {
|
||||
const connControlledIDs = this.conns.get(socketId);
|
||||
|
||||
if (connControlledIDs !== undefined) {
|
||||
added.forEach((clientID) => {
|
||||
connControlledIDs.add(clientID);
|
||||
});
|
||||
removed.forEach((clientID) => {
|
||||
connControlledIDs.delete(clientID);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// broadcast awareness update
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(this.awareness, changedClients)
|
||||
);
|
||||
const data = encoding.toUint8Array(encoder);
|
||||
|
||||
this.io
|
||||
.to(`document-${this.documentId}`)
|
||||
.binary(true)
|
||||
.emit("document.sync", {
|
||||
documentId: this.documentId,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
updateHandler = (update: Uint8Array, origin: any) => {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.writeUpdate(encoder, update);
|
||||
const data = encoding.toUint8Array(encoder);
|
||||
|
||||
this.io
|
||||
.to(`document-${this.documentId}`)
|
||||
.binary(true)
|
||||
.emit("document.sync", {
|
||||
documentId: this.documentId,
|
||||
data,
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// @flow
|
||||
import debug from "debug";
|
||||
import * as decoding from "lib0/dist/decoding.cjs";
|
||||
import * as encoding from "lib0/dist/encoding.cjs";
|
||||
import { debounce } from "lodash";
|
||||
import { Socket } from "socket.io-client";
|
||||
import * as awarenessProtocol from "y-protocols/dist/awareness.cjs";
|
||||
import * as syncProtocol from "y-protocols/dist/sync.cjs";
|
||||
import * as Y from "yjs";
|
||||
import { MESSAGE_AWARENESS, MESSAGE_SYNC } from "../../shared/constants";
|
||||
import documentUpdater from "../commands/documentUpdater";
|
||||
import { Document } from "../models";
|
||||
import WSSharedDoc from "./WSSharedDoc";
|
||||
|
||||
const log = debug("multiplayer");
|
||||
const docs = new Map<string, WSSharedDoc>();
|
||||
const PERSIST_WAIT = 3000;
|
||||
|
||||
export function handleJoin({
|
||||
io,
|
||||
socket,
|
||||
document,
|
||||
documentId,
|
||||
}: {
|
||||
io: any,
|
||||
socket: Socket,
|
||||
document: Document,
|
||||
documentId: string,
|
||||
}) {
|
||||
log(`socket ${socket.id} is joining ${documentId}`);
|
||||
let doc = docs.get(documentId);
|
||||
|
||||
if (!doc) {
|
||||
doc = new WSSharedDoc(document, io);
|
||||
doc.get("prosemirror", Y.XmlFragment);
|
||||
|
||||
if (document.state) {
|
||||
log(`no existing session for ${documentId} – using database state`);
|
||||
Y.applyUpdate(doc, document.state);
|
||||
} else {
|
||||
log(`no existing session for ${documentId} – no database state`);
|
||||
}
|
||||
|
||||
doc.on(
|
||||
"update",
|
||||
debounce(
|
||||
async (update, userId) => {
|
||||
log(`persisting doc (${documentId}) to database`);
|
||||
await documentUpdater({ documentId, ydoc: doc, userId });
|
||||
},
|
||||
PERSIST_WAIT,
|
||||
{
|
||||
maxWait: PERSIST_WAIT * 3,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
docs.set(documentId, doc);
|
||||
}
|
||||
|
||||
doc.conns.set(socket.id, new Set());
|
||||
|
||||
// send sync step 1
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.writeSyncStep1(encoder, doc);
|
||||
|
||||
socket.binary(true).emit("document.sync", {
|
||||
documentId,
|
||||
data: encoding.toUint8Array(encoder),
|
||||
});
|
||||
|
||||
const awarenessStates = doc.awareness.getStates();
|
||||
|
||||
if (awarenessStates.size > 0) {
|
||||
const encoder = encoding.createEncoder();
|
||||
encoding.writeVarUint(encoder, MESSAGE_AWARENESS);
|
||||
encoding.writeVarUint8Array(
|
||||
encoder,
|
||||
awarenessProtocol.encodeAwarenessUpdate(
|
||||
doc.awareness,
|
||||
Array.from(awarenessStates.keys())
|
||||
)
|
||||
);
|
||||
|
||||
socket.binary(true).emit("document.sync", {
|
||||
documentId,
|
||||
data: encoding.toUint8Array(encoder),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleLeave(
|
||||
socketId: string,
|
||||
userId: string,
|
||||
documentId: string
|
||||
) {
|
||||
let doc = docs.get(documentId);
|
||||
|
||||
// this method is called for all leave events, even old-style, so it needs
|
||||
// to handle attempting to leave when there is no existing connection
|
||||
if (!doc || !doc.conns.has(socketId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove ourselves from the awareness state
|
||||
const controlledIds = doc.conns.get(socketId);
|
||||
doc.conns.delete(socketId);
|
||||
awarenessProtocol.removeAwarenessStates(
|
||||
doc.awareness,
|
||||
Array.from(controlledIds),
|
||||
null
|
||||
);
|
||||
|
||||
// last client has left this document connection, time to cleanup and ensure
|
||||
// we've written the latest state to the database.
|
||||
|
||||
// Important note: In multi-server setups this can mean that everyone has left
|
||||
// on an individual server process, however their may still be other clients
|
||||
// connected to other processes
|
||||
// TODO: store connections in redis?
|
||||
if (doc.conns.size === 0) {
|
||||
log(`all clients left doc (${documentId}), persisting…`);
|
||||
await documentUpdater({ documentId, ydoc: doc, userId, done: true });
|
||||
|
||||
doc.destroy();
|
||||
docs.delete(documentId);
|
||||
}
|
||||
}
|
||||
|
||||
export function handleSync(
|
||||
socket: Socket,
|
||||
documentId: string,
|
||||
userId: string,
|
||||
message: Uint8Array
|
||||
) {
|
||||
// check auth with existence of socketId in set
|
||||
let doc = docs.get(documentId);
|
||||
if (!doc) {
|
||||
log(`received sync message but doc (${documentId}) not yet loaded`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.conns.get(socket.id)) {
|
||||
log(
|
||||
`received sync message but socket (${socket.id}) has not joined doc (${documentId})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const encoder = encoding.createEncoder();
|
||||
const decoder = decoding.createDecoder(message);
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
|
||||
switch (messageType) {
|
||||
case MESSAGE_SYNC: {
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.readSyncMessage(decoder, encoder, doc, userId);
|
||||
if (encoding.length(encoder) > 1) {
|
||||
socket.binary(true).emit("document.sync", {
|
||||
documentId,
|
||||
data: encoding.toUint8Array(encoder),
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MESSAGE_AWARENESS: {
|
||||
awarenessProtocol.applyAwarenessUpdate(
|
||||
doc.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
socket.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
export function handleRemoteSync(
|
||||
socketId: string,
|
||||
documentId: string,
|
||||
userId: string,
|
||||
data: {
|
||||
type: string,
|
||||
data: ArrayBuffer,
|
||||
}
|
||||
) {
|
||||
let doc = docs.get(documentId);
|
||||
if (!doc) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
log(`received remote sync message but doc (${documentId}) not loaded`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!doc.conns.get(socketId)) {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
log(
|
||||
`received remote sync message but socket (${socketId}) has not joined doc (${documentId})`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: This is different to handleSync – parsing moved to here so that we
|
||||
// can avoid conversion steps if the doc doesn't already exist in memory.
|
||||
const message = new Uint8Array(Buffer.from(data.data));
|
||||
const encoder = encoding.createEncoder();
|
||||
const decoder = decoding.createDecoder(message);
|
||||
const messageType = decoding.readVarUint(decoder);
|
||||
|
||||
switch (messageType) {
|
||||
case MESSAGE_SYNC: {
|
||||
encoding.writeVarUint(encoder, MESSAGE_SYNC);
|
||||
syncProtocol.readSyncMessage(decoder, encoder, doc, userId);
|
||||
break;
|
||||
}
|
||||
case MESSAGE_AWARENESS: {
|
||||
awarenessProtocol.applyAwarenessUpdate(
|
||||
doc.awareness,
|
||||
decoding.readVarUint8Array(decoder),
|
||||
socketId
|
||||
);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export default function present(team: Team) {
|
||||
sharing: team.sharing,
|
||||
documentEmbeds: team.documentEmbeds,
|
||||
guestSignin: team.guestSignin,
|
||||
multiplayerEditor: team.multiplayerEditor,
|
||||
subdomain: team.subdomain,
|
||||
domain: team.domain,
|
||||
url: team.url,
|
||||
|
||||
@@ -10,6 +10,7 @@ type UserPresentation = {
|
||||
name: string,
|
||||
avatarUrl: ?string,
|
||||
email?: string,
|
||||
color: string,
|
||||
isAdmin: boolean,
|
||||
isSuspended: boolean,
|
||||
};
|
||||
@@ -23,6 +24,7 @@ export default (user: User, options: Options = {}): ?UserPresentation => {
|
||||
userData.isAdmin = user.isAdmin;
|
||||
userData.isSuspended = user.isSuspended;
|
||||
userData.avatarUrl = user.avatarUrl;
|
||||
userData.color = user.color;
|
||||
|
||||
if (options.includeDetails) {
|
||||
userData.email = user.email;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
import type { DocumentEvent, RevisionEvent } from "../events";
|
||||
import { Document, Backlink } from "../models";
|
||||
import { Document, Team, Backlink } from "../models";
|
||||
import { Op } from "../sequelize";
|
||||
import parseDocumentIds from "../utils/parseDocumentIds";
|
||||
import slugify from "../utils/slugify";
|
||||
@@ -76,12 +76,20 @@ export default class Backlinks {
|
||||
break;
|
||||
}
|
||||
case "documents.title_change": {
|
||||
// might as well check
|
||||
const { title, previousTitle } = event.data;
|
||||
if (!previousTitle || title === previousTitle) {
|
||||
break;
|
||||
}
|
||||
|
||||
const document = await Document.findByPk(event.documentId);
|
||||
if (!document) return;
|
||||
|
||||
// might as well check
|
||||
const { title, previousTitle } = event.data;
|
||||
if (!previousTitle || title === previousTitle) break;
|
||||
// TODO: Handle re-writing of titles into CRDT
|
||||
const team = await Team.findByPk(document.teamId);
|
||||
if (team.multiplayerEditor) {
|
||||
break;
|
||||
}
|
||||
|
||||
// update any link titles in documents that lead to this one
|
||||
const backlinks = await Backlink.findAll({
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
import { darken } from "polished";
|
||||
import theme from "../../shared/styles/theme";
|
||||
|
||||
export const palette = [
|
||||
theme.brand.red,
|
||||
theme.brand.blue,
|
||||
theme.brand.purple,
|
||||
theme.brand.pink,
|
||||
theme.brand.marine,
|
||||
theme.brand.green,
|
||||
theme.brand.yellow,
|
||||
darken(0.2, theme.brand.red),
|
||||
darken(0.2, theme.brand.blue),
|
||||
darken(0.2, theme.brand.purple),
|
||||
darken(0.2, theme.brand.pink),
|
||||
darken(0.2, theme.brand.marine),
|
||||
darken(0.2, theme.brand.green),
|
||||
darken(0.2, theme.brand.yellow),
|
||||
];
|
||||
@@ -3,3 +3,7 @@
|
||||
export const USER_PRESENCE_INTERVAL = 5000;
|
||||
export const MAX_AVATAR_DISPLAY = 6;
|
||||
export const MAX_TITLE_LENGTH = 100;
|
||||
|
||||
export const MESSAGE_SYNC = 0;
|
||||
export const MESSAGE_AWARENESS = 1;
|
||||
export const MESSAGE_QUERY_AWARENESS = 3;
|
||||
|
||||
@@ -6,6 +6,11 @@ export const fadeIn = keyframes`
|
||||
to { opacity: 1; }
|
||||
`;
|
||||
|
||||
export const fadeOut = keyframes`
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
`;
|
||||
|
||||
export const fadeAndScaleIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -39,6 +39,7 @@ const colors = {
|
||||
blue: "#3633FF",
|
||||
marine: "#2BC2FF",
|
||||
green: "#42DED1",
|
||||
yellow: "#F5BE31",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1372,6 +1372,40 @@
|
||||
execa "^4.0.0"
|
||||
java-properties "^1.0.0"
|
||||
|
||||
"@rollup/plugin-commonjs@^15.1.0":
|
||||
version "15.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-15.1.0.tgz#1e7d076c4f1b2abf7e65248570e555defc37c238"
|
||||
integrity sha512-xCQqz4z/o0h2syQ7d9LskIMvBSH4PX5PjYdpSSvgS+pQik3WahkQVNWg3D8XJeYjZoVWnIUQYDghuEMRGrmQYQ==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^3.1.0"
|
||||
commondir "^1.0.1"
|
||||
estree-walker "^2.0.1"
|
||||
glob "^7.1.6"
|
||||
is-reference "^1.2.1"
|
||||
magic-string "^0.25.7"
|
||||
resolve "^1.17.0"
|
||||
|
||||
"@rollup/plugin-node-resolve@^9.0.0":
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-9.0.0.tgz#39bd0034ce9126b39c1699695f440b4b7d2b62e6"
|
||||
integrity sha512-gPz+utFHLRrd41WMP13Jq5mqqzHL3OXrfj3/MkSyB6UBIcuNt9j60GCbarzMzdf1VHFpOxfQh/ez7wyadLMqkg==
|
||||
dependencies:
|
||||
"@rollup/pluginutils" "^3.1.0"
|
||||
"@types/resolve" "1.17.1"
|
||||
builtin-modules "^3.1.0"
|
||||
deepmerge "^4.2.2"
|
||||
is-module "^1.0.0"
|
||||
resolve "^1.17.0"
|
||||
|
||||
"@rollup/pluginutils@^3.1.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b"
|
||||
integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==
|
||||
dependencies:
|
||||
"@types/estree" "0.0.39"
|
||||
estree-walker "^1.0.1"
|
||||
picomatch "^2.2.2"
|
||||
|
||||
"@sentry/core@5.23.0":
|
||||
version "5.23.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.23.0.tgz#4d12ee4f5593e66fa5ffde0f9e9164a5468e5cec"
|
||||
@@ -1472,6 +1506,17 @@
|
||||
resolved "https://registry.yarnpkg.com/@tommoor/remove-markdown/-/remove-markdown-0.3.1.tgz#25e7b845d52fcfadf149a3a6a468a931fee7619b"
|
||||
integrity sha512-aM5TtBfBgcUm+B4WWelm2NBAFBk12oNUr67f5lJapSOTkPnwkuzCNwMlsBoDTsRknoZSsUIkcOJB473AnfyqHA==
|
||||
|
||||
"@tommoor/y-prosemirror@^0.4.1":
|
||||
version "0.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@tommoor/y-prosemirror/-/y-prosemirror-0.4.1.tgz#ce3209b09cf090c1125f84c2b8a9456b26aa59e0"
|
||||
integrity sha512-J6KoVtBJKRKrQCzLVAXfbOGh6eD00GiPhPiLK+42RyFSPy4DliFJ9SPgHxaDHOPki70NdSbJ+vy9L8TTV6y5yw==
|
||||
dependencies:
|
||||
"@rollup/plugin-commonjs" "^15.1.0"
|
||||
"@rollup/plugin-node-resolve" "^9.0.0"
|
||||
lib0 "^0.2.34"
|
||||
lodash.flatten "^4.4.0"
|
||||
rollup "^2.32.0"
|
||||
|
||||
"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7":
|
||||
version "7.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.9.tgz#77e59d438522a6fb898fa43dc3455c6e72f3963d"
|
||||
@@ -1510,6 +1555,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.45"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
|
||||
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
|
||||
|
||||
"@types/estree@0.0.39":
|
||||
version "0.0.39"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
|
||||
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
|
||||
|
||||
"@types/events@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
|
||||
@@ -1580,6 +1635,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.0.2.tgz#5bb52ee68d0f8efa9cc0099920e56be6cc4e37f3"
|
||||
integrity sha512-IkVfat549ggtkZUthUzEX49562eGikhSYeVGX97SkMFn+sTZrgRewXjQ4tPKFPCykZHkX1Zfd9OoELGqKU2jJA==
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/stack-utils@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
|
||||
@@ -2762,6 +2824,11 @@ builtin-modules@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-2.0.0.tgz#60b7ef5ae6546bd7deefa74b08b62a43a232648e"
|
||||
integrity sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg==
|
||||
|
||||
builtin-modules@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484"
|
||||
integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==
|
||||
|
||||
builtin-status-codes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
@@ -4527,6 +4594,16 @@ estraverse@^5.1.0:
|
||||
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.1.0.tgz#374309d39fd935ae500e7b92e8a6b4c720e59642"
|
||||
integrity sha512-FyohXK+R0vE+y1nHLoBM7ZTyqRpqAlhdZHCWIWEviFLiGB8b04H6bQs8G+XTthacvT8VuwvteiP7RJSxMs8UEw==
|
||||
|
||||
estree-walker@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700"
|
||||
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
|
||||
|
||||
estree-walker@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0"
|
||||
integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg==
|
||||
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
@@ -5167,7 +5244,7 @@ glob-parent@^5.0.0, glob-parent@~5.1.0:
|
||||
dependencies:
|
||||
is-glob "^4.0.1"
|
||||
|
||||
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
||||
glob@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
|
||||
version "7.1.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
|
||||
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
|
||||
@@ -6173,6 +6250,13 @@ is-redirect@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
|
||||
integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
|
||||
|
||||
is-reference@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7"
|
||||
integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
is-regex@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
|
||||
@@ -6274,6 +6358,11 @@ isomorphic-fetch@2.2.1, isomorphic-fetch@^2.2.1:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isomorphic.js@^0.1.3:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic.js/-/isomorphic.js-0.1.5.tgz#f210e58ba4c96978a991c443cf67283366e9b70f"
|
||||
integrity sha512-MkX5lLQApx/8IAIU31PKvpAZosnu2Jqcj1rM8TzxyA4CR96tv3SgMKQNTCxL58G7696Q57zd7ubHV/hTg+5fNA==
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -7225,6 +7314,13 @@ levn@~0.3.0:
|
||||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
lib0@^0.2.12, lib0@^0.2.28, lib0@^0.2.33, lib0@^0.2.34:
|
||||
version "0.2.34"
|
||||
resolved "https://registry.yarnpkg.com/lib0/-/lib0-0.2.34.tgz#c4479f5f2083894687fcfa9d0b9d9935e35ea008"
|
||||
integrity sha512-cqsVIMPgFlDtgQcpkt7HOY6W3sbYPIe3qxMnbRSwHTgiQancgm+TRDPx28mC6GUZ6lG6Nr0bIWf4Nog6dWUNUg==
|
||||
dependencies:
|
||||
isomorphic.js "^0.1.3"
|
||||
|
||||
lie@~3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
|
||||
@@ -7599,6 +7695,13 @@ macos-release@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
|
||||
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
|
||||
|
||||
magic-string@^0.25.7:
|
||||
version "0.25.7"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051"
|
||||
integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==
|
||||
dependencies:
|
||||
sourcemap-codec "^1.4.4"
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||
@@ -8843,7 +8946,7 @@ pgpass@1.x:
|
||||
dependencies:
|
||||
split "^1.0.0"
|
||||
|
||||
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
|
||||
picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
|
||||
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
|
||||
@@ -9995,6 +10098,13 @@ rollup@^0.41.4:
|
||||
dependencies:
|
||||
source-map-support "^0.4.0"
|
||||
|
||||
rollup@^2.32.0:
|
||||
version "2.32.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.32.1.tgz#625a92c54f5b4d28ada12d618641491d4dbb548c"
|
||||
integrity sha512-Op2vWTpvK7t6/Qnm1TTh7VjEZZkN8RWgf0DHbkKzQBwNf748YhXbozHVefqpPp/Fuyk/PQPAnYsBxAEtlMvpUw==
|
||||
optionalDependencies:
|
||||
fsevents "~2.1.2"
|
||||
|
||||
rope-sequence@^1.3.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.2.tgz#a19e02d72991ca71feb6b5f8a91154e48e3c098b"
|
||||
@@ -10559,6 +10669,11 @@ source-map@~0.4.1:
|
||||
dependencies:
|
||||
amdefine ">=0.0.4"
|
||||
|
||||
sourcemap-codec@^1.4.4:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
|
||||
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
|
||||
|
||||
space-separated-tokens@^1.0.0:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz#85f32c3d10d9682007e917414ddc5c26d1aa6899"
|
||||
@@ -12133,6 +12248,20 @@ xtend@^4.0.0, xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
y-indexeddb@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/y-indexeddb/-/y-indexeddb-9.0.5.tgz#a162526add738a456b7185a6a5626534609ee132"
|
||||
integrity sha512-40VxkqPoK2VxE1vMosS5MfwlHQOvaeLEN89dIkjh7URjZny6bDQOl4yKldaDv9ZosZgYEPyWuWTF3Z92RZ1y+A==
|
||||
dependencies:
|
||||
lib0 "^0.2.12"
|
||||
|
||||
y-protocols@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/y-protocols/-/y-protocols-1.0.1.tgz#7855c900039a02b369590b8ae78bc6e1cbc13c9f"
|
||||
integrity sha512-QP3fCM7c2gGfUi2nqf8gspyO4VW23zv3kNqPNdD3wNxMbuNQenMyoDVZYEo12jzR4RQ3aaDfPK62Sf31SVOmfg==
|
||||
dependencies:
|
||||
lib0 "^0.2.28"
|
||||
|
||||
"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
|
||||
@@ -12238,6 +12367,13 @@ yeast@0.1.2:
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
|
||||
|
||||
yjs@^13.4.4:
|
||||
version "13.4.4"
|
||||
resolved "https://registry.yarnpkg.com/yjs/-/yjs-13.4.4.tgz#3a8b90abf38b14c1d352d9d98098541cf7187d39"
|
||||
integrity sha512-5YUsjK28pzNu2MOvvc+KTSvlnjhNNPiZF828ac0HuPg2rfaex/IBOdZE3bgZ/UEfIUdDPrKNFpXKaP6Z9MzJ1w==
|
||||
dependencies:
|
||||
lib0 "^0.2.33"
|
||||
|
||||
ylru@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f"
|
||||
|
||||
Reference in New Issue
Block a user