본문으로 건너뛰기

원격 저장

에디터에 변경사항이 있을 때마다 전체 상태를 전송하는 건 비효율적입니다.
이런 경우를 위해 실시간 데이터 복제 API를 제공합니다.

설치​

npm install @dineug/erd-editor

복제 store는 패키지 루트가 아니라 @dineug/erd-editor/engine.js 하위 경로에 있습니다.
이 진입점은 DOM을 사용하지 않으므로 Web Worker를 비롯해 메인 스레드 밖 어디에서도 동작합니다.
반면 패키지 루트를 import하면 document가 필요한 <erd-editor> 커스텀 엘리먼트가 등록됩니다.

사용​

type ReplicationStore = {
readonly value: string;
readonly runtimeValue: string;
on: (
listeners: Partial<{ change: (change: ReplicationChange) => void }>
) => Unsubscribe;
setInitialValue: (value: string) => void;
dispatch: (actions: Array<AnyAction> | AnyAction) => void;
dispatchSync: (actions: Array<AnyAction> | AnyAction) => void;
destroy: () => void;
};

type ReplicationChange = {
value: string;
changed: boolean;
};

type InjectEngineContext = {
toWidth: (text: string) => number;
};

type CreateReplicationStore = (
context: InjectEngineContext
) => ReplicationStore;

// example
import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({
toWidth: text => text.length * 10,
});

실행 중인 에디터 복제​

액션은 실행 중인 에디터의 shared store에서 가져옵니다.
공동 편집 문서를 참고하세요.

import { createReplicationStore } from '@dineug/erd-editor/engine.js';

const replicationStore = createReplicationStore({ toWidth });
replicationStore.setInitialValue(editor.runtimeValue);

replicationStore.on({
change: ({ value, changed }) => {
if (changed) save(value);
},
});

const sharedStore = editor.getSharedStore();
sharedStore.subscribe(actions => {
replicationStore.dispatch(actions);
});

경계를 넘어가는 것은 변경사항뿐이며 문서 전체는 전송되지 않습니다.
복제본이 변경사항을 반영하고 결과를 자체적으로 직렬화합니다.
change는 그 결과를 value로 전달하며, changed로 저장할 시점을 알 수 있습니다.

4.0.0부터는 위 예시처럼 저장된 파일이나 에디터의 value가 아니라 에디터의 runtimeValue로 복제본을 초기화하세요.
파일에는 저장한 이후의 편집이 없고, value에는 세션에서 삭제된 엔티티가 없습니다.
삭제된 테이블 없이 초기화된 복제본은 에디터가 그 삭제를 되돌리면 테이블을 이름과 컬럼 없이 되살리고, 그 상태로 저장합니다.

InjectEngineContext​

toWidth​

문자열의 너비를 픽셀 단위로 측정합니다.
store에는 DOM이 없어 직접 텍스트를 측정할 수 없으므로, 이름, 데이터 타입, 기본값, 코멘트가 변경될 때마다 이 함수로 컬럼 너비를 다시 계산합니다.
setInitialValue로 문서를 불러올 때마다 모든 테이블과 컬럼도 다시 측정합니다.
4.0.0부터 너비는 runtimeValue에만 들어 있고 value에는 절대 들어가지 않습니다.
따라서 이 릴리스에서 저장한 문서는 어디에서 저장했든, 그곳에서 텍스트가 어떻게 측정되었든 바이트 단위까지 같은 value로 다시 읽힙니다.

에디터와 동일한 방식으로 측정하세요. 에디터 폰트 스택의 400 12px로 측정한 값을 반올림하고 2px 패딩을 더합니다.
그렇지 않으면 runtimeValue의 너비가 화면에 보이는 값과 어긋납니다.

const TEXT_PADDING = 2;
const FONT =
"400 12px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " +
"'Helvetica Neue', 'Open Sans', system-ui, sans-serif, " +
"'Apple Color Emoji', 'Segoe UI Emoji'";

let context = null;

function getContext() {
if (context) return context;

try {
context = new OffscreenCanvas(0, 0).getContext('2d');
if (context) context.font = FONT;
} catch {
// 사용할 수 있는 canvas가 없음
}

return context;
}

function toWidth(text) {
const context = getContext();
const width = context ? context.measureText(text).width : text.length * 10;

return Math.round(width) + TEXT_PADDING;
}

const replicationStore = createReplicationStore({ toWidth });

text.length * 10은 canvas를 사용할 수 없을 때의 대체 값입니다.
렌더링하지 않고 저장만 하는 문서를 다루는 store에는 이 정도로 충분합니다.

ReplicationStore​

value getter​

현재 에디터 상태를 직렬화합니다.
파싱된 객체가 아니라 그대로 저장할 수 있는 JSON 문자열을 반환합니다.

const data = replicationStore.value;

4.0.0부터 value는 저장 형태, 즉 파일에 담기는 형태의 문서이며, 엘리먼트의 value가 반환하는 것과 같은 텍스트입니다.
현재 있는 엔티티만 고정된 순서로 담으며, 삭제된 엔티티, 쌓임 순서, 그리고 toWidth가 측정하는 너비처럼 불러올 때 다시 계산되는 정보는 담지 않습니다.
두 칸으로 들여쓰고 줄바꿈으로 끝나므로, 같은 문서는 항상 같은 바이트가 됩니다.
담기는 내용은 저장 형태와 런타임 값 문서를 참고하세요.

잠금이 켜진 설정은 잠근 시점의 값으로, 나머지 설정은 현재 상태 그대로 기록됩니다.
Viewport 잠금은 화면 원점과 확대/축소 레벨을 함께 다루며, 탭, Code Generator 언어, 두 이름 케이스, 괄호 타입에는 각각 별도의 잠금이 있습니다.
잠금은 문서의 ignoreSaveSettings를 대체하며, 4.0.0부터 이 필드는 읽지도 쓰지도 않습니다.
4.0.0 이전의 value는 store가 가지고 있는 문서 전체였으며, 이 필드의 scroll 비트와 zoom 비트가 적용되었습니다.

runtimeValue getter​

4.0.0부터 제공되며, 삭제된 엔티티와 불러올 때 계산되는 모든 정보를 포함해 복제본이 가지고 있는 그대로의 문서를 직렬화합니다.
여기서도 잠긴 설정은 각각 잠긴 값으로 기록됩니다.

const snapshot = replicationStore.runtimeValue;

나중에 참여하는 에디터나 다른 store를 setInitialValue로 초기화할 때 사용하세요. 공동 편집 문서를 참고하세요.
파일로는 절대 저장하지 마세요. 삭제된 엔티티가 들어 있으며, 게시된 JSON Schema도 이 값을 거부합니다.
이 삭제된 엔티티 덕분에, 나중에 전달된 삭제 되돌리기가 이 값으로 초기화한 store에서 엔티티를 온전히 되살릴 수 있습니다.
삭제된 엔티티는 파일에 담기는 형태에서 빠지므로, 다음에 파일에서 문서를 열 때까지만 남아 있습니다.

setInitialValue​

이전에 저장했던 에디터 상태를 불러옵니다.

replicationStore.setInitialValue('json...');

저장 형태, 런타임 값, 그리고 v2 문서를 포함해 이전 릴리스가 저장한 문서를 모두 받습니다.
빈 문자열이나 문자열이 아닌 값을 전달하면 오류가 발생하지 않고 비어 있는 새 문서를 불러옵니다.
값을 한 번도 전달받지 않은 store도 같은 새 문서로 시작합니다.
4.0.0부터 이 문서는 모든 잠금이 켜져 있으므로, 복제하는 에디터가 스크롤하거나 확대/축소하거나 탭을 전환하거나 이 설정들을 바꾸어도 value는 처음의 화면, 탭, Code Generator 언어, 이름 케이스, 괄호 타입을 유지합니다. 3.10.0에서는 스크롤과 확대/축소를 모두 저장하지 않았고, 그 이전에는 둘 다 저장했습니다.
저장된 문서는 그 안에 기록된 잠금을 유지합니다.
v2 문서를 포함해 4.0.0 이전에 저장된 모든 문서처럼 잠금이 기록되지 않은 문서는 모든 잠금이 켜진 상태로 불러옵니다. 저장된 값에 상관없이 화면 원점은 0, 0, 확대/축소 레벨은 1, 탭은 ERD로 잠기며, Code Generator 언어, 이름 케이스, 괄호 타입은 저장된 값으로 잠깁니다.
4.0.0부터 setInitialValue는 엘리먼트의 setInitialValue와 마찬가지로 이전 문서에 대해 가지고 있던 협업 레지스터도 함께 버립니다.
병합 충돌 표시가 남은 파일처럼 JSON이 아닌 텍스트도 오류를 발생시키지 않습니다. 파싱 오류는 콘솔에 기록될 뿐이며, 문서는 비워지고 설정은 그대로 유지됩니다.

4.0.0부터 불러오기는 가비지 컬렉션을 수행하지 않으므로, 삭제된 엔티티가 runtimeValue에 남아 있습니다.
이전에는 doc에 더 이상 나타나지 않으면서 4일 이상 변경되지 않은 엔티티를 제거했습니다.

불러오기의 재작성 작업은 setInitialValue가 반환되기 전에 완료됩니다. 각 컬럼의 옵션과 관계에서 다시 읽어 들이는 컬럼의 키 표시, toWidth로 다시 측정하는 모든 테이블과 컬럼의 너비, 각 연결선이 테이블과 만나는 지점, 각 관계의 식별 관계 여부와 부모 쪽 끝의 기호가 여기에 해당합니다.
이 중 어느 것도 value에는 들어가지 않지만, 호출 직후 읽은 replicationStore.runtimeValue에는 이미 모두 들어 있습니다.
다시 재생된 스크롤처럼 불러오기 직후에 도착한 액션은 불러온 문서를 기준으로 비교되므로, 불러오기 자체가 changed로 보고되는 일은 없습니다.

불러오기와 이러한 재작성 작업은 어느 것도 change를 발행하지 않습니다.
불러온 문서를 저장 형태로 다시 저장하려면 setInitialValue가 반환된 뒤 직접 replicationStore.value를 읽으세요.

on@change​

에디터의 상태변경 이벤트를 구독합니다.

const unsubscribe = replicationStore.on({
change: ({ value, changed }) => {
if (changed) {
// save value...
}
},
});

change는 200ms 디바운스되며, 연속된 액션은 하나의 알림으로 전달됩니다.
문서를 변경할 수 있는 종류의 액션과, 이전 에디터가 아직 보낼 수 있는 settings.changeIgnoreSaveSettings에만 발생합니다(dispatch, dispatchSync 항목 참고).
중계된 presence 액션이나 editor.mergeLWW는 이벤트를 발생시키지 않습니다.
구독을 해제하려면 반환된 함수를 호출하세요.

3.10.0부터 리스너는 @dineug/erd-editor/engine.js가 ReplicationStore와 함께 export하는 타입인 ReplicationChange를 전달받습니다.
3.10.0 이전에는 아무것도 전달받지 않았으며, 인자를 무시하는 리스너는 그대로 동작합니다.
value는 저장 형태로 직렬화된 문서로, 그 시점에 replicationStore.value가 반환하는 문자열과 같으므로 다시 직렬화할 필요가 없습니다.
changed는 이전 change 이후, 또는 불러온 이후의 액션이 value를 바이트 단위까지 그대로 두었을 때 false입니다.

잠금 때문에 value에 반영되지 않는 변경도 change를 발생시키며, 이때 changed는 false이므로 changed가 true일 때만 저장하세요.
Viewport가 잠긴 동안의 스크롤이나 확대/축소, Canvas Type이 잠긴 동안의 탭 전환, 그리고 각각의 잠금이 켜진 동안의 Code Generator 언어, 이름 케이스, 괄호 타입 변경이 여기에 해당합니다.
엘리먼트의 change 이벤트는 이런 변경을 제외하지만, 복제본은 여전히 보고합니다.
새 문서와 4.0.0 이전에 저장된 문서에서는 모든 잠금이 켜져 있습니다.
Viewport의 잠금이 꺼져 있으면 스크롤이나 확대/축소가 value를 바꾸며, Canvas Type의 잠금이 꺼져 있으면 에디터의 다른 탭을 여는 것도 value를 바꿉니다.
잠금을 켜거나 끄는 것도 value를 바꿉니다.

4.0.0부터 changed는 삭제된 엔티티도 쌓임 순서도 담지 않는 저장 형태를 비교합니다.
따라서 한 번의 change 안에서 추가되었다가 다시 삭제된 테이블, 다른 공동 편집자가 삭제한 엔티티에 적용된 편집, 쌓임 순서만 바꾸는 중계된 불러오기도 모두 changed가 false로 전달됩니다.

changed는 저장된 파일과 비교하지 않습니다. 저장된 파일은 불러오자마자 value와 달라질 수 있으며, v2 파일을 포함해 이전 릴리스가 저장한 파일이 그렇고, 바이트 순서 표시(BOM)로 시작하는 파일과 직접 편집한 파일도 그렇습니다.
이런 파일과 바이트 단위로 비교하면 잠금 때문에 반영되지 않는 스크롤까지 편집으로 간주하게 됩니다.
changed가 true인 첫 change는 이런 파일을 저장 형태로 넘겨주므로, 이를 저장하면 파일이 한 번 다시 작성됩니다.
toWidth에서 설명한 것처럼, 이 릴리스에서 저장한 파일은 어디에서 저장했든 달라지지 않습니다.
change가 아직 대기 중일 때 setInitialValue가 실행되고 그 change가 발생할 시점까지 아무것도 dispatch되지 않으면, 그 change는 그대로 발생하며 불러온 value와 false인 changed를 전달합니다.

dispatch, dispatchSync​

원격 에디터 변경사항을 replicationStore에 반영합니다.

replicationStore.dispatch(actions); // async
replicationStore.dispatchSync(actions); // sync

dispatch는 마이크로태스크로 지연 실행하고, dispatchSync는 배치를 즉시 반영합니다.

shared store의 스트림을 필터링 없이 그대로 전달해도 됩니다.
두 메서드 모두 문서를 변경할 수 있는 종류의 액션과 이 항목 끝에서 설명하는 두 액션만 남기고 나머지는 버리므로, 공동 편집 세션이 전달하는 presence 정보(각 사용자의 마우스 커서, 포커스, 선택 영역, 드래그 박스)는 아무 동작 없이 통과합니다.

3.10.0부터 남기는 액션에 relationship.changeOnDelete와 relationship.changeOnUpdate가 포함되며, relationship.add는 onDelete와 onUpdate를 함께 전달합니다.
이전 릴리스의 store는 이 액션을 버리고 이 필드를 무시하므로, 관계의 ON DELETE와 ON UPDATE 동작을 잃습니다.

4.0.0부터 남기는 액션에 table.changeGroup, 8개의 tableGroup 액션 중 쌓임 순서를 바꾸는 tableGroup.changeZIndex를 제외한 7개(tableGroup.add, tableGroup.move, tableGroup.moveTo, tableGroup.remove, tableGroup.resize, tableGroup.changeName, tableGroup.changeColor), relationship.changeColumns, settings.changeLockSettings, settings.changeDDLScript도 포함됩니다.
settings.changeRelationshipOptimization은 더 이상 존재하지 않으므로 두 메서드 모두 이 액션을 버립니다.
이전 릴리스의 store는 새 액션을 버리므로 테이블 그룹, 잠금, Schema SQL 스크립트, 그리고 컬럼 매핑이 이미 있는 관계에 지정하는 컬럼을 잃습니다.
복제본은 에디터와 같은 릴리스나 그보다 새로운 릴리스에서 실행하세요.

4.0.0부터 두 메서드는 editor.mergeLWW도 반영합니다. 이 액션은 다른 에디터가 세션에 참여할 때 에디터가 응답으로 보내는 협업 레지스터를 담고 있으므로, 복제본도 에디터와 같은 방식으로 동시 편집의 결과를 결정합니다.
value의 바이트를 하나도 바꾸지 않으며 change도 발생시키지 않습니다.
4.0.0 이전에 릴리스된 에디터가 아직 보낼 수 있는 settings.changeIgnoreSaveSettings는 남기지만 아무것도 바꾸지 않습니다. 이 액션은 changed가 false인 change를 발생시킵니다.

destroy​

replicationStore 인스턴스를 완전히 파괴합니다.

replicationStore.destroy();

이후에는 change를 발행하지 않고 이어지는 dispatch도 무시합니다.
오류가 발생하는 대신 문서가 마지막 값으로 고정됩니다.
다시 시작하려면 새 store를 생성하세요.