Skip to content

Commit

Permalink
basic implementation of the Operation and CollaborationManager (#76)
Browse files Browse the repository at this point in the history
* basic implementation of the Operation and CollaborationManager

* use switch/case and upgrade ts

* use index builder + format fixes

* Apply suggestions from code review

Co-authored-by: Peter <[email protected]>

* fix ci

---------

Co-authored-by: Peter <[email protected]>
  • Loading branch information
nikmel2803 and neSpecc authored Aug 28, 2024
1 parent 1c40976 commit 927cafb
Show file tree
Hide file tree
Showing 15 changed files with 671 additions and 103 deletions.
27 changes: 27 additions & 0 deletions packages/collaboration-manager/.eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
extends:
- codex/ts

ignorePatterns:
- node_modules
- dist

# Eslint seems to not recognize WeakMap as a global
globals:
WeakMap: readonly

plugins:
- import

rules:
import/extensions:
- error
- always
'@typescript-eslint/no-unsafe-declaration-merging':
- 0

overrides:
- files:
- '**/*.test.ts'
- '**/*.spec.ts'
env:
jest: true
24 changes: 24 additions & 0 deletions packages/collaboration-manager/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Swap the comments on the following lines if you don't wish to use zero-installs
# Documentation here: https://yarnpkg.com/features/zero-installs
#!.yarn/cache
#.pnp.*

# IDE
.idea/*

node_modules/*
dist/*

# tests
coverage/
reports/

# stryker temp files
.stryker-tmp
15 changes: 15 additions & 0 deletions packages/collaboration-manager/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type JestConfigWithTsJest, createDefaultEsmPreset } from 'ts-jest';

export default {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: [ '<rootDir>/src/**/*.spec.ts' ],
modulePathIgnorePatterns: [ '<rootDir>/.*/__mocks__', '<rootDir>/.*/mocks' ],
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1',
},
transform: {
...createDefaultEsmPreset().transform,
},
} as JestConfigWithTsJest;
38 changes: 38 additions & 0 deletions packages/collaboration-manager/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@editorjs/collaboration-manager",
"version": "0.0.0",
"packageManager": "[email protected]",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"dev": "tsc --project tsconfig.build.json --watch",
"lint": "eslint ./src",
"lint:ci": "yarn lint --max-warnings 0",
"lint:fix": "yarn lint --fix",
"test": "node --experimental-vm-modules $(yarn bin jest)",
"test:coverage": "yarn test --coverage=true",
"test:mutations": "stryker run",
"clear": "rm -rf ./dist && rm -rf ./tsconfig.build.tsbuildinfo"
},
"dependencies": {
"@editorjs/model": "workspace:^"
},
"devDependencies": {
"@jest/globals": "^29.7.0",
"@stryker-mutator/core": "^7.0.2",
"@stryker-mutator/jest-runner": "^7.0.2",
"@stryker-mutator/typescript-checker": "^7.0.2",
"@types/eslint": "^8",
"@types/jest": "^29.5.12",
"eslint": "^8.38.0",
"eslint-config-codex": "^1.7.2",
"eslint-plugin-import": "^2.29.0",
"jest": "^29.7.0",
"stryker-cli": "^1.0.2",
"ts-jest": "^29.2.5",
"ts-node": "^10.9.2",
"typescript": "^5.5.4"
}
}
87 changes: 87 additions & 0 deletions packages/collaboration-manager/src/CollaborationManager.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-magic-numbers */
import { createDataKey, IndexBuilder } from '@editorjs/model';
import { EditorJSModel } from '@editorjs/model';
import { CollaborationManager } from './CollaborationManager.js';
import { Operation, OperationType } from './Operation.js';

describe('CollaborationManager', () => {
it('should add text on apply Insert Operation', () => {
const model = new EditorJSModel({
blocks: [ {
name: 'paragraph',
data: {
text: {
value: '',
$t: 't',
},
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([0, 4])
.build();
const operation = new Operation(OperationType.Insert, index, {
prevValue: '',
newValue: 'test',
});

collaborationManager.applyOperation(operation);
expect(model.serialized).toStrictEqual({
blocks: [ {
name: 'paragraph',
tunes: {},
data: {
text: {
$t: 't',
value: 'test',
fragments: [],
},
},
} ],
properties: {},
});
});


it('should remove text on apply Remove Operation', () => {
const model = new EditorJSModel({
blocks: [ {
name: 'paragraph',
data: {
text: {
value: 'hel11lo',
$t: 't',
},
},
} ],
});
const collaborationManager = new CollaborationManager(model);
const index = new IndexBuilder().addBlockIndex(0)
.addDataKey(createDataKey('text'))
.addTextRange([
3, 5])
.build();
const operation = new Operation(OperationType.Delete, index, {
prevValue: '11',
newValue: '',
});

collaborationManager.applyOperation(operation);
expect(model.serialized).toStrictEqual({
blocks: [ {
name: 'paragraph',
tunes: {},
data: {
text: {
$t: 't',
value: 'hello',
fragments: [],
},
},
} ],
properties: {},
});
});
});
78 changes: 78 additions & 0 deletions packages/collaboration-manager/src/CollaborationManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { EditorJSModel, ModelEvents } from '@editorjs/model';
import { EventType, TextAddedEvent, TextRemovedEvent } from '@editorjs/model';
import { Operation, OperationType } from './Operation.js';

/**
* CollaborationManager listens to EditorJSModel events and applies operations
*/
export class CollaborationManager {
/**
* EditorJSModel instance to listen to and apply operations
*/
#model: EditorJSModel;

/**
* Creates an instance of CollaborationManager
*
* @param model - EditorJSModel instance to listen to and apply operations
*/
constructor(model: EditorJSModel) {
this.#model = model;
model.addEventListener(EventType.Changed, this.#handleEvent.bind(this));
}

/**
* Applies operation to the model
*
* @param operation - operation to apply
*/
public applyOperation(operation: Operation): void {
const { blockIndex, dataKey, textRange } = operation.index;

if (blockIndex == undefined || dataKey == undefined || textRange == undefined) {
throw new Error('Unsupported index');
}

switch (operation.type) {
case OperationType.Insert:
this.#model.insertText(blockIndex, dataKey, operation.data.newValue);
break;
case OperationType.Delete:
this.#model.removeText(blockIndex, dataKey, textRange[0], textRange[1]);
break;
case OperationType.Modify:
console.log('modify operation is not implemented yet');
// this.#model.insertText(blockIndex, dataKey, operation.data.newValue);
break;
default:
throw new Error('Unknown operation type');
}
}

/**
* Handles EditorJSModel events
*
* @param e - event to handle
*/
#handleEvent(e: ModelEvents): void {
let operation: Operation | null = null;

switch (true) {
case (e instanceof TextAddedEvent):
operation = new Operation(OperationType.Insert, e.detail.index, {
prevValue: '',
newValue: e.detail.data,
});
break;
case (e instanceof TextRemovedEvent):
operation = new Operation(OperationType.Delete, e.detail.index, {
prevValue: e.detail.data,
newValue: '',
});
break;
default:
console.error('Unknown event type', e);
}
console.log('operation', operation);
}
}
59 changes: 59 additions & 0 deletions packages/collaboration-manager/src/Operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Index } from '@editorjs/model';

/**
* Type of the operation
*/
export enum OperationType {
Insert = 'insert',
Delete = 'delete',
Modify = 'modify'
}

/**
* Data for the operation
*/
export interface OperationData {
/**
* Value before the operation
*/
prevValue: string;

/**
* Value after the operation
*/
newValue: string;
}


/**
* Class representing operation on the document model tree
*/
export class Operation {
/**
* Operation type
*/
public type: OperationType;

/**
* Index in the document model tree
*/
public index: Index;

/**
* Operation data
*/
public data: OperationData;

/**
* Creates an instance of Operation
*
* @param type - operation type
* @param index - index in the document model tree
* @param data - operation data
*/
constructor(type: OperationType, index: Index, data: OperationData) {
this.type = type;
this.index = index;
this.data = data;
}
}
2 changes: 2 additions & 0 deletions packages/collaboration-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './CollaborationManager.js';
export * from './Operation.js';
34 changes: 34 additions & 0 deletions packages/collaboration-manager/stryker.conf.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @ts-check
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
const config = {
_comment:
"This config was generated using 'stryker init'. Please take a look at: https://stryker-mutator.io/docs/stryker-js/configuration/ for more information.",
packageManager: "yarn",
thresholds: {
break: 75,
},
thresholds_comment: "Minimum required coverage. Increase once we're closer to 100%.",
clearTextReporter: {
allowEmojis: true,
},
reporters: [
"html",
"clear-text",
"progress",
"dashboard",
],
testRunner: "jest",
testRunner_comment:
"Take a look at https://stryker-mutator.io/docs/stryker-js/jest-runner for information about the jest plugin.",
coverageAnalysis: "perTest",
tsconfigFile: "tsconfig.json",
checkers: ["typescript"],
timeoutMS: 10000,
mutate: ["./src/**/*.ts", "!./src/**/mocks/*.ts", "!./src/**/__mocks__/*.ts", "!./src/**/*.spec.ts"],
/*
* In some cases PRs might not have any unit-tests
*/
allowEmpty: true,
};

export default config;
13 changes: 13 additions & 0 deletions packages/collaboration-manager/tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"declaration": true
},
"include": [
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
]
}
Loading

1 comment on commit 927cafb

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for ./packages/model

St.
Category Percentage Covered / Total
🟢 Statements 100% 745/745
🟢 Branches 99.5% 199/200
🟢 Functions 99.44% 178/179
🟢 Lines 100% 718/718

Test suite run success

390 tests passing in 24 suites.

Report generated by 🧪jest coverage report action from 927cafb

Please sign in to comment.