summaryrefslogtreecommitdiff
path: root/src/main/otClient.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/otClient.ts')
-rw-r--r--src/main/otClient.ts131
1 files changed, 131 insertions, 0 deletions
diff --git a/src/main/otClient.ts b/src/main/otClient.ts
new file mode 100644
index 0000000..7985c66
--- /dev/null
+++ b/src/main/otClient.ts
@@ -0,0 +1,131 @@
+// OT state machine for main process (mirror of renderer otClient)
+import type { OtOp } from './otTypes'
+import { transformOps } from './otTransform'
+
+export type SendFn = (ops: OtOp[], version: number) => void
+export type ApplyFn = (ops: OtOp[]) => void
+
+interface OtState {
+ name: 'synchronized' | 'awaitingConfirm' | 'awaitingWithBuffer'
+ inflight: OtOp[] | null
+ buffer: OtOp[] | null
+ version: number
+}
+
+export class OtClient {
+ private state: OtState
+ private sendFn: SendFn
+ private applyFn: ApplyFn
+
+ constructor(version: number, sendFn: SendFn, applyFn: ApplyFn) {
+ this.state = { name: 'synchronized', inflight: null, buffer: null, version }
+ this.sendFn = sendFn
+ this.applyFn = applyFn
+ }
+
+ get version(): number {
+ return this.state.version
+ }
+
+ get stateName(): string {
+ return this.state.name
+ }
+
+ onLocalOps(ops: OtOp[]) {
+ if (ops.length === 0) return
+
+ switch (this.state.name) {
+ case 'synchronized':
+ this.state = {
+ name: 'awaitingConfirm',
+ inflight: ops,
+ buffer: null,
+ version: this.state.version
+ }
+ this.sendFn(ops, this.state.version)
+ break
+
+ case 'awaitingConfirm':
+ this.state = {
+ name: 'awaitingWithBuffer',
+ inflight: this.state.inflight,
+ buffer: ops,
+ version: this.state.version
+ }
+ break
+
+ case 'awaitingWithBuffer':
+ this.state = {
+ ...this.state,
+ buffer: [...(this.state.buffer || []), ...ops]
+ }
+ break
+ }
+ }
+
+ onAck() {
+ switch (this.state.name) {
+ case 'awaitingConfirm':
+ this.state = {
+ name: 'synchronized',
+ inflight: null,
+ buffer: null,
+ version: this.state.version + 1
+ }
+ break
+
+ case 'awaitingWithBuffer': {
+ const bufferOps = this.state.buffer || []
+ this.state = {
+ name: 'awaitingConfirm',
+ inflight: bufferOps,
+ buffer: null,
+ version: this.state.version + 1
+ }
+ this.sendFn(bufferOps, this.state.version)
+ break
+ }
+
+ case 'synchronized':
+ console.warn('[OtClient:main] unexpected ack in synchronized state')
+ break
+ }
+ }
+
+ onRemoteOps(ops: OtOp[], newVersion: number) {
+ switch (this.state.name) {
+ case 'synchronized':
+ this.state = { ...this.state, version: newVersion }
+ this.applyFn(ops)
+ break
+
+ case 'awaitingConfirm': {
+ const { left: transformedRemote, right: transformedInflight } = transformOps(ops, this.state.inflight || [])
+ this.state = {
+ ...this.state,
+ inflight: transformedInflight,
+ version: newVersion
+ }
+ this.applyFn(transformedRemote)
+ break
+ }
+
+ case 'awaitingWithBuffer': {
+ const { left: remoteAfterInflight, right: inflightAfterRemote } = transformOps(ops, this.state.inflight || [])
+ const { left: remoteAfterBuffer, right: bufferAfterRemote } = transformOps(remoteAfterInflight, this.state.buffer || [])
+ this.state = {
+ ...this.state,
+ inflight: inflightAfterRemote,
+ buffer: bufferAfterRemote,
+ version: newVersion
+ }
+ this.applyFn(remoteAfterBuffer)
+ break
+ }
+ }
+ }
+
+ reset(version: number) {
+ this.state = { name: 'synchronized', inflight: null, buffer: null, version }
+ }
+}