Passa al contenuto principale

Archiviazione remota

Inviare l’intero stato dell’editor a ogni modifica è inefficiente.
Per questo caso, l’editor fornisce un’API per la replica dei dati in tempo reale.

Installazione​

npm install @dineug/erd-editor

Lo store di replica si trova nel sottopercorso @dineug/erd-editor/engine.js, non nella radice del pacchetto.
Questo punto di ingresso non accede al DOM, quindi funziona in un Web Worker o in qualsiasi altro contesto fuori dal thread principale.
Importando invece la radice del pacchetto si registra l’elemento personalizzato <erd-editor>, che richiede un documento.

Utilizzo​

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

Replicare un editor in esecuzione​

Le azioni provengono dallo store condiviso di un editor in esecuzione.
Vedere Modifica collaborativa.

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

Solo la modifica attraversa il confine, mai l’intero documento.
La replica la applica e serializza il risultato dal proprio lato.
change consegna quel risultato come value, e changed indica quando vale la pena scriverlo.

Dalla versione 4.0.0, inizializzare la replica dal runtimeValue dell’editor, come sopra, anziché dal file archiviato o dal value dell’editor.
Al file mancano le modifiche apportate dopo il suo salvataggio, e a value mancano le entità rimosse nella sessione.
Una replica inizializzata senza una tabella rimossa la ripristina senza nome né colonne quando un editor ne annulla la rimozione, e la salva così.

InjectEngineContext​

toWidth​

Misura una stringa in pixel.
Lo store non ha un DOM, quindi non può misurare il testo da sé, e ricalcola la larghezza delle colonne con questa funzione ogni volta che cambia un nome, un tipo di dati, un valore predefinito o un commento.
Misura inoltre di nuovo ogni tabella e colonna ogni volta che setInitialValue carica un documento.
Dalla versione 4.0.0 le larghezze sono conservate solo in runtimeValue, mai in value.
Così un documento salvato da questa versione si rilegge con lo stesso value byte per byte, ovunque sia stato salvato e qualunque misura avesse lì il testo.

Misurare come fa l’editor — 400 12px nell’elenco di font dell’editor, arrotondando e aggiungendo 2 px di spaziatura interna.
Altrimenti le larghezze in runtimeValue si discostano da quelle sullo schermo.

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 è la soluzione di ripiego quando non è disponibile alcun canvas.
È sufficiente per uno store il cui documento viene solo archiviato, mai visualizzato.

ReplicationStore​

value getter​

Serializza lo stato corrente dell’editor.
Restituisce una stringa JSON, pronta da archiviare così com’è — non un oggetto analizzato.

const data = replicationStore.value;

Dalla versione 4.0.0, value è la forma di archiviazione: il documento come lo contiene un file, lo stesso testo restituito dal value dell’elemento.
Contiene solo le entità attive, in un ordine fisso, senza quelle rimosse, senza l’ordine di sovrapposizione e senza nulla di ciò che un caricamento ricalcola, come le larghezze misurate da toWidth.
È indentato con due spazi e termina con un carattere di nuova riga, quindi lo stesso documento produce sempre gli stessi byte.
Per ciò che contiene, vedere Forma di archiviazione e valore di runtime.

Ogni impostazione il cui blocco è attivo viene scritta con il valore a cui è stata bloccata, e ogni altra impostazione così com’è.
Il blocco Viewport copre insieme l’origine della vista e il livello di zoom, e la scheda, il linguaggio del Generatore di codice, entrambi gli stili dei nomi e il tipo di delimitatore hanno ciascuno un proprio blocco.
I blocchi sostituiscono il campo ignoreSaveSettings del documento, che dalla versione 4.0.0 non viene né letto né scritto.
Prima della versione 4.0.0, value era l’intero documento come lo conteneva lo store, con i bit di scorrimento e di zoom di quel campo applicati.

runtimeValue getter​

Dalla versione 4.0.0, serializza il documento come lo contiene la replica, comprese le entità rimosse e tutto ciò che un caricamento ricalcola.
Anche qui ogni impostazione bloccata viene scritta con il suo valore bloccato.

const snapshot = replicationStore.runtimeValue;

Utilizzarlo per inizializzare, tramite il suo setInitialValue, un editor o un altro store che si unisce in seguito; vedere Modifica collaborativa.
Non archiviarlo mai come file: contiene le entità rimosse, e il JSON Schema pubblicato lo rifiuta.
Sono proprio queste entità rimosse a far sì che l’annullamento di una rimozione, inviato in seguito, ripristini l’entità per intero nello store inizializzato a partire da esso.
Restano fino alla successiva apertura del documento dal suo file, la cui forma le esclude.

setInitialValue​

Carica lo stato dell’editor archiviato in precedenza.

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

Accetta la forma di archiviazione, un valore di runtime o un documento salvato da una versione precedente, compreso un documento v2.
Una stringa vuota, o qualsiasi valore che non sia una stringa, carica un documento nuovo e vuoto anziché generare un errore.
Uno store a cui non è mai stato passato un valore parte come lo stesso documento nuovo.
Dalla versione 4.0.0 quel documento ha attivo ogni blocco, quindi il suo value mantiene la vista, la scheda, il linguaggio del Generatore di codice, gli stili dei nomi e il tipo di delimitatore con cui è partito mentre gli editor che replica scorrono, eseguono lo zoom, cambiano scheda o modificano quelle impostazioni. Nella versione 3.10.0 non salvava né lo scorrimento né lo zoom, e prima ancora li salvava entrambi.
Un documento archiviato mantiene i blocchi che indica.
Uno che non ne indica nessuno, cioè qualsiasi documento salvato prima della versione 4.0.0, compreso un documento v2, si carica con ogni blocco attivo: l’origine della vista a 0, 0, il livello di zoom a 1 e la scheda su ERD, qualunque cosa abbia salvato, e il linguaggio del Generatore di codice, gli stili dei nomi e il tipo di delimitatore come li ha salvati.
Dalla versione 4.0.0, setInitialValue scarta anche i registri di collaborazione conservati per il documento precedente, come fa il setInitialValue dell’elemento.
Nemmeno un testo che non è JSON, come un file rimasto con i marcatori di conflitto di merge, genera un errore: l’errore di analisi viene solo registrato nella console, il documento viene svuotato e le impostazioni restano com’erano.

Dalla versione 4.0.0 il caricamento non esegue alcuna garbage collection, quindi le entità rimosse restano in runtimeValue.
Prima scartava le entità che non comparivano più in doc e non erano state modificate da quattro giorni o più.

Le riscritture del caricamento terminano prima che setInitialValue restituisca il controllo: gli indicatori di chiave sulle colonne, ricavati dalle opzioni di ciascuna colonna e dalle relazioni; ogni larghezza di tabella e di colonna, misurata di nuovo con toWidth; i punti in cui ciascun connettore incontra le sue tabelle; e il flag identificante di ciascuna relazione e il simbolo alla sua estremità lato padre.
Nessuna di esse fa parte di value, ma replicationStore.runtimeValue letto subito dopo la chiamata le contiene già tutte.
Un’azione che arriva subito dopo il caricamento, come uno scorrimento riprodotto, viene valutata rispetto al documento caricato, quindi il caricamento stesso non viene mai segnalato come changed.

Né il caricamento né alcuna di queste riscritture emette change.
Leggere direttamente replicationStore.value dopo il ritorno di setInitialValue se si vuole riscrivere il documento caricato nella forma di archiviazione.

on@change​

Effettua la sottoscrizione alle modifiche dello stato dell’editor.

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

change viene emesso con un debounce di 200 ms, quindi una raffica di azioni arriva come un’unica notifica.
Viene emesso solo per azioni di un tipo che può modificare il documento, oltre all’azione settings.changeIgnoreSaveSettings che un editor meno recente può ancora inviare (vedere dispatch, dispatchSync).
Un’azione di presenza inoltrata o editor.mergeLWW non lo attiva mai.
Chiamare la funzione restituita per annullare la sottoscrizione.

Dalla versione 3.10.0 il listener riceve un ReplicationChange, un tipo che @dineug/erd-editor/engine.js esporta accanto a ReplicationStore.
Prima della versione 3.10.0 non riceveva nulla, e un listener che ignora l’argomento continua a funzionare.
value è il documento serializzato nella forma di archiviazione, la stessa stringa che replicationStore.value restituisce in quel momento, quindi non serve serializzarlo di nuovo.
changed è false quando le azioni successive al change precedente, o al caricamento, hanno lasciato value com’era, byte per byte.

Una modifica che un blocco esclude da value emette comunque change, con changed impostato su false, quindi scrivere solo quando changed è true.
Si tratta di uno scorrimento o di uno zoom mentre Viewport è bloccato, di un cambio di scheda mentre Canvas Type è bloccato, e di una modifica del linguaggio del Generatore di codice, di uno stile dei nomi o del tipo di delimitatore mentre il relativo blocco è attivo.
L’evento change dell’elemento esclude una modifica di questo tipo, ma la replica la segnala comunque.
Ogni blocco è attivo in un documento nuovo e in uno salvato prima della versione 4.0.0.
Con Viewport sbloccato uno scorrimento o uno zoom modifica value, e con Canvas Type sbloccato lo fa anche l’apertura di un’altra scheda dell’editor.
Anche attivare o disattivare un blocco modifica value.

Dalla versione 4.0.0, changed confronta la forma di archiviazione, che non contiene né le entità rimosse né l’ordine di sovrapposizione.
Così anche una tabella aggiunta e rimossa di nuovo all’interno di un solo change, una modifica che ricade su un’entità rimossa da un altro collaboratore e un caricamento inoltrato che cambia solo l’ordine di sovrapposizione risultano tutti con changed impostato su false.

changed non confronta mai con il file archiviato, che può già differire da value una volta caricato: è il caso di un file salvato da una versione precedente, compreso un file v2, così come di un file che inizia con un byte order mark e di uno modificato a mano.
Un confronto byte per byte con un file del genere conterebbe come modifica perfino uno scorrimento che un blocco esclude.
Il primo change con changed impostato su true restituisce un file del genere nella forma di archiviazione, quindi scriverlo riscrive il file una sola volta.
Un file salvato da questa versione non differisce, ovunque sia stato salvato, come spiega toWidth.
Se setInitialValue arriva mentre un change è ancora in sospeso, e non viene inviato nulla prima della sua scadenza, quel change arriva comunque, con il value caricato e changed impostato su false.

dispatch, dispatchSync​

Applica al replicationStore le modifiche degli editor remoti.

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

dispatch rimanda l’applicazione a un microtask, mentre dispatchSync applica subito il batch.

È possibile passare il flusso di uno store condiviso senza filtrarlo.
Entrambi i metodi mantengono solo le azioni di un tipo che può modificare il documento, oltre alle due descritte alla fine di questa sezione, e scartano le altre, quindi la presenza trasportata da una sessione collaborativa — il cursore del mouse, il focus, la selezione e il riquadro di trascinamento di ciascun utente — passa senza alcun effetto.

Dalla versione 3.10.0 le azioni mantenute includono relationship.changeOnDelete e relationship.changeOnUpdate, e relationship.add contiene onDelete e onUpdate.
Uno store di una versione precedente scarta quelle azioni e ignora quei campi, quindi perde le azioni ON DELETE e ON UPDATE di una relazione.

Dalla versione 4.0.0 le azioni mantenute includono anche table.changeGroup, sette delle otto azioni tableGroup (tableGroup.add, tableGroup.move, tableGroup.moveTo, tableGroup.remove, tableGroup.resize, tableGroup.changeName e tableGroup.changeColor, tutte tranne la modifica dell’ordine di sovrapposizione tableGroup.changeZIndex), relationship.changeColumns, settings.changeLockSettings e settings.changeDDLScript.
L’azione settings.changeRelationshipOptimization non esiste più, quindi entrambi i metodi la scartano.
Uno store di una versione precedente scarta le nuove azioni, quindi perde i gruppi di tabelle, i blocchi, gli script di Schema SQL e le colonne che Associa colonne assegna a una relazione esistente.
Eseguire la replica sulla stessa versione degli editor o su una più recente.

La versione 4.1.0 non aggiunge alcun tipo di azione e lascia @dineug/erd-editor/engine.js invariato, quindi una replica 4.0.0 mantiene ogni modifica inviata da un editor 4.1.0, esattamente come fa una replica 4.1.0.
Dalla versione 4.1.0, i comandi Replace e Replace All di Trova e sostituisci rinominano anche indici e gruppi di tabelle, tramite le azioni index.changeName e tableGroup.changeName che una replica 4.0.0 già mantiene.
Lo stato proprio del pannello Esplora non viene mai inviato, e un cambio di scheda o uno scorrimento che esso esegue arriva come qualsiasi altro cambio di scheda o scorrimento; vedere Modifica collaborativa.

Dalla versione 4.0.0 entrambi i metodi applicano anche editor.mergeLWW, che trasporta i registri di collaborazione con cui un editor risponde quando un altro si unisce alla sessione, così la replica risolve le modifiche simultanee come fanno gli editor.
Non modifica alcun byte di value e non emette alcun change.
L’azione settings.changeIgnoreSaveSettings, che un editor precedente alla versione 4.0.0 può ancora inviare, viene mantenuta ma non cambia nulla: emette change con changed impostato su false.

destroy​

Distrugge completamente l’istanza di replicationStore.

replicationStore.destroy();

Da quel momento lo store smette di emettere change e ignora gli ulteriori invii di azioni.
Il documento resta fermo al suo ultimo valore anziché generare un errore.
Per ricominciare, creare un nuovo store.