From c24cea3111f3110507644213a4b39c56a84c2c59 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 3 Dec 2023 23:08:58 +0100 Subject: [PATCH] =?UTF-8?q?feat(json-crdt):=20=F0=9F=8E=B8=20start=20debug?= =?UTF-8?q?=20file=20format=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt/file/File.ts | 92 +++++++++++++++++++++++++++++++++ src/json-crdt/file/PatchLog.ts | 25 +++++++++ src/json-crdt/file/constants.ts | 7 +++ src/json-crdt/file/types.ts | 29 +++++++++++ 4 files changed, 153 insertions(+) create mode 100644 src/json-crdt/file/File.ts create mode 100644 src/json-crdt/file/PatchLog.ts create mode 100644 src/json-crdt/file/constants.ts create mode 100644 src/json-crdt/file/types.ts diff --git a/src/json-crdt/file/File.ts b/src/json-crdt/file/File.ts new file mode 100644 index 0000000000..8a66dfad41 --- /dev/null +++ b/src/json-crdt/file/File.ts @@ -0,0 +1,92 @@ +import {Model} from "../model"; +import {PatchLog} from "./PatchLog"; +import {FileModelEncoding} from "./constants"; +import {Encoder as SidecarEncoder} from '../codec/sidecar/binary/Encoder'; +import {Encoder as StructuralEncoderCompact} from '../codec/structural/compact/Encoder'; +import {Encoder as StructuralEncoderVerbose} from '../codec/structural/verbose/Encoder'; +import {encode as encodeCompact} from '../../json-crdt-patch/codec/compact/encode'; +import {encode as encodeVerbose} from '../../json-crdt-patch/codec/verbose/encode'; +import type * as types from "./types"; + +export class File { + public static fromModel(model: Model): File { + return new File(model, PatchLog.fromModel(model)); + } + + constructor( + public readonly model: Model, + public readonly history: PatchLog, + ) {} + + public serialize(params: types.FileSerializeParams = {}): types.FileWriteSequence { + const view = this.model.view(); + const metadata: types.FileMetadata = [ + {}, + FileModelEncoding.SidecarBinary, + ]; + let model: Uint8Array | unknown | null = null; + const modelFormat = params.model ?? 'sidecar'; + switch (modelFormat) { + case 'sidecar': { + metadata[1] = FileModelEncoding.SidecarBinary; + const encoder = new SidecarEncoder(); + const [, uint8] = encoder.encode(this.model); + model = uint8; + break; + } + case 'binary': { + metadata[1] = FileModelEncoding.StructuralBinary; + model = this.model.toBinary(); + break; + } + case 'compact': { + metadata[1] = FileModelEncoding.StructuralCompact; + model = new StructuralEncoderCompact().encode(this.model); + break; + } + case 'verbose': { + metadata[1] = FileModelEncoding.StructuralVerbose; + model = new StructuralEncoderVerbose().encode(this.model); + break; + } + default: + throw new Error(`Invalid model format: ${modelFormat}`); + } + const history: types.FileWriteSequenceHistory = [ + null, + [], + ]; + const patchFormat = params.history ?? 'binary'; + switch (patchFormat) { + case 'binary': { + history[0] = this.history.start.toBinary(); + this.history.patches.forEach(({v}) => { + history[1].push(v.toBinary()); + }); + break; + } + case 'compact': { + history[0] = new StructuralEncoderCompact().encode(this.history.start); + this.history.patches.forEach(({v}) => { + history[1].push(encodeCompact(v)); + }); + break; + } + case 'verbose': { + history[0] = new StructuralEncoderVerbose().encode(this.history.start); + this.history.patches.forEach(({v}) => { + history[1].push(encodeVerbose(v)); + }); + break; + } + default: + throw new Error(`Invalid history format: ${patchFormat}`); + } + return [ + view, + metadata, + model, + history, + ]; + } +} diff --git a/src/json-crdt/file/PatchLog.ts b/src/json-crdt/file/PatchLog.ts new file mode 100644 index 0000000000..7aa07cf7b8 --- /dev/null +++ b/src/json-crdt/file/PatchLog.ts @@ -0,0 +1,25 @@ +import {ITimestampStruct, Patch, ServerClockVector, compare} from "../../json-crdt-patch"; +import {AvlMap} from "../../util/trees/avl/AvlMap"; +import {Model} from "../model"; + +export class PatchLog { + public static fromModel (model: Model): PatchLog { + const start = new Model(model.clock.clone()); + const log = new PatchLog(start); + if (model.api.builder.patch.ops.length) { + const patch = model.api.flush(); + log.push(patch); + } + return log; + } + + public readonly patches = new AvlMap(compare); + + constructor (public readonly start: Model) {} + + public push(patch: Patch): void { + const id = patch.getId(); + if (!id) return; + this.patches.set(id, patch); + } +} diff --git a/src/json-crdt/file/constants.ts b/src/json-crdt/file/constants.ts new file mode 100644 index 0000000000..3ccde424af --- /dev/null +++ b/src/json-crdt/file/constants.ts @@ -0,0 +1,7 @@ +export const enum FileModelEncoding { + None = 0, + SidecarBinary = 1, + StructuralBinary = 5, + StructuralCompact = 6, + StructuralVerbose = 7, +} diff --git a/src/json-crdt/file/types.ts b/src/json-crdt/file/types.ts new file mode 100644 index 0000000000..32b4450530 --- /dev/null +++ b/src/json-crdt/file/types.ts @@ -0,0 +1,29 @@ +import type {FileModelEncoding} from "./constants"; + +export type FileMetadata = [ + map: {}, + modelFormat: FileModelEncoding, +]; + +export type FileWriteSequence = [ + view: unknown | null, + metadata: FileMetadata, + model: Uint8Array | unknown | null, + history: FileWriteSequenceHistory, +]; + +export type FileWriteSequenceHistory = [ + model: Uint8Array | unknown | null, + patches: Array, +]; + +export type FileReadSequence = [ + ...FileWriteSequence, + ...frontier: Array, +]; + +export interface FileSerializeParams { + noView?: boolean; + model?: 'sidecar' | 'binary' | 'compact' | 'verbose'; + history?: 'binary' | 'compact' | 'verbose'; +}