|
|
@@ -23,7 +23,8 @@ const SINGLE_USER_FLUSH_DELAY = 2000
|
|
|
const MULTI_USER_FLUSH_DELAY = 500
|
|
|
const INFLIGHT_OP_TIMEOUT = 5000 // Retry sending ops after 5 seconds without an ack
|
|
|
const WAIT_FOR_CONNECTION_TIMEOUT = 500
|
|
|
-const FATAL_OP_TIMEOUT = 30000
|
|
|
+const FATAL_OP_TIMEOUT = 45000
|
|
|
+const RECENT_ACK_LIMIT = 2 * SINGLE_USER_FLUSH_DELAY
|
|
|
|
|
|
type Update = Record<string, any>
|
|
|
|
|
|
@@ -42,7 +43,9 @@ export class ShareJsDoc extends EventEmitter {
|
|
|
// @ts-ignore
|
|
|
_doc: Doc
|
|
|
private editorWatchdogManager: EditorWatchdogManager
|
|
|
- private lastAcked: Date | null = null
|
|
|
+ private lastAcked: number | null = null
|
|
|
+ private pendingOpCreatedAt: number | null = null
|
|
|
+ private inflightOpCreatedAt: number | null = null
|
|
|
private queuedMessageTimer: number | null = null
|
|
|
private queuedMessages: Message[] = []
|
|
|
private detachEditorWatchdogManager: (() => void) | null = null
|
|
|
@@ -90,13 +93,19 @@ export class ShareJsDoc extends EventEmitter {
|
|
|
})
|
|
|
this._doc.setFlushDelay(SINGLE_USER_FLUSH_DELAY)
|
|
|
this._doc.on('change', (...args: any[]) => {
|
|
|
+ if (!this.pendingOpCreatedAt) {
|
|
|
+ debugConsole.log('set pendingOpCreatedAt', new Date())
|
|
|
+ this.pendingOpCreatedAt = performance.now()
|
|
|
+ }
|
|
|
return this.trigger('change', ...args)
|
|
|
})
|
|
|
this.editorWatchdogManager = new EditorWatchdogManager({
|
|
|
parent: globalEditorWatchdogManager,
|
|
|
})
|
|
|
this._doc.on('acknowledge', () => {
|
|
|
- this.lastAcked = new Date() // note time of last ack from server for an op we sent
|
|
|
+ this.lastAcked = performance.now() // note time of last ack from server for an op we sent
|
|
|
+ this.inflightOpCreatedAt = null
|
|
|
+ debugConsole.log('unset inflightOpCreatedAt')
|
|
|
this.editorWatchdogManager.onAck() // keep track of last ack globally
|
|
|
return this.trigger('acknowledge')
|
|
|
})
|
|
|
@@ -107,6 +116,10 @@ export class ShareJsDoc extends EventEmitter {
|
|
|
return this.trigger('remoteop', ...args)
|
|
|
})
|
|
|
this._doc.on('flipped_pending_to_inflight', () => {
|
|
|
+ this.inflightOpCreatedAt = this.pendingOpCreatedAt
|
|
|
+ debugConsole.log('set inflightOpCreatedAt from pendingOpCreatedAt')
|
|
|
+ this.pendingOpCreatedAt = null
|
|
|
+ debugConsole.log('unset pendingOpCreatedAt')
|
|
|
return this.trigger('flipped_pending_to_inflight')
|
|
|
})
|
|
|
this._doc.on('saved', () => {
|
|
|
@@ -280,7 +293,7 @@ export class ShareJsDoc extends EventEmitter {
|
|
|
this.connection.id = this.socket.publicId
|
|
|
this._doc.autoOpen = false
|
|
|
this._doc._connectionStateChanged(state)
|
|
|
- return (this.lastAcked = null) // reset the last ack time when connection changes
|
|
|
+ this.lastAcked = null // reset the last ack time when connection changes
|
|
|
}
|
|
|
|
|
|
hasBufferedOps() {
|
|
|
@@ -299,10 +312,18 @@ export class ShareJsDoc extends EventEmitter {
|
|
|
// check if we have received an ack recently (within a factor of two of the single user flush delay)
|
|
|
return (
|
|
|
this.lastAcked !== null &&
|
|
|
- Date.now() - this.lastAcked.getTime() < 2 * SINGLE_USER_FLUSH_DELAY
|
|
|
+ performance.now() - this.lastAcked < RECENT_ACK_LIMIT
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ getInflightOpCreatedAt() {
|
|
|
+ return this.inflightOpCreatedAt
|
|
|
+ }
|
|
|
+
|
|
|
+ getPendingOpCreatedAt() {
|
|
|
+ return this.pendingOpCreatedAt
|
|
|
+ }
|
|
|
+
|
|
|
private attachEditorWatchdogManager(editor: EditorFacade) {
|
|
|
// end-to-end check for edits -> acks, for this very ShareJsdoc
|
|
|
// This will catch a broken connection and missing UX-blocker for the
|