Pular para o conteúdo principal

Armazenamento remoto

Enviar todo o estado do editor a cada alteração é ineficiente.
Para esse caso, o editor oferece uma API de replicação de dados em tempo real.

Instalação​

npm install @dineug/erd-editor

O armazenamento de replicação fica no subcaminho @dineug/erd-editor/engine.js, não na raiz do pacote.
Esse ponto de entrada não toca no DOM, então é executado em um Web Worker ou em qualquer outro lugar fora da thread principal.
Importar a raiz do pacote, em vez disso, precisa de um documento: a partir da 5.0.0 ela não define nenhum elemento, mas avaliá-la constrói as duas classes de elemento. Antes da 5.0.0, ela registrava o elemento personalizado <erd-editor>.

Uso​

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,
});

Replicar um editor ao vivo​

As ações vêm do armazenamento compartilhado de um editor ao vivo.
Consulte Edição colaborativa.

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);
});

Só a alteração atravessa a fronteira, nunca o documento inteiro.
A réplica a aplica e serializa o resultado do seu próprio lado.
O evento change entrega esse resultado como value, e changed indica quando vale a pena gravá-lo.

A partir da 4.0.0, inicialize a réplica com o runtimeValue do editor, como acima, e não com o arquivo armazenado nem com o value do editor.
O arquivo não tem as edições feitas desde que foi salvo, e o value não tem as entidades removidas na sessão.
Uma réplica inicializada sem uma tabela removida a traz de volta sem o nome nem as colunas quando um editor desfaz a remoção, e a salva assim.
A partir da 4.2.0, o mesmo vale para uma palavra, um termo ou um domínio removido de uma planilha da aba Dicionário.
Quando um editor desfaz a remoção, uma réplica inicializada sem a linha volta a listá-la vazia, e, como value deixa de fora uma linha vazia, a réplica salva o documento sem ela.
No caso de um domínio, as colunas e os termos vinculados a ele também continuam desvinculados nessa réplica, já que ela nunca teve os vínculos e o desfazer não os envia.

A partir da 5.1.0, o @dineug/erd-editor-yjs também pode manter editores em sincronia por meio de uma sala Yjs.
Alimentada como acima a partir de um elemento vinculado a uma sala, uma réplica recebe apenas as edições feitas naquele elemento, nunca as que a sala traz: essas chegam ao elemento como de outro editor, e um armazenamento compartilhado nunca repassa o que recebeu.
Para armazenar o documento de uma sala, salve o value do elemento vinculado, ou o de um armazenamento de pares vinculado, como descreve O que a sala contém.
Um provider de persistência do Yjs armazena a própria sala, que contém um valor em tempo de execução e as edições feitas desde então, e não um arquivo; consulte Limites.

InjectEngineContext​

toWidth​

Mede uma string em pixels.
O armazenamento não tem DOM, então não consegue medir texto por conta própria, e recalcula as larguras das colunas com esta função sempre que um nome, tipo de dados, valor padrão ou comentário muda.
A partir da 4.2.0, isso inclui o nome lógico de uma tabela ou coluna.
Ele mede a célula Domínio de uma coluna, que mostra o nome do domínio ao qual a coluna está vinculada, sempre que a coluna é vinculada a um domínio ou desvinculada dele e sempre que esse domínio é adicionado, renomeado ou removido.
Ele também mede de novo todas as tabelas e colunas sempre que setInitialValue carrega um documento.
A partir da 4.0.0, as larguras ficam apenas no runtimeValue, nunca no value.
Assim, um documento salvo por esta versão é lido de volta com o mesmo value, byte a byte, onde quer que tenha sido salvo e independentemente de como o texto era medido ali.

Meça como o editor mede — 400 12px na pilha de fontes do editor, arredondado, mais 2 px de espaçamento interno.
Caso contrário, as larguras no runtimeValue se afastam das que aparecem na tela.

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 {
// no canvas available
}

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 é a alternativa usada quando nenhum canvas está disponível.
Ela basta para um armazenamento cujo documento é apenas salvo, nunca renderizado.

ReplicationStore​

value getter​

Serializa o estado atual do editor.
Retorna uma string JSON, pronta para ser armazenada como está — não um objeto analisado.

const data = replicationStore.value;

A partir da 4.0.0, value é a forma de armazenamento: o documento tal como um arquivo o contém, o mesmo texto que o value do elemento retorna.
Ele contém apenas as entidades ativas, em uma ordem fixa, sem as removidas, a ordem de empilhamento ou qualquer coisa que um carregamento calcule de novo, como as larguras que toWidth mede.
A partir da 4.2.0, ele também contém os nomes lógicos, se cada nome foi derivado dos dicionários, os vínculos com domínios e as palavras, os termos e os domínios da aba Dicionário.
Ele também deixa de fora, a partir da 4.2.0, cada campo de texto vazio de uma entidade, cada lista de doc e cada coleção que não contém nada e o próprio collections quando todas as coleções estão vazias.
Ele é indentado com dois espaços, com cada linha de dicionário em uma linha própria do texto, e termina com uma quebra de linha, então o mesmo documento sempre gera os mesmos bytes.
Consulte Forma de armazenamento para saber o que ele contém.

Cada configuração cujo bloqueio está ativado é gravada com o valor em que foi bloqueada, e todas as outras, como estão.
O bloqueio Viewport abrange a origem da área visível e o nível de zoom juntos, e a aba, a linguagem do Gerador de código, os dois estilos de nome e o tipo de delimitador têm, cada um, seu próprio bloqueio.
A partir da 4.2.0, o mesmo vale para a exibição de nomes, cujo bloqueio Name View está ativado em um documento novo.
Os bloqueios substituem o campo ignoreSaveSettings do documento, que não é lido nem gravado a partir da 4.0.0.
Antes da 4.0.0, value era o documento inteiro tal como o armazenamento o mantinha, com os bits de rolagem e de zoom desse campo aplicados.

runtimeValue getter​

A partir da 4.0.0, serializa o documento tal como a réplica o mantém, incluindo as entidades removidas e tudo o que um carregamento calcula de novo.
Aqui também, cada configuração bloqueada é gravada com seu valor bloqueado.

const snapshot = replicationStore.runtimeValue;

Use-o para inicializar um editor ou outro armazenamento que entre depois, por meio do setInitialValue dele; consulte Edição colaborativa.
Uma réplica não tem getLWW nem getVersion, então não repassa registros de colaboração com runtimeValue: um editor inicializado a partir dela os obtém dos editores em execução, como descreve Inicializar um colaborador.
Nunca o armazene como o arquivo: ele traz as entidades removidas, e o JSON Schema publicado o recusa.
São essas entidades removidas que permitem que o desfazer de uma remoção, enviado depois, traga a entidade de volta inteira no armazenamento inicializado a partir dele.
Elas permanecem até a próxima vez que o documento for aberto a partir do seu arquivo, cuja forma as deixa de fora.

setInitialValue​

Carrega o estado do editor armazenado anteriormente.

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

Aceita a forma de armazenamento, um valor em tempo de execução ou um documento salvo por uma versão anterior, incluindo um documento v2.
Uma string em branco, ou qualquer coisa que não seja uma string, carrega um documento novo e vazio em vez de gerar um erro.
Um armazenamento que nunca recebeu um valor começa como esse mesmo documento novo.
A partir da 4.0.0, esse documento tem todos os bloqueios ativados, então seu value mantém a área visível, a aba, a linguagem do Gerador de código, os estilos de nome e o tipo de delimitador com que começou enquanto os editores que ele replica rolam, aplicam zoom, trocam de aba ou alteram essas configurações. Na 3.10.0, ele não salvava nem a rolagem nem o zoom, e antes disso salvava os dois.
A partir da 4.2.0, ele também mantém sua exibição de nomes, Physical Names, enquanto os editores trocam as deles.
Um documento armazenado mantém os bloqueios que indica.
Um que não indica nenhum, o que vale para todo documento salvo antes da 4.0.0, incluindo um documento v2, é carregado com todos os bloqueios ativados: a origem da área visível em 0, 0, o nível de zoom em 1 e a aba em ERD, independentemente do que tenha salvo, e a linguagem do Gerador de código, os estilos de nome e o tipo de delimitador como os salvou.
Um documento salvo antes da 4.2.0 não indica nenhum bloqueio Name View, então é carregado com esse bloqueio ativado e na exibição Physical Names, mesmo um salvo com todos os outros bloqueios desativados.
A partir da 4.0.0, setInitialValue também descarta os registros de colaboração mantidos para o documento anterior, como faz o setInitialValue do elemento.
Um texto que não é JSON, como um arquivo que ficou com marcadores de conflito de merge, também não gera erro: o erro de análise é apenas registrado no console, o documento é esvaziado e as configurações ficam como estavam.

A partir da 4.0.0, o carregamento não executa coleta de lixo, então as entidades removidas permanecem no runtimeValue.
Antes, ele descartava as entidades que já não apareciam em doc e não tinham sido alteradas havia quatro dias ou mais.

As reescritas do carregamento terminam antes que setInitialValue retorne: as marcas de chave nas colunas, lidas das opções de cada coluna e dos relacionamentos; a largura de cada tabela e coluna, medida de novo com toWidth; os pontos em que cada conector encontra suas tabelas; e o sinalizador identificador de cada relacionamento e o símbolo na sua extremidade do lado pai.
Nenhuma delas faz parte do value, mas replicationStore.runtimeValue, lido logo após a chamada, já contém todas elas.
Uma ação que chega logo depois do carregamento, como uma rolagem reproduzida, é comparada com o documento carregado, então o próprio carregamento nunca é informado como changed.

Nem o carregamento nem qualquer dessas reescritas emite change.
Leia você mesmo replicationStore.value assim que setInitialValue retornar se quiser que o documento carregado seja gravado de volta na forma de armazenamento.

on@change​

Assina as alterações no estado do editor.

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

O change tem debounce de 200 ms, então uma rajada de ações chega como uma única notificação.
Ele dispara apenas para ações de um tipo que pode alterar o documento, além da ação settings.changeIgnoreSaveSettings que um editor mais antigo ainda pode enviar (consulte dispatch, dispatchSync).
Uma ação de presença retransmitida ou editor.mergeLWW nunca o aciona.
Chame a função retornada para cancelar a assinatura.

A partir da 3.10.0, o listener recebe um ReplicationChange, um tipo que @dineug/erd-editor/engine.js exporta ao lado de ReplicationStore.
Antes da 3.10.0, ele não recebia nada, e um listener que ignora o argumento continua funcionando.
value é o documento serializado na forma de armazenamento, a mesma string que replicationStore.value retorna naquele momento, então não é preciso serializá-lo de novo.
changed é false quando as ações desde o change anterior, ou desde o carregamento, deixaram value byte a byte como estava.

Uma alteração que um bloqueio deixa fora do value ainda dispara change, com changed definido como false, então grave apenas quando changed for true.
É o caso de uma rolagem ou um zoom enquanto Viewport está bloqueado, de uma troca de aba enquanto Canvas Type está bloqueado e de uma alteração da linguagem do Gerador de código, de um estilo de nome ou do tipo de delimitador enquanto seu próprio bloqueio está ativado, o que, a partir da 5.1.0, só um editor anterior à 5.1.0 ainda envia (consulte dispatch, dispatchSync).
A partir da 4.2.0, o mesmo vale para uma troca da exibição de nomes enquanto Name View está bloqueado: o editor ainda a envia, mas a réplica a descarta, como fazem os outros editores.
Uma troca da exibição de nomes feita em um editor somente leitura é descartada da mesma forma, qualquer que seja seu bloqueio.
O evento change do elemento deixa essa alteração de fora, mas a réplica ainda a informa.
Todos os bloqueios estão ativados em um documento novo e em um salvo antes da 4.0.0, e Name View está bloqueado em qualquer documento salvo antes da 4.2.0.
Com Viewport desbloqueado, uma rolagem ou um zoom altera value, com Canvas Type desbloqueado, abrir outra das abas do editor também o altera, e, com Name View desbloqueado, também o altera uma troca da exibição de nomes feita por um editor que não é somente leitura.
Ativar ou desativar um bloqueio também altera value.

A partir da 4.0.0, changed compara a forma de armazenamento, que não contém nem as entidades removidas nem a ordem de empilhamento.
Assim, uma tabela adicionada e removida de novo dentro de um mesmo change, uma edição que recai sobre uma entidade que outro colaborador removeu e um carregamento retransmitido que só altera a ordem de empilhamento também resultam, todos, em changed definido como false.
A partir da 4.2.0, o mesmo acontece com uma linha adicionada a uma planilha da aba Dicionário e deixada vazia, porque a forma de armazenamento deixa de fora uma linha que não contém nada.

changed nunca compara com o seu arquivo armazenado, que já pode diferir de value assim que é carregado: é o caso de um arquivo salvo por uma versão anterior, incluindo um arquivo v2, e também de um arquivo que começa com uma marca de ordem de bytes e de um editado à mão.
A partir da 4.2.0, isso inclui um arquivo salvo pela 4.0.0 ou pela 4.1.0, porque value deixa de fora os campos de texto vazios das entidades e as listas de doc e as coleções vazias que um arquivo assim contém.
Comparar bytes com um arquivo assim contaria como edição até mesmo uma rolagem que um bloqueio deixa de fora.
O primeiro change com changed definido como true devolve esse arquivo na forma de armazenamento, então gravá-lo reescreve o arquivo uma única vez.
Um arquivo salvo por esta versão não difere, onde quer que tenha sido salvo, como toWidth explica.
Se setInitialValue chegar enquanto um change ainda estiver pendente, e nada for despachado antes do momento previsto para ele, esse change ainda chega, com o value carregado e changed definido como false.

dispatch, dispatchSync​

Aplica ao replicationStore as alterações de um editor remoto.

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

dispatch adia a aplicação para uma microtarefa, e dispatchSync aplica o lote imediatamente.

É possível repassar o fluxo de um armazenamento compartilhado sem filtrá-lo.
Os dois métodos mantêm apenas as ações de um tipo que pode alterar o documento, além das duas descritas no fim desta seção, e descartam o resto, então a presença que uma sessão colaborativa transporta — o cursor do mouse, o foco, a seleção e o retângulo de arrasto de cada usuário — passa sem efeito algum.

A partir da 3.10.0, as ações mantidas incluem relationship.changeOnDelete e relationship.changeOnUpdate, e relationship.add traz onDelete e onUpdate.
Um armazenamento de uma versão anterior descarta essas ações e ignora esses campos, então perde as ações ON DELETE e ON UPDATE de um relacionamento.

A partir da 4.0.0, as ações mantidas também incluem table.changeGroup, sete das oito ações tableGroup (tableGroup.add, tableGroup.move, tableGroup.moveTo, tableGroup.remove, tableGroup.resize, tableGroup.changeName e tableGroup.changeColor, todas exceto a alteração de empilhamento tableGroup.changeZIndex), relationship.changeColumns, settings.changeLockSettings e settings.changeDDLScript.
A ação settings.changeRelationshipOptimization não existe mais, então os dois métodos a descartam.
Um armazenamento de uma versão anterior descarta as novas ações, então perde os grupos de tabelas, os bloqueios, os scripts do Esquema SQL e as colunas que Mapear colunas atribui a um relacionamento existente.
Execute a réplica na mesma versão dos editores ou em uma mais recente.

A 4.1.0 não adiciona nenhum tipo de ação e deixa @dineug/erd-editor/engine.js como estava, então uma réplica 4.0.0 mantém todas as alterações que um editor 4.1.0 envia, assim como uma réplica 4.1.0.
A partir da 4.1.0, Replace e Replace All em Localizar e substituir também renomeiam índices e grupos de tabelas, por meio das ações index.changeName e tableGroup.changeName que uma réplica 4.0.0 já mantém.
O estado próprio do explorador nunca é enviado, e uma troca de aba ou uma rolagem que ele faz chega como qualquer outra troca de aba ou rolagem; consulte Edição colaborativa.

A partir da 4.2.0, as ações mantidas também incluem table.changeLogicalName, table.changeNameDerived, column.changeLogicalName, column.changeDomain, column.changeNameDerived, settings.changeNameViewMode e dictionary.add, dictionary.update e dictionary.remove, que levam cada edição, classificação e importação de CSV feita nas planilhas da aba Dicionário.
Das trocas da exibição de nomes, uma réplica aplica apenas as que um editor que não é somente leitura faz enquanto Name View está desbloqueado, e descarta as demais (consulte on@change).
Um armazenamento de uma versão anterior descarta todas essas ações, então perde os nomes lógicos, os vínculos com domínios, a informação de se cada nome veio dos dicionários, a exibição de nomes e as palavras, os termos e os domínios.
Um editor anterior à 4.2.0 na sessão ainda pode tirar parte disso de uma réplica 4.2.0.
Um Import de Esquema SQL, GraphQL, DBML ou AML nesse editor, mas não um Import and Add, esvazia as palavras, os termos e os domínios da réplica.
Desfazer nele um carregamento, um Import ou um clear() carrega a própria cópia do documento dele na réplica, sem nomes lógicos, vínculos com domínios nem dicionários; consulte Misturar versões.
@dineug/erd-editor/engine.js exporta os mesmos createReplicationStore, ReplicationStore e ReplicationChange de antes, e createReplicationStore recebe o mesmo contexto.

A 5.1.0 não adiciona nenhum tipo de ação nem altera nenhuma exportação de @dineug/erd-editor/engine.js, então uma réplica da 4.2.0 em diante ainda mantém toda alteração que um editor 5.1.0 envia.
A partir da 5.1.0, a réplica resolve as edições feitas no mesmo instante como fazem os editores 5.1.0; consulte Edições simultâneas.
Ela mantém uma versão para a posição de uma tabela, para a posição e o tamanho de uma nota e de um grupo de tabelas e para a origem e o nível de zoom da área visível.
Um editor 5.1.0 envia um arrasto, um deslocamento ou um zoom como o ponto onde ele termina, por meio de table.moveTo, memo.moveTo, tableGroup.moveTo, settings.scrollTo e settings.changeZoomLevel, e a réplica aceita cada uma dessas ações, e cada memo.resize e tableGroup.resize, somente quando sua versão não é inferior à que ela mantém.
Assim, quando dois desses editores movem ou redimensionam a mesma tabela, nota ou grupo de tabelas no mesmo instante, a réplica a salva no ponto e no tamanho que todos eles mostram, em qualquer ordem em que os ouça, e um arrasto, um deslocamento ou um zoom que o transporte entrega duas vezes não move nada duas vezes.
Com Viewport desbloqueado, a réplica salva a origem e o nível de zoom cada um a partir da escrita com a maior versão, onde estava o editor que a fez.
Um zoom também move a origem, então um deslocamento e um zoom que dois editores fazem no mesmo instante podem ser salvos como a origem de um editor com o nível de zoom do outro.
Antes da 5.1.0, a réplica somava os passos de deslocamento e de zoom de todos os editores em uma única área visível.

As distâncias que um editor anterior à 5.1.0 envia para um arrasto, um deslocamento ou um zoom (table.move, memo.move, tableGroup.move, settings.streamScrollTo e settings.streamZoomLevel) continuam sendo somadas ao que a réplica mantém, fora das versões.
Uma mudança de cor continua chegando sem versão: a réplica, como cada editor, a marca com uma versão própria e mantém a cor que ouviu por último, então duas mudanças de cor de uma mesma tabela, nota ou grupo de tabelas feitas no mesmo instante podem ser salvas de forma diferente do que um editor mostra.
Uma réplica anterior à 5.1.0 aplica os pontos e os tamanhos na ordem em que chegam, então pode salvar uma tabela em um lugar diferente de onde os editores 5.1.0 a mostram.
Mantenha todos os editores da sessão, e a réplica, na 5.1.0 ou posterior para que as edições feitas no mesmo instante terminem iguais, salvo as exceções que Edições simultâneas lista.

A partir da 5.1.0, um editor envia uma alteração da linguagem do Gerador de código, de um estilo de nome ou do tipo de delimitador somente enquanto o bloqueio dessa configuração está desativado, marcada com shared: true em seu payload, e a réplica aplica toda alteração marcada, como fazem os editores; consulte Edição colaborativa.
A réplica aplica uma alteração não marcada, que um editor anterior à 5.1.0 envia independentemente do bloqueio, somente enquanto essa configuração está desbloqueada do seu lado, e a descarta enquanto a configuração está bloqueada (consulte on@change).

A partir da 4.0.0, os dois métodos também aplicam a ação editor.mergeLWW, que traz os registros de colaboração com que um editor responde quando outro entra na sessão, então a réplica resolve as edições simultâneas como os editores fazem.
Ela não altera nenhum byte de value e não dispara nenhum change.
A ação settings.changeIgnoreSaveSettings, que um editor lançado antes da 4.0.0 ainda pode enviar, é mantida, mas não altera nada: ela dispara change com changed definido como false.

destroy​

Destrói completamente a instância do replicationStore.

replicationStore.destroy();

Depois disso, o armazenamento deixa de emitir change e ignora os despachos seguintes.
O documento fica congelado no seu último valor em vez de gerar um erro.
Crie um novo armazenamento para recomeçar.