feat: better client side error handling

This commit is contained in:
Eric Maciel 2021-01-12 09:25:52 -05:00
parent c72c42ec05
commit d4b089a480
5 changed files with 92 additions and 84 deletions

View File

@ -5,14 +5,7 @@ import { CollabAction } from '../model'
export * from './testUtils' export * from './testUtils'
const toJS = (node: any) => { const toJS = (node: any) => JSON.parse(JSON.stringify(node))
try {
return JSON.parse(JSON.stringify(node))
} catch (e) {
console.error('Convert to js failed!!! Return null')
return null
}
}
const cloneNode = (node: any) => toSync(toJS(node)) const cloneNode = (node: any) => toSync(toJS(node))

View File

@ -45,15 +45,38 @@ export const AutomergeConnector = {
let changed: any let changed: any
operations.forEach(op => { for (let i = 0; i < operations.length; i++) {
changed = Automerge.change<SyncDoc>(changed || doc, d => const op = operations[i]
applyOperation(d.children, op)
)
})
changed = Automerge.change(changed || doc, d => { try {
setCursor(e.clientId, e.selection, d, operations, cursorData || {}) changed = Automerge.change<SyncDoc>(changed || doc, d =>
}) applyOperation(d.children, op)
)
} catch (err) {
e.handleError(err, {
type: 'applySlateOps - applyOperation',
automergeChanged: Automerge.save(changed || doc),
operation: op
})
// return early to avoid applying any further operations after we encounter an error
return
}
}
try {
changed = Automerge.change(changed || doc, d => {
setCursor(e.clientId, e.selection, d, operations, cursorData || {})
})
} catch (err) {
e.handleError(err, {
type: 'applySlateOps - setCursor',
clientId: e.clientId,
automergeDocument: Automerge.save(changed || doc),
operations,
cursorData
})
}
e.docSet.setDoc(docId, changed) e.docSet.setDoc(docId, changed)
}, },
@ -63,22 +86,32 @@ export const AutomergeConnector = {
*/ */
receiveDocument: (e: AutomergeEditor, docId: string, data: string) => { receiveDocument: (e: AutomergeEditor, docId: string, data: string) => {
const currentDoc = e.docSet.getDoc(docId) let currentDoc: Automerge.FreezeObject<SyncDoc> | null = null
let externalDoc: Automerge.FreezeObject<SyncDoc> | null = null
let mergedDoc: Automerge.FreezeObject<SyncDoc> | null = null
const externalDoc = Automerge.load<SyncDoc>(data) try {
currentDoc = e.docSet.getDoc(docId)
externalDoc = Automerge.load<SyncDoc>(data)
mergedDoc = Automerge.merge<SyncDoc>(
externalDoc,
currentDoc || Automerge.init()
)
const mergedDoc = Automerge.merge<SyncDoc>( e.docSet.setDoc(docId, mergedDoc)
externalDoc,
currentDoc || Automerge.init()
)
e.docSet.setDoc(docId, mergedDoc) Editor.withoutNormalizing(e, () => {
e.children = toJS(mergedDoc).children
Editor.withoutNormalizing(e, () => { e.onChange()
e.children = toJS(mergedDoc).children })
} catch (err) {
e.onChange() e.handleError(err, {
}) type: 'receiveDocument',
currentDoc: currentDoc && Automerge.save(currentDoc),
externalDoc: externalDoc && Automerge.save(externalDoc),
mergedDoc: mergedDoc && Automerge.save(mergedDoc)
})
}
}, },
/** /**
@ -93,13 +126,20 @@ export const AutomergeConnector = {
) => { ) => {
try { try {
const current = e.docSet.getDoc(docId) const current = e.docSet.getDoc(docId)
const updated = e.connection.receiveMsg(data) const updated = e.connection.receiveMsg(data)
const operations = Automerge.diff(current, updated) const operations = Automerge.diff(current, updated)
if (operations.length) { if (operations.length) {
const slateOps = toSlateOp(operations, current) let slateOps: any[]
try {
slateOps = toSlateOp(operations, current)
} catch (err) {
e.handleError(err, {
type: 'applyOperation - toSlateOp',
operations,
current: Automerge.save(current)
})
}
e.isRemote = true e.isRemote = true
@ -117,13 +157,18 @@ export const AutomergeConnector = {
Promise.resolve().then(_ => (e.isRemote = false)) Promise.resolve().then(_ => (e.isRemote = false))
} }
} catch (e) { } catch (err) {
// unset remove flag // unset remote flag
if (e.isRemote) { if (e.isRemote) {
e.isRemote = false e.isRemote = false
} }
throw e const current = e.docSet.getDoc(docId)
e.handleError(err, {
type: 'applyOperation',
data,
current: current ? Automerge.save(current) : null
})
} }
}, },

View File

@ -2,18 +2,11 @@ import Automerge from 'automerge'
import { Editor } from 'slate' import { Editor } from 'slate'
import { CollabAction, CursorData, SyncDoc } from '@hiveteams/collab-bridge' import { CollabAction, CursorData, SyncDoc } from '@hiveteams/collab-bridge'
interface ErrorData {
docId: string
serializedData: string
opData?: string
slateOperations?: string
}
export interface AutomergeOptions { export interface AutomergeOptions {
docId: string docId: string
cursorData?: CursorData cursorData?: CursorData
preserveExternalHistory?: boolean preserveExternalHistory?: boolean
onError?: (msg: string | Error, data: ErrorData) => void onError?: (msg: string | Error, data: any) => void
} }
export interface AutomergeEditor extends Editor { export interface AutomergeEditor extends Editor {
@ -32,11 +25,11 @@ export interface AutomergeEditor extends Editor {
receiveDocument: (data: string) => void receiveDocument: (data: string) => void
receiveOperation: (data: Automerge.Message) => void receiveOperation: (data: Automerge.Message) => void
gabageCursor: () => void garbageCursor: () => void
onCursor: (data: any) => void onCursor: (data: any) => void
handleError: (err: Error | string, opData?: string) => void handleError: (err: Error | string, data?: any) => void
} }
export interface SocketIOPluginOptions { export interface SocketIOPluginOptions {
@ -44,7 +37,7 @@ export interface SocketIOPluginOptions {
connectOpts: SocketIOClient.ConnectOpts connectOpts: SocketIOClient.ConnectOpts
onConnect?: () => void onConnect?: () => void
onDisconnect?: () => void onDisconnect?: () => void
onError?: (msg: string | Error, data: ErrorData) => void onError?: (msg: string | Error, data: any) => void
} }
export interface WithSocketIOEditor { export interface WithSocketIOEditor {

View File

@ -31,16 +31,10 @@ const withAutomerge = <T extends Editor>(
* Helper function for handling errors * Helper function for handling errors
*/ */
editor.handleError = (err: Error | string, opData?: string) => { editor.handleError = (err: Error | string, data: any = {}) => {
const { docId, cursorData, onError } = options const { onError } = options
if (onError && cursorData) { if (onError) {
const document = editor.docSet.getDoc(docId) onError(err, data)
onError(err, {
docId: docId,
serializedData: document ? Automerge.save(document) : 'No document',
opData,
slateOperations: JSON.stringify(editor.operations)
})
} }
} }
@ -72,12 +66,8 @@ const withAutomerge = <T extends Editor>(
* Clear cursor data * Clear cursor data
*/ */
editor.gabageCursor = () => { editor.garbageCursor = () => {
try { AutomergeConnector.garbageCursor(editor, docId)
AutomergeConnector.garbageCursor(editor, docId)
} catch (err) {
editor.handleError(err)
}
} }
/** /**
@ -87,12 +77,7 @@ const withAutomerge = <T extends Editor>(
const operations = editor.operations const operations = editor.operations
if (!editor.isRemote) { if (!editor.isRemote) {
try { AutomergeConnector.applySlateOps(editor, docId, operations, cursorData)
AutomergeConnector.applySlateOps(editor, docId, operations, cursorData)
} catch (err) {
editor.handleError(err)
}
onChange() onChange()
} }
} }
@ -102,11 +87,7 @@ const withAutomerge = <T extends Editor>(
*/ */
editor.receiveDocument = data => { editor.receiveDocument = data => {
try { AutomergeConnector.receiveDocument(editor, docId, data)
AutomergeConnector.receiveDocument(editor, docId, data)
} catch (err) {
editor.handleError(err, JSON.stringify(data))
}
} }
/** /**
@ -117,16 +98,12 @@ const withAutomerge = <T extends Editor>(
// ignore document updates for differnt docIds // ignore document updates for differnt docIds
if (docId !== data.docId) return if (docId !== data.docId) return
try { AutomergeConnector.applyOperation(
AutomergeConnector.applyOperation( editor,
editor, docId,
docId, data,
data, preserveExternalHistory
preserveExternalHistory )
)
} catch (err) {
editor.handleError(err, JSON.stringify(data))
}
} }
return editor return editor

View File

@ -47,7 +47,7 @@ const withSocketIO = <T extends AutomergeEditor>(
// On socket io disconnect, cleanup cursor and call the provided onDisconnect callback // On socket io disconnect, cleanup cursor and call the provided onDisconnect callback
socket.on('disconnect', () => { socket.on('disconnect', () => {
editor.gabageCursor() editor.garbageCursor()
onDisconnect && onDisconnect() onDisconnect && onDisconnect()
}) })