diff options
Diffstat (limited to 'src/main/otClient.ts')
| -rw-r--r-- | src/main/otClient.ts | 131 |
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 } + } +} |
