TL;DR
typegoose와 Yjs를 같이 사용할 수 있는 라이브러리를 만듬(유지보수 안함)
본론
처음에 Notion, Roam Research, Workflowy같은 어플리케이션을 만들다가 이전에 긱뉴스에서 본 CRDT 관련 글이 인상깊어서 적용해보기로 했다.
yjs는 그 자체로 CRDT 기능에 충실했고 군더더기 없이 심플했다. 그리고 websocket, indexeddb 등을 지원하기 때문에 레퍼런스가 훌륭하다고 생각했다.
처음에는 y-leveldb와 mongodown 을 섞어서 사용하고 있었지만 오류가 많았다.(이 글을 쓰기 오래전에 만들어서 정확히 생각이 안나지만…^^;) 오류를 고치고 고치고 고치다가 결국 어떻게 돌아가는 지 대략 이해하게 되어서 yTypegoose를 만들어서 사용하게 되었다.
- 사용 방법
yTypegoose.createModel('nodes');
const docId = 'node-1';
const doc = new Y.Doc();
await yTypegoose.storeUpdate(docId, Y.encodeStateAsUpdate(doc));
yTypegoose.ts
import * as Y from 'yjs'; | |
import { getModelForClass, prop, ReturnModelType } from '@typegoose/typegoose'; | |
const Y_TYPEGOOSE = { | |
DEFAULT_COLLECTION_NAME: 'ydoc', | |
TYPE: { | |
STATE_VECTOR: 'sv', | |
UPDATE: 'update', | |
META: 'meta', | |
}, | |
}; | |
enum Type { | |
STATE_VECTOR = 'sv', | |
UPDATE = 'update', | |
META = 'meta', | |
} | |
class YDoc { | |
@prop({ required: true, index: true }) | |
name!: string; | |
@prop({ required: true, enum: Type }) | |
type!: string; | |
@prop({ required: true }) | |
value!: Buffer; | |
} | |
let YDocModel: ReturnModelType<typeof YDoc>; | |
const createModel = ( | |
collection = Y_TYPEGOOSE.DEFAULT_COLLECTION_NAME, | |
): void => { | |
YDocModel = getModelForClass(YDoc, { | |
schemaOptions: { collection }, | |
}); | |
}; | |
const getUpdates = async (name: string): Promise<Uint8Array[]> => { | |
const updates = await YDocModel.find({ name, type: 'update' }) | |
.sort({ _id: -1 }) | |
.select({ value: 1 }); | |
return updates.map((update) => new Uint8Array(update.value)); | |
}; | |
const getYDoc = async (name: string): Promise<Y.Doc> => { | |
const updates = await getUpdates(name); | |
const ydoc = new Y.Doc(); | |
ydoc.transact(() => { | |
for (let i = 0; i < updates.length; i++) { | |
Y.applyUpdate(ydoc, updates[i]); | |
} | |
}); | |
return ydoc; | |
}; | |
const isExistsDoc = async (name: string): Promise<boolean> => { | |
const ret = await YDocModel.exists({ name }); | |
return ret; | |
}; | |
const storeUpdate = async (name: string, value: Uint8Array): Promise<void> => { | |
const existsDoc = await isExistsDoc(name); | |
if (!existsDoc) { | |
const ydoc = new Y.Doc(); | |
Y.applyUpdate(ydoc, value); | |
const sv = Y.encodeStateVector(ydoc); | |
await new YDocModel({ | |
name, | |
type: Y_TYPEGOOSE.TYPE.STATE_VECTOR, | |
value: Buffer.from(sv), | |
}).save(); | |
} | |
await new YDocModel({ | |
name, | |
type: Y_TYPEGOOSE.TYPE.UPDATE, | |
value: Buffer.from(value), | |
}).save(); | |
}; | |
const clearDocument = async (name: string): Promise<void> => { | |
await YDocModel.deleteMany({ name }); | |
}; | |
export default { | |
createModel, | |
getYDoc, | |
storeUpdate, | |
clearDocument, | |
}; |
결론
CRDT를 구현하려면 Yjs를 사용하는 걸 추천한다. 사용하기 쉽고 레퍼런스도 많았다. 그리고 Typegoose와 Yjs를 사용하려면 yTypegoose.ts를 참고하는 것도 나쁘지 않다. 아 그리고 지금 유지보수를 안해서 참고만 하는 게 좋을 거 같다.
Top comments (0)