;
/**
* Execute the given command.
*
@@ -318,28 +339,101 @@ export class JWEditor {
*/
async execCommand = Commands
>(
commandName: C,
- params?: CommandParams
,
+ params?: CommandParamsType
,
+ ): Promise;
+ /**
+ * Execute the command or arbitrary code in `callback` in memory.
+ *
+ * TODO: create memory for each plugin who use the command then use
+ * squashInto(winnerSliceKey, winnerSliceKey, newMasterSliceKey)
+ *
+ * @param commandName name identifier of the command to execute or callback
+ * @param params arguments object of the command to execute
+ */
+ async execCommand = Commands
>(
+ commandName: C | (() => Promise | void),
+ params?: CommandParamsType,
): Promise {
- return await this.dispatcher.dispatch(commandName, params);
+ const isFrozen = this.memory.isFrozen();
+
+ let memorySlice: string;
+ if (isFrozen) {
+ // Switch to the next memory slice (unfreeze the memory).
+ memorySlice = this._memoryID.toString();
+ this.memory.switchTo(memorySlice);
+ this.memoryInfo.commandNames = new VersionableArray();
+ }
+
+ // Execute command.
+ if (typeof commandName === 'function') {
+ this.memoryInfo.commandNames.push('@custom');
+ await commandName();
+ } else {
+ this.memoryInfo.commandNames.push(commandName);
+ await this.dispatcher.dispatch(commandName, params);
+ }
+
+ if (isFrozen) {
+ // Check if it's frozen for calling execCommand inside a call of
+ // execCommand Create the next memory slice (and freeze the
+ // current memory).
+ this._memoryID++;
+ const nextMemorySlice = this._memoryID.toString();
+ this.memory.create(nextMemorySlice);
+
+ // Send the commit message with a froozen memory.
+ const changesLocations = this.memory.getChangesLocations(
+ memorySlice,
+ this.memory.sliceKey,
+ );
+ await this.dispatcher.dispatchHooks('@commit', {
+ changesLocations: changesLocations,
+ commandNames: this.memoryInfo.commandNames,
+ });
+ }
}
/**
- * Execute arbitrary code in `callback`, then dispatch the event.
+ * Create a temporary range corresponding to the given boundary points and
+ * call the given callback with the newly created range as argument. The
+ * range is automatically destroyed after calling the callback.
+ *
+ * @param bounds The points corresponding to the range boundaries.
+ * @param callback The callback to call with the newly created range.
+ * @param mode
*/
- async execCustomCommand = Commands
>(
- callback: () => Promise,
+ async withRange(
+ bounds: [Point, Point],
+ callback: (range: VRange) => Promise | void,
+ mode?: Mode,
): Promise {
- await callback();
- await this.dispatcher.dispatchHooks('@custom');
+ return this.execCommand(async () => {
+ this.memoryInfo.commandNames.push('@withRange');
+ const range = new VRange(this, bounds, mode);
+ await callback(range);
+ range.remove();
+ });
}
/**
* Stop this editor instance.
*/
async stop(): Promise {
+ if (this.memory) {
+ this.memory.create('stop');
+ this.memory.switchTo('stop'); // Unfreeze the memory.
+ }
for (const plugin of this.plugins.values()) {
await plugin.stop();
}
+ if (this.memory) {
+ this.memory.create('stopped'); // Freeze the memory.
+ this.memory = null;
+ }
+ this.plugins.clear();
+ this.dispatcher = new Dispatcher(this);
+ this.selection = new VSelection(this);
+ this.contextManager = new ContextManager(this);
// Clear loaders.
this.loaders = {};
this._stage = EditorStage.CONFIGURATION;
diff --git a/packages/core/src/JWPlugin.ts b/packages/core/src/JWPlugin.ts
index 1fd0ce77b..de2e1067f 100644
--- a/packages/core/src/JWPlugin.ts
+++ b/packages/core/src/JWPlugin.ts
@@ -33,6 +33,8 @@ export class JWPlugin {
async stop(): Promise {
// This is where plugins can do asynchronous work when the editor is
// stopping (e.g. save on a server, close connections, etc).
+ this.dependencies.clear();
+ this.editor = null;
}
}
export interface JWPlugin {
diff --git a/packages/core/src/Memory/Memory.ts b/packages/core/src/Memory/Memory.ts
new file mode 100644
index 000000000..40eb7fccd
--- /dev/null
+++ b/packages/core/src/Memory/Memory.ts
@@ -0,0 +1,902 @@
+import { VersionableID, VersionableParams } from './Versionable';
+import { ArrayParams } from './VersionableArray';
+import {
+ removedItem,
+ memoryProxyPramsKey,
+ NotVersionableError,
+ VersionableAllreadyVersionableError,
+ MemoryError,
+ FroozenError,
+} from './const';
+
+type SliceKey = string;
+type VersionableMemoryID = number;
+export type MemoryAllowedType = MemoryAllowedPrimitiveTypes | MemoryAllowedObjectType;
+export type MemoryAllowedObjectType = LoopObject | LoopArray | LoopSet | object;
+interface LoopObject {
+ [x: string]: MemoryAllowedType;
+}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+type LoopArray = MemoryAllowedType[];
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+type LoopSet = Set;
+export type MemoryAllowedPrimitiveTypes =
+ | string
+ | number
+ | boolean
+ | VersionableID
+ | object
+ | Function
+ | symbol; // object must to be ProxyUniqID
+
+// MemoryType
+// MemoryType for Object
+
+// Type that the memory handles in practice. This is how it is stored in memory.
+export class MemoryTypeObject {
+ props: Record = {};
+}
+
+// MemoryType for Array
+
+// Type that the memory handles in practice. This is how it is stored in memory.
+export class MemoryTypeArray extends MemoryTypeObject {
+ patch: Record = {};
+}
+
+// Output of memory given to proxy to operate
+export class MemoryArrayCompiledWithPatch {
+ // the proxy has to differentiate between what was already there and what is
+ // being done in the current slice because deleting a key does not yield the
+ // same result if the key was already there before this slice or not (it
+ // would be marked as "removed" or ignored if wasn't already there.)
+ constructor(
+ // array as it appears in the slice right before the current one "array as of t-1"
+ public compiledValues: Record,
+ // very last patch at current time t
+ public newValues: MemoryTypeArray,
+ // new properties on the array at current time t
+ public props: Record,
+ ) {}
+}
+
+// MemoryType for Set
+
+// Type that the memory handles in practice. This is how it is stored in memory.
+export class MemoryTypeSet {
+ add: Set = new Set();
+ delete: Set = new Set();
+}
+// Output of memory given to proxy to operate (not sure it is useful to rename
+// the type. semantic value of "compiled")
+export type MemoryCompiledSet = Set;
+
+export type MemoryType = MemoryTypeObject | MemoryTypeArray | MemoryTypeSet;
+
+// MemoryType end
+
+export type MemoryCompiled = MemoryTypeObject | MemoryArrayCompiledWithPatch | MemoryCompiledSet;
+
+// List of memory accessors usable by versionable (we don't want to give them
+// access to the true memory object !)
+export interface MemoryWorker {
+ ID: number;
+ getProxy: (ID: VersionableID) => object;
+ getSlice: () => Record;
+ getSliceValue: (ID: VersionableID) => MemoryCompiled;
+ isFrozen: () => boolean;
+ markDirty: (ID: VersionableID) => void;
+ addSliceProxyParent: (
+ ID: VersionableID,
+ parentID: VersionableID,
+ attributeName: string,
+ ) => boolean;
+ deleteSliceProxyParent: (
+ ID: VersionableID,
+ parentID: VersionableID,
+ attributeName: string,
+ ) => boolean;
+ linkToMemory: (obj: MemoryAllowedType) => MemoryAllowedType;
+}
+
+export type patches = { sliceKey: SliceKey; value: MemoryType }[];
+type proxyAttributePath = string[];
+
+export interface ChangesLocations {
+ add: object[]; // The added versionables
+ move: object[]; // The versionables that changed parents
+ remove: object[]; // Versionables that had one or more parents and no longer have any.
+ update: [
+ object, // The versionables that changed
+ (
+ | string[] // The updated object keys
+ | number[] // the updated indexes for versionable array
+ | void
+ ), // Nothing for the versionable set
+ ][];
+}
+
+export const parentedPathSeparator = '•';
+
+export function markAsDiffRoot(obj: MemoryAllowedObjectType): void {
+ obj[memoryProxyPramsKey].isDiffRoot = true;
+}
+
+let memoryID = 0;
+const memoryRootSliceName = '';
+
+export class MemorySlice {
+ name: SliceKey;
+ parent: MemorySlice;
+ children: MemorySlice[] = [];
+ snapshotOrigin?: MemorySlice;
+ snapshot?: MemorySlice;
+ data: Record = {}; // registry of values
+ linkedParentOfProxy: Record = {};
+ invalidCache: Record = {}; // paths that have been changed in given memory slice (when switching slice, this set is loaded in _invalidateCache)
+ ids: Set = new Set();
+ constructor(name: SliceKey, parent?: MemorySlice) {
+ this.name = name;
+ this.parent = parent;
+ }
+ getPrevious(): MemorySlice {
+ return this.snapshotOrigin ? this.snapshotOrigin.parent : this.parent;
+ }
+}
+
+export class Memory {
+ _id: number;
+ _sliceKey: SliceKey; // identifier or current memory slice
+ _slices: Record = {}; // Record
+ _currentSlice: MemorySlice; // Record
+ _proxies: Record = {};
+ _rootProxies: Record = {};
+ _memoryWorker: MemoryWorker;
+ _numberOfFlatSlices = 40;
+ _numberOfSlicePerSnapshot = 8;
+ _autoSnapshotCheck = 0;
+
+ constructor() {
+ this._id = ++memoryID;
+ this.create(memoryRootSliceName);
+ this.switchTo(memoryRootSliceName);
+ this._memoryWorker = {
+ ID: this._id,
+ getProxy: (ID: VersionableID): object => this._proxies[ID as VersionableMemoryID],
+ getSlice: (): Record => this._currentSlice.data,
+ getSliceValue: (ID: VersionableID): MemoryCompiled =>
+ this._getValue(this._sliceKey, ID),
+ isFrozen: this.isFrozen.bind(this),
+ // Mark as "modified" in this slice
+ markDirty: (ID: VersionableID): boolean =>
+ (this._currentSlice.invalidCache[ID as VersionableMemoryID] = true),
+ // I am the proxy, I tell you I synchornized the value
+ deleteSliceProxyParent: this._deleteSliceProxyParent.bind(this),
+ addSliceProxyParent: this._addSliceProxyParent.bind(this),
+ linkToMemory: this._linkToMemory.bind(this),
+ };
+ Object.freeze(this._memoryWorker);
+ }
+
+ get sliceKey(): string {
+ return this._sliceKey;
+ }
+
+ /**
+ * Create a memory slice.
+ * Modifications and changes (of objects bound to memory) are all recorded
+ * in these slices. The new slice created will be noted as being the
+ * continuation (or the child) of the current one.
+ *
+ * A slice with "children" is immutable. The modifications are therefore
+ * blocked and an error will be triggered if a code tries to modify one of
+ * these objects. To be able to edit again, you must destroy the "child"
+ * slices or change the memory slice.
+ *
+ * @param sliceKey
+ */
+ create(sliceKey: SliceKey): this {
+ this._create(sliceKey, this._sliceKey);
+ return this;
+ }
+ /**
+ * Change the working memory slice (this must be created beforehand).
+ *
+ * @param sliceKey
+ */
+ switchTo(sliceKey: SliceKey): this {
+ if (!(sliceKey in this._slices)) {
+ throw new MemoryError(
+ 'You must create the "' + sliceKey + '" slice before switch on it',
+ );
+ }
+ if (sliceKey === this._sliceKey) {
+ return;
+ }
+ const invalidCache = this._aggregateInvalidCaches(this._sliceKey, sliceKey);
+ this._currentSlice = this._slices[sliceKey];
+ this._sliceKey = sliceKey;
+ for (const key of invalidCache) {
+ const proxy = this._proxies[key];
+ const params = proxy[memoryProxyPramsKey] as VersionableParams;
+ params.synchronize();
+ }
+ this._autoSnapshotCheck++;
+ if (!(this._autoSnapshotCheck % this._numberOfSlicePerSnapshot)) {
+ this._autoSnapshot();
+ }
+ return this;
+ }
+ /**
+ * Attach a versionable to memory.
+ * The versionable will then be versioned and its modifications will be
+ * recorded in the corresponding memory slots.
+ * All other versionables linked to given versionable attached to memory
+ * are automatically linked to memory.
+ *
+ * (Items bound to memory by this function will be noted as part of the
+ * root of changes @see getRoots )
+ *
+ * @param versionable
+ */
+ attach(versionable: MemoryAllowedObjectType): void {
+ const params = versionable[memoryProxyPramsKey] as VersionableParams;
+ if (!params) {
+ throw new NotVersionableError();
+ }
+ if (params.object === versionable) {
+ throw new VersionableAllreadyVersionableError();
+ }
+ if (!params.verify(versionable)) {
+ throw new NotVersionableError();
+ }
+ if (this.isFrozen()) {
+ throw new FroozenError();
+ }
+ if (!params.memory || params.memory !== this._memoryWorker) {
+ params.isDiffRoot = true;
+ this._linkToMemory(versionable);
+ }
+ this._rootProxies[params.ID as VersionableMemoryID] = true;
+ }
+ /**
+ * Returns the parents of the object.
+ *
+ * Example: p = {}; v = {point: p} axis = {origin: p}
+ * The parents of p are [[v, ['point']], [axis, ['origin']]]
+ *
+ * @param versionable
+ */
+ getParents(versionable: MemoryAllowedObjectType): Map {
+ const pathChanges = new Map();
+ const nodeID = versionable[memoryProxyPramsKey].ID;
+ const pathList: [VersionableID, string[]][] = [[nodeID, []]];
+ while (pathList.length) {
+ const path = pathList.pop();
+ const [nodeID, pathToNode] = path;
+
+ const parentProxy: MemoryAllowedObjectType = this._proxies[
+ nodeID as VersionableMemoryID
+ ];
+ let paths: string[][] = pathChanges.get(parentProxy);
+ if (!paths) {
+ paths = [];
+ pathChanges.set(parentProxy, paths);
+ }
+ paths.push(path[1]);
+
+ if (this._rootProxies[nodeID as VersionableMemoryID]) {
+ continue;
+ }
+ this._getProxyParentedPath(this._sliceKey, nodeID).forEach(path => {
+ const parentNodeID = path.split(parentedPathSeparator, 1)[0];
+ const partPath = path.slice(parentNodeID.length + 1);
+ pathList.push([+parentNodeID, [partPath].concat(pathToNode)]);
+ });
+ }
+ return pathChanges;
+ }
+ /**
+ * Return the location of the changes.
+ *
+ * @param from
+ * @param to
+ */
+ getChangesLocations(from: SliceKey, to: SliceKey): ChangesLocations {
+ const diff: ChangesLocations = {
+ add: [],
+ move: [],
+ remove: [],
+ update: [],
+ };
+
+ const ancestorKey = this._getCommonAncestor(from, to);
+ const refs = this._getChangesPath(from, to, ancestorKey);
+ if (from === ancestorKey && from !== to) {
+ refs.shift();
+ }
+
+ const removeFromUpdate = new Set();
+
+ let previous: MemorySlice;
+ let ref: MemorySlice;
+ while ((ref = refs.pop())) {
+ const linkedParentOfProxy = ref.linkedParentOfProxy;
+ for (const ID in linkedParentOfProxy) {
+ const proxy = this._proxies[ID];
+ if (linkedParentOfProxy[ID].length) {
+ if (ref.ids.has(+ID)) {
+ diff.add.push(proxy);
+ removeFromUpdate.add(proxy);
+ } else {
+ diff.move.push(proxy);
+ }
+ } else {
+ diff.remove.push(proxy);
+ removeFromUpdate.add(proxy);
+ }
+ }
+
+ if (ref.parent === previous) {
+ for (const ID of previous.ids) {
+ const proxy = this._proxies[ID];
+ diff.remove.push(proxy);
+ removeFromUpdate.add(proxy);
+ }
+ }
+
+ const slice = ref.data;
+ Object.keys(slice).forEach(ID => {
+ const id = +ID;
+ const memoryItem = slice[id];
+ const proxy = this._proxies[ID];
+ if (removeFromUpdate.has(proxy)) {
+ return;
+ } else if (memoryItem instanceof MemoryTypeArray) {
+ const keys = Object.keys(memoryItem.props);
+ if (keys.length) {
+ diff.update.push([proxy, keys]);
+ }
+ const params = proxy[memoryProxyPramsKey] as ArrayParams;
+ const uniqIDs = params.uniqIDs;
+ const len = uniqIDs.length;
+ const half = len;
+ const indexes: number[] = [];
+ for (const i in memoryItem.patch) {
+ let index = half;
+ let step = index + 1;
+ while (step) {
+ const value = uniqIDs[index];
+ if (value === i) {
+ break;
+ } else if (value > i) {
+ index -= step;
+ if (index < 0) {
+ index = 0;
+ }
+ } else {
+ index += step;
+ if (index >= len) {
+ index = len - 1;
+ }
+ }
+ step = (step / 2) | 0;
+ if (step === 0 && value > i && uniqIDs[index] < i) {
+ index++;
+ }
+ }
+ if (!indexes.includes(index)) {
+ indexes.push(index);
+ }
+ }
+ if (indexes.length) {
+ diff.update.push([proxy, indexes]);
+ }
+ } else if (memoryItem instanceof MemoryTypeSet) {
+ diff.update.push([proxy, null]);
+ } else {
+ const keys = Object.keys(memoryItem.props);
+ if (keys.length) {
+ diff.update.push([proxy, keys]);
+ }
+ }
+ });
+
+ previous = ref;
+ }
+ return diff;
+ }
+ /**
+ * Get if the current memory slice are imutable or not.
+ *
+ */
+ isFrozen(): boolean {
+ return this._currentSlice.children.length > 0;
+ }
+ /**
+ * Remove a memory slice.
+ * The current slice cannot be the one being deleted or one of its children.
+ *
+ * @param sliceKey
+ */
+ remove(sliceKey: SliceKey): this {
+ if (!(sliceKey in this._slices)) {
+ return this;
+ }
+ if (sliceKey === memoryRootSliceName) {
+ throw new MemoryError('You should not remove the original memory slice');
+ }
+
+ let ref = this._slices[this._sliceKey];
+ while (ref) {
+ if (ref.name === sliceKey) {
+ throw new MemoryError('Please switch to a non-children slice before remove it');
+ }
+ ref = ref.parent;
+ }
+
+ const IDs = this._remove(sliceKey);
+ // check if the IDs are linked evrywere
+ Object.values(this._slices).forEach(reference => {
+ const linkedParentOfProxy = reference.linkedParentOfProxy;
+ IDs.forEach(ID => {
+ if (ID in linkedParentOfProxy) {
+ IDs.delete(ID);
+ }
+ });
+ });
+ // remove unlinked items
+ IDs.forEach(ID => {
+ delete this._proxies[ID];
+ delete this._rootProxies[ID];
+ });
+ return this;
+ }
+ /**
+ * Return ancestor versionables noted as roots.
+ *
+ * There are two ways for a versionable to be root, either via the
+ * 'linkToMemory' method, or with the 'markAsDiffRoot' utility function.
+ *
+ * @param proxy
+ */
+ getRoots(proxy: MemoryAllowedObjectType): Set {
+ const roots: Set = new Set();
+
+ const nodeID = proxy[memoryProxyPramsKey].ID;
+ const pathList: [VersionableID, string[]][] = [[nodeID, []]];
+ while (pathList.length) {
+ const path = pathList.pop();
+ const [nodeID, pathToNode] = path;
+ if (this._rootProxies[nodeID as VersionableMemoryID]) {
+ roots.add(this._proxies[nodeID as VersionableMemoryID]);
+ continue;
+ }
+ this._getProxyParentedPath(this._sliceKey, nodeID).forEach(path => {
+ const parentNodeID = path.split(parentedPathSeparator, 1)[0];
+ const partPath = path.slice(parentNodeID.length + 1);
+ pathList.push([+parentNodeID, [partPath].concat(pathToNode)]);
+ });
+ }
+ return roots;
+ }
+ /**
+ * Return the list of names of all previous memory slice of the given
+ * memory slice.
+ *
+ * @param sliceKey
+ * @param withoutSnapshot
+ */
+ getPath(sliceKey: SliceKey, withoutSnapshot?: boolean): SliceKey[] {
+ const sliceKeys = [];
+ let ref = this._slices[sliceKey];
+ while (ref && ref.name) {
+ sliceKeys.push(ref.name);
+ ref = withoutSnapshot ? ref.getPrevious() : ref.parent;
+ }
+ return sliceKeys;
+ }
+ /**
+ * Create the snapshot of different memory slices (use the path between the
+ * memory slice to get all changes) and merge the changes into a new
+ * destination slice.
+ *
+ * @param fromSliceKey
+ * @param unitSliceKey
+ * @param newSliceKey
+ */
+ snapshot(fromSliceKey: SliceKey, unitSliceKey: SliceKey, newSliceKey: SliceKey): void {
+ const refs = this._slices;
+ const fromRref = refs[fromSliceKey];
+ const untilRref = refs[unitSliceKey];
+
+ const newRef = this._create(newSliceKey, fromRref.parent && fromRref.parent.name);
+ this._squashInto(fromSliceKey, unitSliceKey, newRef.name);
+
+ untilRref.children.forEach(child => {
+ child.parent = newRef;
+ });
+ newRef.children = untilRref.children;
+ untilRref.children = [];
+ untilRref.snapshot = newRef;
+ newRef.snapshotOrigin = untilRref;
+ }
+ /**
+ * Compress all changes between two parented memory slices and remove all
+ * children memory slice.
+ *
+ * @param fromSliceKey
+ * @param unitSliceKey
+ */
+ compress(fromSliceKey: SliceKey, unitSliceKey: SliceKey): boolean {
+ const refs = this._slices;
+ const fromRref = refs[fromSliceKey];
+ const untilRref = refs[unitSliceKey];
+ const toRemove = fromRref.children.slice().map(ref => ref.name);
+ fromRref.children = untilRref.children.splice(0);
+ this._squashInto(fromSliceKey, unitSliceKey, fromSliceKey);
+ let key: SliceKey;
+ while ((key = toRemove.pop())) {
+ this._remove(key);
+ }
+ return true;
+ }
+
+ /////////////////////////////////////////////////////
+ // private
+ /////////////////////////////////////////////////////
+
+ private _addSliceProxyParent(
+ ID: VersionableID,
+ parentID: VersionableID,
+ attributeName: string,
+ ): void {
+ const sliceKey = this._sliceKey;
+ const sliceLinkedParentOfProxy = this._slices[sliceKey].linkedParentOfProxy;
+ const path =
+ parentID +
+ (attributeName === undefined
+ ? memoryRootSliceName
+ : parentedPathSeparator + attributeName);
+ let parents = sliceLinkedParentOfProxy[ID as VersionableMemoryID];
+ if (!parents) {
+ const parented = this._getProxyParentedPath(sliceKey, ID);
+ parents = sliceLinkedParentOfProxy[ID as VersionableMemoryID] = parented
+ ? parented.slice()
+ : [];
+ }
+ parents.push(path);
+ }
+ private _deleteSliceProxyParent(
+ ID: VersionableID,
+ parentID: VersionableID,
+ attributeName: string,
+ ): void {
+ const sliceKey = this._sliceKey;
+ const sliceLinkedParentOfProxy = this._slices[sliceKey].linkedParentOfProxy;
+ const path =
+ parentID +
+ (attributeName === undefined
+ ? memoryRootSliceName
+ : parentedPathSeparator + attributeName);
+ let parents = this._getProxyParentedPath(sliceKey, ID);
+ const index = parents.indexOf(path);
+ if (!sliceLinkedParentOfProxy[ID as VersionableMemoryID]) {
+ parents = sliceLinkedParentOfProxy[ID as VersionableMemoryID] = parents.slice();
+ }
+ parents.splice(index, 1);
+ }
+ private _compiledArrayPatches(patches: patches): MemoryTypeArray {
+ const props = {};
+ const valueBySeq = {};
+ while (patches.length) {
+ const patch = patches.pop();
+ const step = patch.value as MemoryTypeArray;
+ Object.assign(props, step.props);
+ Object.assign(valueBySeq, step.patch);
+ }
+ return {
+ patch: valueBySeq,
+ props: props,
+ };
+ }
+ private _compiledSetPatches(patches: patches): MemoryCompiledSet {
+ const obj = new Set() as MemoryCompiledSet;
+ while (patches.length) {
+ const patch = patches.pop();
+ const step = patch.value as MemoryTypeSet;
+ step.add.forEach((item: MemoryAllowedPrimitiveTypes) =>
+ (obj as MemoryCompiledSet).add(item),
+ );
+ step.delete.forEach((item: MemoryAllowedPrimitiveTypes) =>
+ (obj as MemoryCompiledSet).delete(item),
+ );
+ }
+ return obj;
+ }
+ private _compiledObjectPatches(patches: patches): MemoryTypeObject {
+ const obj = new MemoryTypeObject();
+ const props = obj.props;
+ while (patches.length) {
+ const patch = patches.pop();
+ const step = patch.value as MemoryTypeObject;
+ Object.assign(props, step.props);
+ }
+ Object.keys(props).forEach(key => {
+ if (props[key] === removedItem) {
+ delete props[key];
+ }
+ });
+ return obj;
+ }
+ private _create(sliceKey: SliceKey, fromSliceKey: SliceKey): MemorySlice {
+ const refs = this._slices;
+ if (refs[sliceKey]) {
+ throw new Error('The memory slice "' + sliceKey + '" already exists');
+ }
+ const parent = refs[fromSliceKey];
+ const ref = (refs[sliceKey] = new MemorySlice(sliceKey, parent));
+ if (parent) {
+ parent.children.push(ref);
+ }
+ return ref;
+ }
+ private _getChangesPath(
+ fromSliceKey: SliceKey,
+ toSliceKey: SliceKey,
+ ancestorKey: SliceKey,
+ ): MemorySlice[] {
+ const fromPath = [];
+ let ref = this._slices[fromSliceKey];
+ while (ref) {
+ fromPath.push(ref);
+ if (ref.name === ancestorKey) {
+ break;
+ }
+ ref = ref.getPrevious();
+ }
+
+ const toPath = [];
+ ref = this._slices[toSliceKey];
+ while (ref) {
+ if (ref.name === ancestorKey) {
+ break;
+ }
+ toPath.push(ref);
+ ref = ref.getPrevious();
+ }
+ toPath.reverse();
+ return fromPath.concat(toPath);
+ }
+ private _getCommonAncestor(sliceKeyA: SliceKey, sliceKeyB: SliceKey): SliceKey {
+ const rootB = this._slices[sliceKeyB];
+ let refA = this._slices[sliceKeyA];
+ while (refA) {
+ let refB = rootB;
+ while (refB) {
+ if (refA.name === refB.name) {
+ return refA.name;
+ }
+ refB = refB.getPrevious();
+ }
+ refA = refA.getPrevious();
+ }
+ }
+ private _getProxyParentedPath(sliceKey: SliceKey, ID: VersionableID): proxyAttributePath {
+ // bubbling up magic for proxyParents
+ let ref = this._slices[sliceKey];
+ while (ref) {
+ const slice = ref.linkedParentOfProxy;
+ const path = slice && slice[ID as VersionableMemoryID];
+ if (path) {
+ return path;
+ }
+ ref = ref.parent;
+ }
+ return [];
+ }
+ private _getValue(sliceKey: string, ID: VersionableID): MemoryCompiled {
+ const patch = this._getPatches(undefined, sliceKey, ID);
+ if (!patch) {
+ return;
+ }
+ if (patch.type === 'set') {
+ return this._compiledSetPatches(patch.patches);
+ } else if (patch.type === 'array') {
+ return this._getValueArray(sliceKey, ID, patch.patches);
+ } else {
+ return this._compiledObjectPatches(patch.patches);
+ }
+ }
+ private _getValueArray(sliceKey: string, ID: VersionableID, patches: patches): MemoryCompiled {
+ const ref = this._slices[sliceKey];
+ let owner: MemoryTypeArray;
+ if (ref.data[ID as VersionableMemoryID]) {
+ owner = patches.shift().value as MemoryTypeArray;
+ }
+ const value = this._compiledArrayPatches(patches);
+ return new MemoryArrayCompiledWithPatch(
+ value.patch,
+ owner || new MemoryTypeArray(),
+ value.props,
+ );
+ }
+ private _getPatches(
+ fromSliceKey: SliceKey,
+ toSliceKey: string,
+ ID: VersionableID,
+ ): { patches: patches; type: string } {
+ let ref = this._slices[toSliceKey];
+ let type: string;
+ const patches = [];
+ while (ref && ref.name !== fromSliceKey) {
+ const slice = ref.data;
+ const value = slice && slice[ID as VersionableMemoryID];
+ if (!value) {
+ ref = ref.parent;
+ continue;
+ }
+ if (!type) {
+ if (value instanceof MemoryTypeArray) {
+ type = 'array';
+ } else if (value instanceof MemoryTypeSet) {
+ type = 'set';
+ } else {
+ type = 'object';
+ }
+ }
+ patches.push({
+ sliceKey: ref.name,
+ value: value,
+ });
+ ref = ref.parent;
+ }
+ if (!type) {
+ return;
+ }
+ return {
+ patches: patches,
+ type: type,
+ };
+ }
+ private _aggregateInvalidCaches(from: SliceKey, to: SliceKey): Set {
+ const invalidCache = new Set();
+ if (from === to) {
+ return invalidCache;
+ }
+ const ancestorKey = this._getCommonAncestor(from, to);
+ const refs = this._getChangesPath(from, to, ancestorKey);
+ if (this._sliceKey === ancestorKey) {
+ refs.shift();
+ }
+ while (refs.length) {
+ const ref = refs.pop();
+ Object.keys(ref.invalidCache).forEach(key => {
+ // It was invalid before, it is still invalid now since it wasn't yet read
+ invalidCache.add(+key);
+ });
+ }
+ return invalidCache;
+ }
+ private _linkToMemory(proxy: MemoryAllowedObjectType): void {
+ const params: VersionableParams = proxy[memoryProxyPramsKey];
+ if (params.memory) {
+ if (params.memory !== this._memoryWorker) {
+ throw new MemoryError('This object is already linked to a other memory');
+ }
+ return;
+ }
+
+ const ID = params.ID as VersionableMemoryID;
+ params.memory = this._memoryWorker;
+ params.linkCallback(this._memoryWorker);
+ this._proxies[ID] = proxy;
+
+ if (params.isDiffRoot) {
+ this._rootProxies[ID] = true;
+ }
+
+ this._currentSlice.ids.add(+ID);
+ }
+ private _remove(sliceKey: SliceKey): Set {
+ const IDs = [];
+ let ref = this._slices[sliceKey];
+ const index = ref.parent.children.indexOf(ref);
+ ref.parent.children.splice(index, 1);
+
+ const refs = [ref];
+ while ((ref = refs.pop())) {
+ const sliceKey = ref.name;
+ ref.children.forEach(ref => refs.push(ref));
+ Object.keys(ref.linkedParentOfProxy).forEach(ID => IDs.push(+ID));
+ delete this._slices[sliceKey];
+ }
+ return new Set(IDs);
+ }
+ private _squashInto(
+ fromSliceKey: SliceKey,
+ unitSliceKey: SliceKey,
+ intoSliceKey: SliceKey,
+ ): void {
+ const refs = this._slices;
+ const fromRref = refs[fromSliceKey];
+ const untilRref = refs[unitSliceKey];
+ const intoRef = refs[intoSliceKey];
+
+ const references = [];
+ let ref = untilRref;
+ while (ref) {
+ references.push(ref);
+ if (ref === fromRref) {
+ break;
+ }
+ ref = ref.parent;
+ }
+ if (!ref) {
+ throw new Error('Can not merge the slices');
+ }
+
+ const intoLinkedParentOfProxy = refs[intoSliceKey].linkedParentOfProxy;
+ const intoInvalidCache = refs[intoSliceKey].invalidCache;
+ const intoSlices = intoRef.data;
+
+ while ((ref = references.pop())) {
+ const LinkedParentOfProxy = ref.linkedParentOfProxy;
+ Object.keys(LinkedParentOfProxy).forEach(ID => {
+ intoLinkedParentOfProxy[ID] = LinkedParentOfProxy[ID].slice();
+ });
+
+ Object.keys(ref.invalidCache).forEach(link => {
+ intoInvalidCache[link] = true;
+ });
+
+ const slice = ref.data;
+ Object.keys(slice).forEach(ID => {
+ const id = +ID;
+ const memoryItem = slice[id];
+ if (!intoSlices[id]) {
+ intoSlices[id] = memoryItem;
+ return;
+ }
+ if (memoryItem instanceof MemoryTypeArray) {
+ const intoItem = intoSlices[id] as MemoryTypeArray;
+ Object.assign(intoItem.patch, memoryItem.patch);
+ Object.assign(intoItem.props, memoryItem.props);
+ } else if (memoryItem instanceof MemoryTypeSet) {
+ const intoItem = intoSlices[id] as MemoryTypeSet;
+ memoryItem.add.forEach(item => {
+ if (!intoItem.delete.has(item)) {
+ intoItem.add.add(item);
+ } else {
+ intoItem.delete.delete(item);
+ }
+ });
+ memoryItem.delete.forEach(item => {
+ if (!intoItem.add.has(item)) {
+ intoItem.delete.add(item);
+ } else {
+ intoItem.add.delete(item);
+ }
+ });
+ } else {
+ const intoItem = intoSlices[id] as MemoryTypeObject;
+ Object.assign(intoItem.props, memoryItem.props);
+ }
+ });
+ }
+ }
+ private _autoSnapshot(): void {
+ const refs = [];
+ let ref = this._currentSlice;
+ while (ref && ref.name) {
+ refs.push(ref);
+ ref = ref.parent;
+ }
+ if (refs.length > this._numberOfFlatSlices + this._numberOfSlicePerSnapshot) {
+ const fromSliceKey = refs[refs.length - 1].name;
+ const unitSliceKey = refs[refs.length - 1 - this._numberOfSlicePerSnapshot].name;
+ const newSliceKey = unitSliceKey + '[snapshot from ' + fromSliceKey + ']';
+ this.snapshot(fromSliceKey, unitSliceKey, newSliceKey);
+ }
+ }
+}
diff --git a/packages/core/src/Memory/Versionable.ts b/packages/core/src/Memory/Versionable.ts
new file mode 100644
index 000000000..ab2a9ef0a
--- /dev/null
+++ b/packages/core/src/Memory/Versionable.ts
@@ -0,0 +1,157 @@
+import {
+ MemoryAllowedType,
+ MemoryAllowedObjectType,
+ MemoryWorker,
+ MemoryTypeArray,
+ MemoryTypeObject,
+ MemoryTypeSet,
+} from './Memory';
+import { _proxifyObject } from './VersionableObject';
+import { _proxifyArray } from './VersionableArray';
+import { _proxifySet } from './VersionableSet';
+import {
+ memoryProxyNotVersionableKey,
+ NotVersionableError,
+ VersionableAllreadyVersionableError,
+ memoryProxyPramsKey,
+} from './const';
+
+export class VersionableID extends Number {}
+VersionableID.prototype[memoryProxyNotVersionableKey] = true;
+
+let MemoryID = 0;
+export function generateVersionableID(): VersionableID {
+ return new VersionableID(++MemoryID);
+}
+
+// Magic memory key that is set on the object through a symbol (inaccessible for
+// others). It stores everything that is useful for the memory to handle this object.
+export interface VersionableParams {
+ ID: VersionableID; // proxy id in memory.
+ memory?: MemoryWorker; // interface for versionale to access memory (since we don't want to give them the true memory object !)
+ proxy: MemoryAllowedObjectType; // proxy itself (what we return, what people actually manipulate)
+ object: MemoryAllowedObjectType; // Object that is proxified (it is synchronized with the proxy because the proxy updates it at some points ... -- not the Set !)
+ linkCallback: (memory: MemoryWorker) => void; // function to call when this versionable is linked to a memory
+ synchronize: () => void; // Synchronize the versionable object with the memory information.
+ verify: (obj: MemoryAllowedType) => boolean; // Verify if the given proxy matches the versionable object.
+ MemoryType: typeof MemoryTypeObject | typeof MemoryTypeArray | typeof MemoryTypeSet;
+ isDiffRoot?: true; // is it a standalone object or does it only makes sense as part of an other object's structure ?
+}
+
+// queue of stuff to proxify
+const toProxify = new Map void>>();
+
+/**
+ * Take an object and return a versionable proxy to this object.
+ *
+ * @param object
+ */
+export function makeVersionable(object: T): T {
+ const params = object[memoryProxyPramsKey] as VersionableParams;
+ if (params) {
+ if (params.object === object) {
+ throw new VersionableAllreadyVersionableError();
+ }
+ if (params && params.verify(object)) {
+ return object;
+ }
+ }
+ const proxy = _proxify(object);
+ toProxify.forEach((callbacks, torototo) => {
+ toProxify.delete(torototo);
+ const proxy = _proxify(torototo);
+ callbacks.forEach(callback => callback(proxy));
+ });
+ return proxy;
+}
+/**
+ * Create a proxy from a versionable with given handler.
+ *
+ * @param versionable
+ * @param handler
+ */
+export function proxifyVersionable(
+ versionable: T,
+ handler: ProxyHandler,
+): T {
+ const newProxy = new Proxy(versionable as object, handler);
+ versionable[memoryProxyPramsKey].proxy = newProxy;
+ return newProxy as T;
+}
+/**
+ * Mark the current object as not versionable in memory.
+ * A non versionable object is not linked to the memory. The memory does not
+ * take care of the change inside this object, and this object is nerver
+ * immutable.
+ *
+ * @param object
+ */
+export function markNotVersionable(object: MemoryAllowedType): void {
+ object[memoryProxyNotVersionableKey] = true;
+}
+/**
+ * Throw an error if the given object is not a versionable.
+ *
+ * @param object
+ */
+export function _checkVersionable(object: T): void {
+ if (
+ typeof object !== 'object' ||
+ object === null ||
+ object[memoryProxyNotVersionableKey] // this is set by the user
+ ) {
+ return;
+ }
+
+ const params = object[memoryProxyPramsKey];
+ if (params) {
+ if (params.object === object) {
+ throw new VersionableAllreadyVersionableError();
+ }
+ if (params.verify(object)) {
+ // Already versioned ! (we could have inherited from the `params` of
+ // another, already versioned object, but we might not)
+ return;
+ }
+ }
+
+ throw new NotVersionableError();
+}
+// Recursive proxification is very limited because of callback depth. To
+// circumvent this issue, we queue the proxification of children.
+export function _stackedProxify(
+ customClass: T,
+ callback: (proxy: T) => void,
+): void {
+ if (
+ !customClass ||
+ typeof customClass !== 'object' ||
+ customClass[memoryProxyNotVersionableKey]
+ ) {
+ callback(customClass);
+ return;
+ }
+ const params = customClass[memoryProxyPramsKey];
+ if (params) {
+ callback(params.proxy);
+ }
+ const callbacks = toProxify.get(customClass) || [];
+ toProxify.set(customClass, callbacks);
+ callbacks.push(callback);
+}
+
+function _proxify(customClass: T): T {
+ const params = customClass[memoryProxyPramsKey];
+ if (params && params.verify(customClass)) {
+ return params.proxy;
+ }
+ let proxy: T;
+ if (customClass instanceof Set) {
+ proxy = _proxifySet(customClass) as T;
+ } else if (customClass instanceof Array) {
+ proxy = _proxifyArray(customClass) as T;
+ } else {
+ proxy = _proxifyObject(customClass as Record) as T;
+ }
+ return proxy;
+}
diff --git a/packages/core/src/Memory/VersionableArray.ts b/packages/core/src/Memory/VersionableArray.ts
new file mode 100644
index 000000000..b184bbd9f
--- /dev/null
+++ b/packages/core/src/Memory/VersionableArray.ts
@@ -0,0 +1,566 @@
+import {
+ MemoryAllowedType,
+ MemoryAllowedPrimitiveTypes,
+ MemoryWorker,
+ MemoryType,
+ MemoryTypeArray,
+ MemoryArrayCompiledWithPatch,
+} from './Memory';
+import { VersionableID, _checkVersionable, VersionableParams } from './Versionable';
+import {
+ removedItem,
+ memoryProxyPramsKey,
+ memoryProxyNotVersionableKey,
+ FroozenError,
+} from './const';
+import {
+ _proxifyObject,
+ GettersSetters,
+ ProxyObjectHandler,
+ proxyObjectHandler,
+ keyType,
+} from './VersionableObject';
+
+type uniqSec = number[];
+type uniqID = string;
+type uniqIDMemoryTypeValues = Record;
+
+const Undefined = Symbol('jabberwockMemoryUndefined');
+
+export interface ArrayParams extends VersionableParams {
+ uniqIDs: uniqID[];
+ sequences: uniqID[];
+ proxy: MemoryAllowedType[];
+ object: MemoryAllowedType[];
+ map: Map;
+ syncSlice?: Record;
+ previousSliceValues?: uniqIDMemoryTypeValues;
+ proto: GettersSetters;
+}
+
+const proxyArrayHandler = {
+ get(array: MemoryAllowedType[], prop: keyType, proxy: object): MemoryAllowedType {
+ if (typeof prop === 'symbol' || !isNaN(prop as number)) {
+ return array[prop];
+ }
+ const params = array[memoryProxyPramsKey];
+ if (!params.memory) {
+ return array[prop];
+ }
+ switch (prop) {
+ case 'indexOf':
+ return indexOf.bind(proxy, params);
+ case 'includes':
+ return includes.bind(proxy, params);
+ case 'splice':
+ return splice.bind(proxy, params);
+ case 'push':
+ return array[prop];
+ case 'unshift':
+ return unshift.bind(proxy, params);
+ case 'shift':
+ return shift.bind(proxy, params);
+ case 'pop':
+ return pop.bind(proxy, params);
+ case 'forEach':
+ return forEach.bind(proxy, params);
+ case 'map':
+ return map.bind(proxy, params);
+ case 'filter':
+ return filter.bind(proxy, params);
+ default:
+ return array[prop];
+ }
+ },
+ set(
+ proxyObject: MemoryAllowedType[],
+ prop: keyType,
+ value: MemoryAllowedType,
+ proxy: object,
+ ): boolean {
+ const params: ArrayParams = proxyObject[memoryProxyPramsKey];
+ const array = params.object;
+ if (
+ typeof prop === 'symbol' ||
+ !params.memory ||
+ (prop !== 'length' && isNaN(prop as number))
+ ) {
+ return proxyObjectHandler.set(array, prop, value, proxy);
+ }
+
+ const index = +prop;
+ const memory = params.memory;
+ if (memory.isFrozen()) {
+ throw new FroozenError();
+ }
+
+ const oldValue = array[prop];
+ if (oldValue === value || (value === removedItem && !(prop in array))) {
+ // no change
+ return true;
+ }
+
+ const slice = memory.getSlice();
+ let memoryArray = slice[params.ID as number] as MemoryTypeArray;
+ if (!memoryArray) {
+ slice[params.ID as number] = memoryArray = new params.MemoryType() as MemoryTypeArray;
+ }
+ if (slice !== params.syncSlice) {
+ // allready sync, the current value (before update) is the previous value
+ params.syncSlice = slice;
+ params.previousSliceValues = {};
+ array.forEach((value: MemoryAllowedType, index: number) => {
+ params.previousSliceValues[params.uniqIDs[index]] = value;
+ });
+ }
+
+ if (prop === 'length') {
+ const length = +(value as number);
+ for (let index = length; index < array.length; index++) {
+ const val = array[index];
+ const oldParams = typeof val === 'object' && val[memoryProxyPramsKey];
+ if (oldParams) {
+ memory.deleteSliceProxyParent(
+ oldParams.ID,
+ params.ID,
+ '´' + params.uniqIDs[index],
+ );
+ }
+ const uid = params.uniqIDs[index];
+ if (
+ params.previousSliceValues[uid] === removedItem ||
+ !(uid in params.previousSliceValues)
+ ) {
+ delete memoryArray.patch[uid];
+ } else {
+ memoryArray.patch[uid] = removedItem;
+ }
+ }
+ array.length = length;
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ return true;
+ }
+
+ let newParams: VersionableParams;
+ if (value !== null && typeof value === 'object' && !value[memoryProxyNotVersionableKey]) {
+ _checkVersionable(value);
+ memory.linkToMemory(value as object);
+ newParams = value[memoryProxyPramsKey];
+ }
+
+ array[prop] = value;
+
+ if (oldValue === Undefined) {
+ const uid = params.uniqIDs[index];
+ if (newParams) {
+ memoryArray.patch[uid] = newParams.ID;
+ memory.addSliceProxyParent(newParams.ID, params.ID, '´' + uid);
+ } else {
+ memoryArray.patch[uid] = value;
+ }
+ params.map.set(value, uid);
+ } else {
+ const uniqIDs = params.uniqIDs;
+ const uid = uniqIDs[index];
+
+ // begin with remove previous
+
+ if (uid) {
+ const mapUID = params.map.get(oldValue);
+ if (mapUID === uid) {
+ params.map.delete(oldValue);
+ const otherIndex = array.indexOf(oldValue);
+ if (otherIndex !== -1) {
+ params.map.set(oldValue, uniqIDs[otherIndex]);
+ }
+ }
+ if (
+ params.previousSliceValues[uid] === removedItem ||
+ !(uid in params.previousSliceValues)
+ ) {
+ delete memoryArray.patch[uid];
+ } else {
+ memoryArray.patch[uid] = removedItem;
+ }
+
+ const oldParams =
+ oldValue && typeof oldValue === 'object' && oldValue[memoryProxyPramsKey];
+ if (oldParams) {
+ memory.deleteSliceProxyParent(oldParams.ID, params.ID, '´' + uid);
+ }
+ }
+
+ if (value === removedItem) {
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ return true;
+ }
+
+ // and then we add item
+
+ if (!uid && index > uniqIDs.length) {
+ // add fake undefined values (don't add undefined in array)
+ for (let k = uniqIDs.length; k < index; k++) {
+ const newUniqID = generateUid(params.sequences, uniqIDs[k - 1]);
+ uniqIDs.push(newUniqID);
+ memoryArray.patch[newUniqID] = Undefined;
+ }
+ }
+
+ const isEnd = index >= uniqIDs.length;
+ const nearest = isEnd ? undefined : uniqIDs[index];
+ const newUniqID = generateUid(params.sequences, nearest, isEnd);
+ uniqIDs[index] = newUniqID;
+
+ if (newParams) {
+ memory.addSliceProxyParent(newParams.ID, params.ID, '´' + newUniqID);
+ memoryArray.patch[newUniqID] = newParams.ID;
+ } else {
+ memoryArray.patch[newUniqID] = value;
+ }
+ if (!params.map.has(oldValue)) {
+ params.map.set(value, newUniqID);
+ }
+ }
+
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ return true;
+ },
+ deleteProperty(obj: object, prop: keyType): boolean {
+ // `removedItem` is a marker to notify that there was something here but
+ // it got removed
+ this.set(obj, prop, removedItem);
+ delete obj[prop];
+ return true;
+ },
+} as ProxyObjectHandler;
+
+export function _proxifyArray(array: T[]): T[] {
+ const proxyObject = _proxifyObject(array);
+ const proxy = new Proxy(proxyObject, proxyArrayHandler) as T[];
+ const params = proxyObject[memoryProxyPramsKey] as ArrayParams;
+ params.proxy = proxy;
+ params.linkCallback = linkVersionable;
+ params.MemoryType = MemoryTypeArray;
+ params.map = new Map();
+ params.uniqIDs = [];
+ params.sequences = [];
+ return proxy;
+}
+
+function unshift(params: ArrayParams, ...items: MemoryAllowedType[]): number {
+ for (let k = 0, len = items.length; k < len; k++) {
+ const item = items[k];
+ params.object.unshift(Undefined);
+ params.uniqIDs.unshift(generateUid(params.sequences, undefined));
+ this[0] = item;
+ }
+ return params.object.length;
+}
+function shift(params: ArrayParams): MemoryAllowedType {
+ const value = params.object[0];
+ this['0'] = removedItem;
+ params.object.shift();
+ params.uniqIDs.shift();
+ return value;
+}
+function pop(params: ArrayParams): MemoryAllowedType {
+ const lastIndex = params.object.length - 1;
+ const value = params.object[lastIndex];
+ this.length = lastIndex;
+ return value;
+}
+function splice(
+ params: ArrayParams,
+ index: number,
+ nb: number,
+ ...items: MemoryAllowedType[]
+): MemoryAllowedType[] {
+ const array = params.object;
+ const uniqIDs = params.uniqIDs;
+ const len = array.length;
+ if (nb === undefined) {
+ nb = len - index;
+ }
+ const value: MemoryAllowedType[] = new (array.constructor as typeof Array)();
+ if (nb > 0) {
+ for (let i = 0; i < nb; i++) {
+ value.push(array[i + index]);
+ }
+ for (let i = 0; i < nb; i++) {
+ this[(i + index).toString()] = removedItem;
+ }
+ array.splice(index, nb);
+ uniqIDs.splice(index, nb);
+ }
+ for (let key = 0, len = items.length; key < len; key++) {
+ const item = items[key];
+ const i = key + index;
+ array.splice(i, 0, Undefined);
+ const nearest = uniqIDs[i - 1];
+ uniqIDs.splice(i, 0, generateUid(params.sequences, nearest));
+ this[i] = item;
+ }
+ return value;
+}
+function forEach(
+ params: ArrayParams,
+ callback: (value: MemoryAllowedType, index: number, array: MemoryAllowedType[]) => void,
+): void {
+ const array = params.object;
+ for (let index = 0, len = array.length; index < len; index++) {
+ callback(array[index], index, this);
+ }
+}
+function map(
+ params: ArrayParams,
+ callback: (
+ value: MemoryAllowedType,
+ index: number,
+ array: MemoryAllowedType[],
+ ) => MemoryAllowedType,
+): MemoryAllowedType[] {
+ const result = [];
+ const array = params.object;
+ for (let index = 0, len = array.length; index < len; index++) {
+ result.push(callback(array[index], index, this));
+ }
+ return result;
+}
+function filter(
+ params: ArrayParams,
+ callback: (value: MemoryAllowedType, index: number, array: MemoryAllowedType[]) => boolean,
+): MemoryAllowedType[] {
+ const result = [];
+ const array = params.object;
+ for (let index = 0, len = array.length; index < len; index++) {
+ const value = array[index];
+ if (callback(value, index, this)) {
+ result.push(value);
+ }
+ }
+ return result;
+}
+function indexOf(params: ArrayParams, item: MemoryAllowedType): number {
+ return params.object.indexOf(item);
+}
+function includes(params: ArrayParams, item: MemoryAllowedType): boolean {
+ return params.object.includes(item);
+}
+function proxifySyncArray(): { compiled: uniqIDMemoryTypeValues; sequences: uniqID[] } {
+ // Synchronization function
+ // Most methods of the "proxy" will call this synchronization function, even
+ // if it is not yet linked to a memory !
+ const params = this as ArrayParams;
+ const memory = params.memory;
+ // empties the array
+ params.uniqIDs.length = 0;
+ params.object.length = 0;
+ // Clear props
+ const keys = Object.keys(params.object);
+ let key: string;
+ while ((key = keys.pop())) {
+ delete params.object[key];
+ }
+ const rawValues = memory.getSliceValue(params.ID) as MemoryArrayCompiledWithPatch;
+
+ if (!rawValues) {
+ return;
+ }
+ const values = Object.assign({}, rawValues.compiledValues, rawValues.newValues.patch);
+ const sequences = Object.keys(values);
+ sequences.sort();
+ proxifySyncArrayItems(memory, sequences, values, params.object, params.uniqIDs);
+ const props = Object.assign({}, rawValues.props, rawValues.newValues.props);
+ proxifySyncArrayItems(memory, Object.keys(props), props, params.object);
+
+ params.syncSlice = memory.getSlice() as Record;
+ params.previousSliceValues = rawValues.compiledValues;
+ params.sequences = sequences;
+
+ params.map.clear();
+ params.object.forEach((item: MemoryAllowedType, i: number) => {
+ params.map.set(item, params.uniqIDs[i]);
+ });
+}
+function proxifySyncArrayItems(
+ memory: MemoryWorker,
+ keys: string[],
+ values: Record,
+ array: MemoryAllowedType[],
+ uniqIDs?: string[],
+): void {
+ let index = 0;
+ for (let k = 0, len = keys.length; k < len; k++) {
+ const key = keys[k];
+ let value = values[key];
+ if (value === removedItem) {
+ continue;
+ }
+ if (value instanceof VersionableID) {
+ value = memory.getProxy(value);
+ }
+ if (uniqIDs) {
+ if (value !== Undefined) {
+ array[index] = value;
+ }
+ uniqIDs.push(key);
+ index++;
+ } else {
+ array[key] = value;
+ }
+ }
+}
+function linkVersionable(memory: MemoryWorker): void {
+ const params = this as ArrayParams;
+ params.memory = memory;
+ params.synchronize = proxifySyncArray;
+ const slice = memory.getSlice();
+ const array = params.object;
+ const keys = Object.keys(array);
+ const len = keys.length;
+ const ID = params.ID;
+ if (len === 0) {
+ memory.markDirty(ID);
+ return;
+ }
+ const memoryArray = (slice[ID as number] = new params.MemoryType() as MemoryTypeArray);
+ const props = memoryArray.props;
+ const patch = memoryArray.patch;
+ const uniqIDs = params.uniqIDs;
+ const sequences = params.sequences;
+ let arrayIndex = -1;
+ for (let k = 0; k < len; k++) {
+ const key = keys[k];
+ const index = +key;
+ const value = array[key];
+ const valueParams =
+ value !== null && typeof value === 'object' && value[memoryProxyPramsKey];
+
+ if (valueParams) {
+ memory.linkToMemory(value as object);
+ }
+ if (isNaN(index)) {
+ if (valueParams) {
+ props[key] = valueParams.ID;
+ memory.addSliceProxyParent(valueParams.ID, ID, key);
+ } else {
+ props[key] = value;
+ }
+ } else {
+ arrayIndex++;
+ while (arrayIndex < index) {
+ const newUniqID = generateUid(sequences, undefined, true);
+ uniqIDs[arrayIndex] = newUniqID;
+ patch[newUniqID] = Undefined;
+ arrayIndex++;
+ }
+ const newUniqID = generateUid(sequences, undefined, true);
+ uniqIDs[index] = newUniqID;
+ if (valueParams) {
+ patch[newUniqID] = valueParams.ID;
+ memory.addSliceProxyParent(valueParams.ID, ID, '´' + newUniqID);
+ } else {
+ patch[newUniqID] = value;
+ }
+ }
+ }
+ params.map.clear();
+ array.forEach((item: MemoryAllowedType, i: number) => {
+ if (!params.map.has(item)) {
+ params.map.set(item, uniqIDs[i]);
+ }
+ });
+ memory.markDirty(ID);
+}
+
+// IDs
+
+function allocUid(min: uniqSec, max: uniqSec): uniqSec {
+ const step = 4;
+ if (!min && !max) {
+ return [128];
+ }
+ min = min || [];
+ max = max || [];
+ const res = [];
+ let minSeq = 0;
+ let maxSeq = max[0];
+ for (let index = 0, len = Math.max(min.length, max.length); index < len; index++) {
+ minSeq = min[index] | 0;
+ maxSeq = index in max ? max[index] : 4096;
+ if (minSeq === 4095 && maxSeq === 4096) {
+ res.push(minSeq);
+ } else if (minSeq === maxSeq) {
+ res.push(minSeq);
+ } else if (minSeq === maxSeq - 1 && len > index - 1) {
+ res.push(minSeq);
+ } else {
+ break;
+ }
+ }
+ const diff = (maxSeq - minSeq) >> 1;
+ if (diff === 0) {
+ res.push(min.length ? 128 : 2048);
+ } else if (minSeq === 0) {
+ res.push(maxSeq - Math.min(diff, step));
+ } else {
+ res.push(minSeq + Math.min(diff, step));
+ }
+ return res;
+}
+function hexaToSeq(str: string): uniqSec {
+ const seq = [];
+ for (let k = 0, len = str.length; k < len; k += 3) {
+ seq.push(parseInt(str.slice(k, k + 3), 16));
+ }
+ return seq;
+}
+function SeqToHexa(seq: uniqSec): uniqID {
+ let str = '';
+ const len = seq.length;
+ for (let k = 0; k < len; k++) {
+ const n = seq[k];
+ if (n === 0) {
+ str += '000';
+ } else if (n < 16) {
+ str += '00' + n.toString(16);
+ } else if (n < 256) {
+ str += '0' + n.toString(16);
+ } else {
+ str += n.toString(16);
+ }
+ }
+ return str;
+}
+function generateUid(sortedUniqIDs: uniqID[], min: uniqID, isEnd?: boolean): uniqID {
+ let max: uniqID;
+ if (isEnd) {
+ min = sortedUniqIDs[sortedUniqIDs.length - 1];
+ } else if (min) {
+ max = sortedUniqIDs[sortedUniqIDs.indexOf(min) + 1];
+ } else {
+ max = sortedUniqIDs[0];
+ }
+ const minSeq = min && hexaToSeq(min);
+ const maxSeq = max && hexaToSeq(max);
+ const newUniqID = SeqToHexa(allocUid(minSeq, maxSeq));
+
+ if (isEnd) {
+ sortedUniqIDs.push(newUniqID);
+ } else {
+ const sortedIndex = min ? sortedUniqIDs.indexOf(min) : -1;
+ if (sortedIndex === -1) {
+ sortedUniqIDs.unshift(newUniqID);
+ } else {
+ sortedUniqIDs.splice(sortedIndex + 1, 0, newUniqID);
+ }
+ }
+ return newUniqID;
+}
+
+export class VersionableArray extends Array {
+ constructor(...items: T[]) {
+ super(...items);
+ return _proxifyArray(this);
+ }
+}
diff --git a/packages/core/src/Memory/VersionableObject.ts b/packages/core/src/Memory/VersionableObject.ts
new file mode 100644
index 000000000..bb8594420
--- /dev/null
+++ b/packages/core/src/Memory/VersionableObject.ts
@@ -0,0 +1,278 @@
+import {
+ MemoryAllowedType,
+ MemoryAllowedPrimitiveTypes,
+ MemoryTypeObject,
+ MemoryWorker,
+} from './Memory';
+import {
+ VersionableID,
+ _checkVersionable,
+ VersionableParams,
+ generateVersionableID,
+ _stackedProxify,
+} from './Versionable';
+
+import {
+ removedItem,
+ memoryProxyNotVersionableKey,
+ memoryProxyPramsKey,
+ FroozenError,
+ symbolVerify,
+} from './const';
+
+export type keyType = string | number | symbol;
+
+interface ObjectParams extends VersionableParams {
+ proxy: Record;
+ object: Record;
+ proto: GettersSetters;
+}
+
+export interface GettersSetters {
+ getters: Record MemoryAllowedType>;
+ setters: Record void>;
+}
+
+const classGettersSetters: WeakMap = new WeakMap();
+const rootPrototype = Object.getPrototypeOf({});
+export function getPrototypeGettersSetters(obj: object): GettersSetters {
+ let gettersSetters = classGettersSetters.get(obj.constructor);
+ if (gettersSetters) {
+ return gettersSetters;
+ }
+ gettersSetters = {
+ getters: {},
+ setters: {},
+ };
+ classGettersSetters.set(obj.constructor, gettersSetters);
+ obj = Object.getPrototypeOf(obj);
+ do {
+ const descs = Object.getOwnPropertyDescriptors(obj);
+ Object.keys(descs).forEach(propName => {
+ const desc = descs[propName];
+ if (!gettersSetters.getters[propName] && desc.get) {
+ gettersSetters.getters[propName] = desc.get as () => MemoryAllowedType;
+ }
+ if (!gettersSetters.setters[propName] && desc.set) {
+ gettersSetters.setters[propName] = desc.set as (value: MemoryAllowedType) => void;
+ }
+ });
+ } while ((obj = Object.getPrototypeOf(obj)) && obj !== rootPrototype);
+ return gettersSetters;
+}
+
+export interface ProxyObjectHandler {
+ get(obj: MemoryAllowedType, prop: keyType, proxy: object): MemoryAllowedType;
+ set(obj: MemoryAllowedType, prop: keyType, value: MemoryAllowedType, proxy: object): boolean;
+ has(obj: MemoryAllowedType, prop: keyType): boolean;
+ ownKeys(obj: MemoryAllowedType): keyType[];
+ getOwnPropertyDescriptor(obj: MemoryAllowedType, prop: string | symbol): PropertyDescriptor;
+ deleteProperty(obj: MemoryAllowedType, prop: string): boolean;
+}
+
+export const proxyObjectHandler = {
+ set(obj: object, prop: keyType, value: MemoryAllowedType, proxy: object): boolean {
+ // Object.assign might try to set the value of the paramsKey. We
+ // obviously don't want that. Let it think it succeeded (returning false
+ // will throw an error, which is not what we want here.)
+ if (prop === memoryProxyPramsKey) {
+ return true;
+ }
+ if (prop === symbolVerify) {
+ obj[symbolVerify] = value;
+ return true;
+ }
+
+ const params = obj[memoryProxyPramsKey] as ObjectParams;
+ const memory = params.memory;
+
+ if (!memory && value === removedItem) {
+ // "synchronize" the delete
+ delete obj[prop];
+ return true;
+ }
+
+ // if the property is a method of the class or prototype.
+ const protoMethod = params.proto.setters[prop as string];
+ if (protoMethod) {
+ protoMethod.call(proxy, value as MemoryAllowedPrimitiveTypes);
+ return true;
+ }
+
+ // if the property is a getter
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
+ if (desc?.set) {
+ desc.set.call(proxy, value as MemoryAllowedPrimitiveTypes);
+ return true;
+ }
+
+ _checkVersionable(value);
+
+ // if not linked to memory
+ if (!memory) {
+ obj[prop] = value as MemoryAllowedPrimitiveTypes;
+ return true;
+ }
+
+ if (memory.isFrozen()) {
+ throw new FroozenError();
+ }
+
+ const oldValue = obj[prop];
+ // The value is the same, or we are deleting a value that was not
+ // already there in the first place.
+ if (oldValue === value || (value === removedItem && !(prop in obj))) {
+ return true;
+ }
+
+ const slice = memory.getSlice();
+ let memoryObject = slice[params.ID as number] as MemoryTypeObject;
+ if (!memoryObject) {
+ slice[params.ID as number] = memoryObject = new params.MemoryType() as MemoryTypeObject;
+ }
+ const memoryObjectProps = memoryObject.props;
+
+ let memoryItem = value;
+ if (value !== null && typeof value === 'object' && !value[memoryProxyNotVersionableKey]) {
+ // if object, the stored value needs to be "converted" (see Set)
+ memory.linkToMemory(value as object);
+ const newParams = value && value[memoryProxyPramsKey];
+ memoryItem = newParams.ID;
+ memory.addSliceProxyParent(newParams.ID, params.ID, prop as string);
+ }
+
+ // if the old value was a versionable as well, sever its link to its parent
+ const oldParams = oldValue && typeof oldValue === 'object' && oldValue[memoryProxyPramsKey];
+ if (oldParams) {
+ memory.deleteSliceProxyParent(oldParams.ID, params.ID, prop as string);
+ }
+
+ if (value === removedItem) {
+ memoryObjectProps[prop as string] = removedItem; // notify that the deletion happened in this slice
+ delete obj[prop];
+ } else {
+ memoryObjectProps[prop as string] = memoryItem;
+ obj[prop] = value;
+ }
+
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ return true;
+ },
+ deleteProperty(obj: object, prop: keyType): boolean {
+ // `removedItem` is a marker to notify that there was something here but
+ // it got removed
+ this.set(obj, prop, removedItem);
+ delete obj[prop];
+ return true;
+ },
+} as ProxyObjectHandler;
+
+export function _proxifyObject(obj: object): object {
+ const proxy = new Proxy(obj, proxyObjectHandler) as object;
+
+ obj[memoryProxyPramsKey] = {
+ ID: generateVersionableID(),
+ linkCallback: linkVersionable,
+ synchronize: proxifySyncObject,
+ MemoryType: MemoryTypeObject,
+ verify: verify,
+ proxy: proxy,
+ object: obj,
+ proto: getPrototypeGettersSetters(obj),
+ } as ObjectParams;
+
+ const keys = Object.keys(obj);
+ for (let k = 0, len = keys.length; k < len; k++) {
+ const key = keys[k];
+ const value = obj[key];
+ _stackedProxify(value, newValue => {
+ if (newValue !== value) {
+ obj[key] = newValue;
+ }
+ });
+ }
+ return proxy;
+}
+
+function verify(proxy: MemoryAllowedType): boolean {
+ const params = this as ObjectParams;
+ const obj = params.object as object;
+ proxy[symbolVerify] = true;
+ const value = obj[symbolVerify] as boolean;
+ obj[symbolVerify] = false;
+ return value;
+}
+export function proxifySyncObject(): void {
+ // Synchronization function
+ // Most methods of the "proxy" will call this synchronization function, even
+ // if it is not yet linked to a memory !
+ const params = this as ObjectParams;
+ const memory = params.memory;
+ const memoryObject = memory.getSliceValue(params.ID) as MemoryTypeObject;
+ const memoryObjectProps = (memoryObject && memoryObject.props) || {};
+
+ // Clear keys that do not exist anymore
+ let keys = Object.keys(params.object);
+ let key: string;
+ while ((key = keys.pop())) {
+ // if the object is not present in this slice or it does not have this key
+ if (!(key in memoryObjectProps)) {
+ delete params.object[key];
+ }
+ }
+
+ if (!memoryObject) {
+ return;
+ }
+
+ // Update values according to what is stored
+ keys = Object.keys(memoryObjectProps);
+ while ((key = keys.pop())) {
+ let value = memoryObjectProps[key];
+ if (value instanceof VersionableID) {
+ // Convert proxy references to actual proxy
+ value = memory.getProxy(value);
+ }
+ params.object[key] = value;
+ }
+}
+function linkVersionable(memory: MemoryWorker): void {
+ const params = this as ObjectParams;
+ params.memory = memory;
+ const slice = params.memory.getSlice();
+ const obj = params.object;
+ const ID = params.ID;
+ const keys = Object.keys(obj);
+ if (keys.length) {
+ const memoryObjectProps = Object.assign({}, obj);
+ let key: string;
+ while ((key = keys.pop())) {
+ const value = obj[key];
+ // Some of the values in the original object may be versionable and
+ // need to be converted
+ const valueParams =
+ value !== null && typeof value === 'object' && value[memoryProxyPramsKey];
+ if (valueParams) {
+ memory.linkToMemory(value as object);
+ memory.addSliceProxyParent(valueParams.ID, ID, key);
+ memoryObjectProps[key] = valueParams.ID;
+ }
+ }
+ const memoryObject = new params.MemoryType() as MemoryTypeObject;
+ memoryObject.props = memoryObjectProps;
+ slice[ID as number] = memoryObject; // store the "pure" value in memory
+ }
+ memory.markDirty(ID);
+}
+export class VersionableObject {
+ constructor(obj?: Record | object) {
+ if (obj) {
+ const keys = Object.keys(obj);
+ let key: string;
+ while ((key = keys.pop())) {
+ this[key] = obj[key];
+ }
+ }
+ return _proxifyObject(this);
+ }
+}
diff --git a/packages/core/src/Memory/VersionableSet.ts b/packages/core/src/Memory/VersionableSet.ts
new file mode 100644
index 000000000..1484e7284
--- /dev/null
+++ b/packages/core/src/Memory/VersionableSet.ts
@@ -0,0 +1,293 @@
+import {
+ MemoryAllowedType,
+ MemoryAllowedPrimitiveTypes,
+ MemoryWorker,
+ MemoryTypeSet,
+ MemoryCompiledSet,
+} from './Memory';
+import {
+ VersionableID,
+ _checkVersionable,
+ VersionableParams,
+ generateVersionableID,
+ _stackedProxify,
+} from './Versionable';
+
+import { memoryProxyNotVersionableKey, memoryProxyPramsKey, FroozenError } from './const';
+
+interface SetParams extends VersionableParams {
+ size: number;
+ object: Set;
+ proxy: VersionableSet;
+}
+
+// People can override the set methods. They will be called from the proxy, but
+// sometimes we want to call the true original methods, not the override of the
+// user. This is how we do it.
+const genericSet = new Set() as Set;
+const genericSetPrototype = Set.prototype;
+
+function setPrototype(
+ proxy: VersionableSet,
+ obj: Set,
+): void {
+ do {
+ // This function loops on the prototypes of the object. This is what
+ // stops it.
+ // TODO refactor: while !== genericSetPrototype
+ if (obj === genericSetPrototype) {
+ break;
+ }
+ const op = Object.getOwnPropertyNames(obj);
+ for (let i = 0; i < op.length; i++) {
+ const prop = op[i]; // propName
+ if (!proxy[prop]) {
+ proxy[prop] = obj[prop];
+ }
+ }
+ } while ((obj = Object.getPrototypeOf(obj)));
+}
+
+// eslint-disable-next-line @typescript-eslint/no-empty-function
+function nothing(): void {}
+
+export class VersionableSet extends Set {
+ [memoryProxyPramsKey]: SetParams;
+ constructor(params?: Set | T[]) {
+ super();
+ let set: Set; // original set (won't be synced, it's just for its method ex: overrides)
+ let size = 0;
+ if (!params) {
+ set = genericSet as Set;
+ } else if (params instanceof Array) {
+ set = genericSet as Set;
+ params.forEach(value => {
+ size++;
+ _stackedProxify(value, newValue => {
+ set.add.call(this, newValue);
+ });
+ });
+ } else {
+ if (params instanceof VersionableSet) {
+ set = params[memoryProxyPramsKey].object as Set;
+ } else {
+ set = params as Set;
+ }
+ params.forEach(value => {
+ size++;
+ _stackedProxify(value, newValue => {
+ set.add.call(this, newValue);
+ });
+ });
+ setPrototype(this, set);
+ }
+
+ this[memoryProxyPramsKey] = {
+ ID: generateVersionableID(),
+ linkCallback: linkVersionable,
+ synchronize: nothing,
+ MemoryType: MemoryTypeSet,
+ verify: (proxy: MemoryAllowedType): boolean => proxy === this,
+ size: size,
+ object: set,
+ proxy: this,
+ };
+ }
+
+ add(item: T): this {
+ // For Set specifically, this line will never actually *proxify* per se.
+ // It will either work if the item is already proxified, or throw an
+ // error if it is not.
+ _checkVersionable(item);
+
+ const params = this[memoryProxyPramsKey];
+ const memory = params.memory;
+ if (memory && memory.isFrozen()) {
+ throw new FroozenError();
+ }
+
+ const check = this.has(item);
+ if (!check) {
+ params.object.add.call(this, item);
+ }
+
+ if (check || !memory) {
+ // Nothing changed. Either the item was already there, or we don't
+ // care because we are not linked to memory.
+ return this;
+ }
+
+ let memoryItem = item as MemoryAllowedPrimitiveTypes;
+ if (item !== null && typeof item === 'object' && !item[memoryProxyNotVersionableKey]) {
+ // The item is versionable, great, but it is not versioned yet !
+ // This call versions it into the memory.
+ memory.linkToMemory(item as object);
+ const itemParams = item[memoryProxyPramsKey];
+ memoryItem = itemParams.ID;
+ memory.addSliceProxyParent(itemParams.ID, params.ID, undefined);
+ }
+
+ // Get current slice.
+ const slice = memory.getSlice();
+ let memorySet = slice[params.ID as number] as MemoryTypeSet; // read the pure value stored in memory
+ if (!memorySet) {
+ slice[params.ID as number] = memorySet = new MemoryTypeSet();
+ // Mark the set as being modified in this slice (not necesarilly "dirty")
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ }
+ // Update the stored changes for this slice
+ memorySet.add.add(memoryItem);
+ memorySet.delete.delete(memoryItem);
+
+ return this;
+ }
+ delete(item: T): boolean {
+ const params = this[memoryProxyPramsKey];
+ const memory = params.memory;
+ if (memory && memory.isFrozen()) {
+ throw new FroozenError();
+ }
+
+ const check = this.has(item);
+ if (check) {
+ params.object.delete.call(this, item);
+ }
+
+ if (!check || !memory) {
+ return check;
+ }
+
+ let memoryItem = item as MemoryAllowedPrimitiveTypes;
+ const itemParams = item && typeof item === 'object' && item[memoryProxyPramsKey];
+ if (itemParams) {
+ memoryItem = itemParams.ID as VersionableID;
+ memory.deleteSliceProxyParent(itemParams.ID, params.ID, undefined);
+ }
+
+ const slice = memory.getSlice();
+ let memorySet = slice[params.ID as number] as MemoryTypeSet;
+ if (!memorySet) {
+ slice[params.ID as number] = memorySet = new MemoryTypeSet();
+ }
+ memorySet.delete.add(memoryItem);
+ memorySet.add.delete(memoryItem);
+
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+
+ return check;
+ }
+ clear(): this {
+ const params = this[memoryProxyPramsKey];
+ const memory = params.memory;
+ if (memory && memory.isFrozen()) {
+ throw new FroozenError();
+ }
+ if (this.size === 0) {
+ return this;
+ }
+
+ if (!memory) {
+ params.object.clear.call(this);
+ return this;
+ }
+
+ const slice = memory.getSlice();
+ let memorySet = slice[params.ID as number] as MemoryTypeSet;
+ if (!memorySet) {
+ slice[params.ID as number] = memorySet = new params.MemoryType() as MemoryTypeSet;
+ }
+
+ params.object.forEach.call(this, (item: T) => {
+ const itemParams = item && typeof item === 'object' && item[memoryProxyPramsKey];
+ if (itemParams) {
+ item = itemParams.ID;
+ memory.deleteSliceProxyParent(itemParams.ID, params.ID, undefined);
+ }
+ memorySet.delete.add(item);
+ memorySet.add.delete(item);
+ });
+ params.object.clear.call(this);
+
+ memory.markDirty(params.ID); // mark the cache as invalid when change the slide memory
+ return this;
+ }
+ has(item: T): boolean {
+ const params = this[memoryProxyPramsKey];
+ return params.object.has.call(this, item);
+ }
+ values(): IterableIterator {
+ const params = this[memoryProxyPramsKey];
+ return params.object.values.call(this);
+ }
+ keys(): IterableIterator {
+ const params = this[memoryProxyPramsKey];
+ return params.object.keys.call(this);
+ }
+ forEach(callback: (value: T, value2: T, set: Set) => void): void {
+ const params = this[memoryProxyPramsKey];
+ return params.object.forEach.call(this, callback);
+ }
+ entries(): IterableIterator<[T, T]> {
+ const params = this[memoryProxyPramsKey];
+ return params.object.entries.call(this);
+ }
+}
+
+function proxifySyncSet(): void {
+ // Synchronization function
+ // Most methods of the "proxy" will call this synchronization function, even
+ // if it is not yet linked to a memory !
+ const params = this as SetParams;
+ const memory = params.memory;
+ const object = params.object;
+ const proxy = params.proxy;
+ // get current object state in memory
+ const memorySet = memory.getSliceValue(params.ID) as MemoryCompiledSet;
+
+ // reset all keys (+ explanation of best/worst case scenario)
+ object.clear.call(proxy);
+ if (!memorySet) {
+ return;
+ }
+ // Update values according to what is stored
+ memorySet.forEach(item => {
+ if (item instanceof VersionableID) {
+ item = memory.getProxy(item);
+ }
+ object.add.call(proxy, item);
+ });
+}
+
+// This will be set on the versionable params object and called with the params
+// as the value of `this`. It is created here so that it is created only once !
+function linkVersionable(memory: MemoryWorker): void {
+ const params = this as SetParams;
+ params.memory = memory;
+ params.synchronize = proxifySyncSet;
+ memory.markDirty(params.ID);
+ if (!params.proxy.size) {
+ return;
+ }
+ const slice = memory.getSlice();
+
+ const memorySet = new params.MemoryType() as MemoryTypeSet;
+ slice[params.ID as number] = memorySet; // store the "pure" value in memory
+ params.object.forEach.call(params.proxy, (value: MemoryAllowedType): void => {
+ const valueParams =
+ value !== null && typeof value === 'object' && value[memoryProxyPramsKey];
+ if (valueParams) {
+ // If object is versionable then link it to memory as well
+ memory.linkToMemory(value as object);
+ memory.addSliceProxyParent(valueParams.ID, params.ID, undefined);
+ memorySet.add.add(valueParams.ID);
+ } else {
+ memorySet.add.add(value);
+ }
+ });
+}
+
+export function _proxifySet(set: Set): VersionableSet {
+ const versionableSet = new VersionableSet(set);
+ set[memoryProxyPramsKey] = versionableSet[memoryProxyPramsKey];
+ return versionableSet;
+}
diff --git a/packages/core/src/Memory/const.ts b/packages/core/src/Memory/const.ts
new file mode 100644
index 000000000..01623a86b
--- /dev/null
+++ b/packages/core/src/Memory/const.ts
@@ -0,0 +1,43 @@
+import { CustomError } from '../../../utils/src/errors';
+
+export const memoryProxyNotVersionableKey = Symbol('jabberwockMemoryNotVersionable');
+export const memoryProxyPramsKey = Symbol('jabberwockMemoryParams');
+export const removedItem = Symbol('jabberwockMemoryRemovedItem');
+export const symbolVerify = Symbol('jabberwockMemoryVerify');
+
+/**
+ * Creates an instance representing an error that occurs when theyr are any
+ * error in the memory feature or with the integration of the memory.
+ */
+export class MemoryError extends CustomError {
+ constructor(message?: string, ...params) {
+ super(message, ...params);
+ this.message = message || 'Jabberwok error in memory feature';
+ }
+}
+export class NotVersionableError extends MemoryError {
+ constructor() {
+ super();
+ this.message =
+ 'You can only link to the memory the instance of VersionableObject, VersionableArray or VersionableSet.' +
+ "\nIf that's not possible, then you can also use makeVersionable method on your custom object." +
+ '\nIf you do not want to make versionable this object, indicate it using MarkNotVersionable method' +
+ '\nPlease read the Jabberwock documentation.';
+ }
+}
+export class VersionableAllreadyVersionableError extends MemoryError {
+ constructor() {
+ super();
+ this.message =
+ 'This object was already update and a proxy was create to be versionable.' +
+ '\nPlease use it instead of the source object.';
+ }
+}
+export class FroozenError extends MemoryError {
+ constructor() {
+ super();
+ this.message =
+ 'This memory is froozen and immutable.' +
+ '\nYou can not update a memory version who content memory dependencies';
+ }
+}
diff --git a/packages/core/src/Memory/test/memory.perf.ts b/packages/core/src/Memory/test/memory.perf.ts
new file mode 100644
index 000000000..8cf628cbd
--- /dev/null
+++ b/packages/core/src/Memory/test/memory.perf.ts
@@ -0,0 +1,458 @@
+/* eslint-disable max-nested-callbacks */
+import { expect } from 'chai';
+
+import { Memory } from '../Memory';
+import { makeVersionable } from '../Versionable';
+import { VersionableArray } from '../VersionableArray';
+import { VersionableObject } from '../VersionableObject';
+
+let ID = 0;
+let Text = 0;
+
+class Node extends VersionableObject {
+ ID: number;
+ parent: Node;
+}
+
+class TestCharNode extends Node {
+ bold: boolean;
+ char: string;
+ constructor(char: string) {
+ super();
+ Text++;
+ this.ID = Text;
+ this.bold = true;
+ this.char = char;
+ }
+ display(): string {
+ return this.char;
+ }
+}
+class TestNode extends Node {
+ name: string;
+ attr: Record;
+ children: Node[];
+ tagName: string;
+ constructor() {
+ super();
+ ID++;
+ this.ID = ID;
+ this.name = '' + ID;
+ this.children = new VersionableArray() as Node[];
+ this.attr = makeVersionable({
+ b: new Set(['p', 'o' + ID]),
+ c: {
+ 'x': ID % 2 ? 'y' : 'z',
+ 'A': 'AAA',
+ },
+ }) as Record;
+ }
+
+ display(withSpace = false, level = 0): string {
+ const space = Array(2 * level).join(' ');
+ let str = (withSpace ? space : '') + this.name;
+ const add = (value: string, key: string): string => (str += key + ':' + value + ';');
+ for (const key in this.attr) {
+ if (key === 'b') {
+ const m = [];
+ (this.attr[key] as Set).forEach(value => m.push(value));
+ m.join(';');
+ str += ' b<' + m.join(';') + '>';
+ } else {
+ str += ' c[';
+ const style = this.attr[key];
+ Object.keys(style).forEach(key => add(style[key], key));
+ str += ']';
+ }
+ }
+ level++;
+ if (this.children.length) {
+ str += '{';
+ if (withSpace) {
+ str += '\n';
+ }
+ const a = [];
+ this.children.forEach(child => a.push((child as TestNode).display(withSpace, level)));
+ str += a.join('|');
+ if (withSpace) {
+ str += '\n' + space;
+ }
+ str += '}';
+ }
+ return str;
+ }
+}
+
+function addChildren(
+ memory: Memory,
+ root: TestNode,
+ deep: number,
+ num: number,
+ textLength?: number,
+): void {
+ const children = [];
+ for (let k = 0; k < num; k++) {
+ const child = new TestNode();
+ if (deep > 0) {
+ addChildren(memory, child, deep - 1, num, textLength);
+ } else if (textLength) {
+ const texts = [];
+ for (let i = 0; i < textLength; i++) {
+ const child = new TestCharNode('a');
+ texts.push(child);
+ }
+ child.children.push(...texts);
+ }
+ memory.attach(child);
+ children.push(child);
+ }
+ root.children.push(...children);
+}
+function simulateAddSection(memory: Memory, root: TestNode): TestNode {
+ const section = new TestNode();
+ root.children.push(section);
+ addChildren(memory, section, 2, 3, 30);
+ return section;
+}
+function read(node: Node): void {
+ if (node instanceof TestNode) {
+ // eslint-disable-next-line no-unused-expressions
+ node.name;
+ (node.attr.b as Set).has('p');
+ // eslint-disable-next-line no-unused-expressions
+ node.attr.c;
+ // eslint-disable-next-line no-unused-expressions
+ (node.attr.c as Record).x;
+ // eslint-disable-next-line no-unused-expressions
+ node.children[0];
+ } else if (node instanceof TestCharNode) {
+ // eslint-disable-next-line no-unused-expressions
+ node.char;
+ }
+}
+function readAll(node: Node, nodes?: Node[]): Node[] {
+ nodes = nodes || [];
+ nodes.push(node);
+ read(node);
+ const children = (node as TestNode).children;
+ if (children) {
+ for (let k = 0, len = children.length; k < len; k++) {
+ const node = children[k];
+ readAll(node as Node, nodes);
+ }
+ }
+ return nodes;
+}
+
+describe('test performances', () => {
+ describe('core', () => {
+ describe('state', () => {
+ describe('Memory', () => {
+ let memory: Memory;
+ let array: VersionableArray;
+ before(() => {
+ memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ array = new VersionableArray();
+ });
+ it('Create and Link 100 arrays and add inside 1000 items', () => {
+ const d = Date.now();
+ for (let k = 0; k < 10; k++) {
+ array = new VersionableArray();
+ memory.attach(array);
+ for (let k = 0; k < 1000; k++) {
+ array.push(k);
+ }
+ }
+ const dt = Date.now() - d;
+ expect(dt).to.lessThan(150);
+ });
+ it('array forEach (with length 1000)', () => {
+ const d = Date.now();
+ const fn = function emptyCallbackForTest(): void {
+ return;
+ };
+ for (let k = 0; k < 1000; k++) {
+ array.forEach(fn);
+ }
+ const dt = Date.now() - d;
+ expect(dt).to.lessThan(10, 'Time to use 1000 * forEach');
+ });
+ it('array indexOf (with length 1000)', () => {
+ const d = Date.now();
+ for (let k = 0; k < 1000; k++) {
+ array.indexOf(k);
+ }
+ const dt = Date.now() - d;
+ expect(dt).to.lessThan(10, 'Time to use 1000 * indexOf');
+ });
+ });
+ describe('versionableNode', () => {
+ describe('children, parent, attributes', () => {
+ ID = 0;
+ const memory = new Memory();
+
+ memory.create('1');
+ memory.switchTo('1');
+ const root = new TestNode();
+ memory.attach(root);
+
+ it('create and read nodes', () => {
+ addChildren(memory, root, 0, 1);
+
+ expect(root.display()).to.equal(
+ '1 b c[x:y;A:AAA;]{2 b
c[x:z;A:AAA;]}',
+ );
+
+ memory.create('1-1');
+ memory.create('1-2');
+
+ memory.switchTo('1-1');
+ addChildren(memory, root, 1, 2);
+ expect(root.display()).to.equal(
+ '1 b
c[x:y;A:AAA;]{2 b
c[x:z;A:AAA;]|3 b
c[x:y;A:AAA;]{4 b
c[x:z;A:AAA;]|5 b
c[x:y;A:AAA;]}|6 b
c[x:z;A:AAA;]{7 b
c[x:y;A:AAA;]|8 b
c[x:z;A:AAA;]}}',
+ );
+
+ memory.switchTo('1-2');
+ addChildren(memory, root, 0, 2);
+ expect(root.display()).to.equal(
+ '1 b
c[x:y;A:AAA;]{2 b
c[x:z;A:AAA;]|9 b
c[x:y;A:AAA;]|10 b
c[x:z;A:AAA;]}',
+ );
+
+ memory.switchTo('1-1');
+ memory.create('1-1-55');
+ memory.switchTo('1-1-55');
+
+ const child = root.children[1] as TestNode;
+ addChildren(memory, child, 0, 5);
+ let nb = 0;
+ Object.keys(memory._slices['1-1-55'].data).forEach((key: string) => {
+ if (!isNaN(+key)) {
+ nb++;
+ }
+ });
+ expect(nb).to.equal(21); // should create 5 nodes (5 nodes * 4 (node + attr + b + c) (not the empty children) + 1 children list)
+ memory.create('1-1-55-1');
+ memory.switchTo('1-1-55-1');
+ child.children.splice(1, 3);
+ const slice = memory._slices['1-1-55-1'].data;
+ expect(Object.keys(slice).length).to.equal(1); // one patch
+ });
+ });
+ describe('Performance (double time to have an error)', () => {
+ ID = 0;
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ const root = new TestNode();
+ memory.attach(root);
+
+ it('Should write 100 chars in maximum 100ms', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ const root = new TestNode();
+ memory.attach(root);
+
+ const section = simulateAddSection(memory, root);
+ const child = (section.children[0] as TestNode).children[1] as TestNode;
+
+ const d = Date.now();
+ for (let k = 0; k < 100; k++) {
+ memory.create('test-' + k);
+ memory.switchTo('test-' + k);
+ const children = child.children;
+ // add char
+ children.push(new TestCharNode(k.toString()));
+ // simulate read new text for redraw
+ for (let k = 0, len = children.length; k < len; k++) {
+ const node = children[k];
+ if (node instanceof TestCharNode) {
+ // eslint-disable-next-line no-unused-expressions
+ node.char;
+ }
+ }
+ }
+ const dt = Date.now() - d;
+
+ expect(dt).to.lessThan(100);
+ });
+ function testAddAndRead(memory: Memory, root: TestNode, nb: number): number {
+ for (let k = 0; k < nb; k++) {
+ memory.create('1-t-' + k);
+ memory.switchTo('1-t-' + k);
+ }
+
+ memory.create('1-t');
+ memory.switchTo('1-t');
+
+ const section = simulateAddSection(memory, root);
+
+ memory.switchTo('test');
+ memory.switchTo('1-t');
+
+ const d = Date.now();
+ const nodes = readAll(section);
+ const dt = Date.now() - d;
+
+ let nodesNb = 0;
+ let chars = 0;
+ for (let k = 0, len = nodes.length; k < len; k++) {
+ const node = nodes[k];
+ if (node instanceof TestNode) {
+ nodesNb++;
+ } else {
+ chars++;
+ }
+ }
+
+ console.info(
+ 'Time to load ' +
+ nodesNb +
+ ' nodes and ' +
+ chars +
+ ' chars on ' +
+ nb +
+ ' slices',
+ dt,
+ );
+
+ const d2 = Date.now();
+ readAll(section);
+ const dt2 = Date.now() - d2;
+ console.info(
+ 'Time to re-load ' +
+ nodesNb +
+ ' nodes and ' +
+ chars +
+ ' chars on ' +
+ nb +
+ ' slices',
+ dt2,
+ );
+
+ return dt;
+ }
+ it('Should create and nodes with 10 slices in minimum time', () => {
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+ const dt = testAddAndRead(memory, root, 10);
+ expect(dt).to.lessThan(50);
+ });
+ it('Should remove 10 slices in minimum time', () => {
+ const d = Date.now();
+ memory.switchTo('1');
+ memory.remove('test');
+ const dt = Date.now() - d;
+ expect(dt).to.lessThan(6);
+ });
+ it('Should create and nodes with 100 slices in maximum 30ms', () => {
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+ const dt = testAddAndRead(memory, root, 100);
+ expect(dt).to.lessThan(50);
+ });
+ it('Should remove 100 slices in minimum time', () => {
+ const d = Date.now();
+ memory.switchTo('1');
+ memory.remove('test');
+ const dt = Date.now() - d;
+ expect(dt).to.lessThan(6);
+ });
+ it('Should read nodes with 1.000 slices in maximum 30ms', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ const root = new TestNode();
+ memory.attach(root);
+ const dt = testAddAndRead(memory, root, 1000);
+ expect(dt).to.lessThan(50);
+ });
+ it('Should create a lot of nodes in minimum time', () => {
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('1-3');
+ memory.switchTo('1-3');
+
+ const d = Date.now();
+ const nodeInti = ID;
+ const textInti = Text;
+ for (let k = 0; k < 30; k++) {
+ simulateAddSection(memory, root);
+ }
+ const dt = Date.now() - d;
+
+ console.info(
+ 'Time to create ' +
+ (ID - nodeInti) +
+ ' nodes and ' +
+ (Text - textInti) +
+ ' chars',
+ dt,
+ );
+ expect(dt).to.lessThan(1000); // perf to create node
+ });
+ it('Should switch slice in minimum time', () => {
+ const d = Date.now();
+ memory.switchTo('test');
+ memory.create('1-2');
+ memory.switchTo('1-3');
+ memory.create('1-3-1');
+ memory.switchTo('1-2');
+ memory.switchTo('1-3-1');
+ const dt = Date.now() - d;
+ console.info('Time to switch memory', dt);
+ expect(dt).to.lessThan(50);
+ });
+ it('Should read the nodes the first time in minimum time', () => {
+ memory.switchTo('1-3-1');
+ const d = Date.now();
+ readAll(root);
+ const dt = Date.now() - d;
+ console.info('Time to load nodes', dt);
+ expect(dt).to.lessThan(250);
+ });
+ it('Should read the nodes in minimum time', () => {
+ const times = [];
+
+ const d = Date.now();
+ const nodes = readAll(root);
+ const dt = Date.now() - d;
+ times.push(dt);
+ console.info('Time to re-load nodes', dt);
+
+ for (let k = 0; k < 5; k++) {
+ const d = Date.now();
+ // nodes.forEach(read);
+ readAll(root);
+ const dt = Date.now() - d;
+ times.push(dt);
+ console.info('Time to re-load nodes', dt);
+ }
+
+ let nodesNb = 0;
+ let chars = 0;
+ nodes.forEach(node => {
+ if (node instanceof TestNode) {
+ nodesNb++;
+ } else {
+ chars++;
+ }
+ });
+ const average = Math.round(times.reduce((a, b) => a + b) / times.length);
+ console.info(
+ 're-load ' + nodesNb + ' nodes and ' + chars + ' chars time average',
+ average,
+ );
+ expect(average).to.lessThan(90);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/core/src/Memory/test/memory.test.ts b/packages/core/src/Memory/test/memory.test.ts
new file mode 100644
index 000000000..501ea7dfe
--- /dev/null
+++ b/packages/core/src/Memory/test/memory.test.ts
@@ -0,0 +1,2463 @@
+/* eslint-disable max-nested-callbacks */
+import { expect } from 'chai';
+import { Memory, markAsDiffRoot } from '../Memory';
+import { VersionableObject } from '../VersionableObject';
+import { VersionableArray } from '../VersionableArray';
+import { VersionableSet } from '../VersionableSet';
+import { makeVersionable, markNotVersionable, proxifyVersionable } from '../Versionable';
+import { memoryProxyPramsKey } from '../const';
+
+describe('core', () => {
+ describe('state', () => {
+ describe('memory', () => {
+ describe('throw', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+
+ it('if try create a slice twice', () => {
+ expect((): Memory => memory.create('1')).to.throw('already exists');
+ });
+ it('if try remove a slice before switch on an other', () => {
+ memory.create('rem-2');
+ memory.switchTo('rem-2');
+ memory.create('rem-3');
+ memory.switchTo('rem-3');
+ expect((): Memory => memory.remove('rem-2')).to.throw('switch');
+ });
+ it('if try remove the original slice', () => {
+ expect((): Memory => memory.remove('')).to.throw('original');
+ });
+ it('if try switch on a undefined slice', () => {
+ expect((): Memory => memory.switchTo('2')).to.throw('must create');
+ });
+ it('if try to link a non versionable object', () => {
+ expect((): void => {
+ memory.attach({
+ test: 1,
+ });
+ }).to.throw('VersionableObject', 'object');
+ expect((): void => {
+ memory.attach([1]);
+ }).to.throw('VersionableObject', 'array');
+ expect((): void => {
+ memory.attach(new Set([1]));
+ }).to.throw('VersionableObject', 'set');
+ });
+ it('if try to link a non versionable object from proxy Object.assing', () => {
+ const versionable = makeVersionable({ test: 1 });
+ const obj = Object.assign({}, versionable);
+ expect((): void => {
+ memory.attach(obj);
+ }).to.throw('VersionableObject');
+ });
+ it('if try to add as attribute a non versionable object from proxy Object.assing', () => {
+ const versionable = makeVersionable({ test: 1 });
+ const obj = Object.assign({}, versionable);
+ const ref = makeVersionable({ toto: undefined });
+ expect((): void => {
+ ref.toto = obj;
+ }).to.throw('VersionableObject');
+ });
+ it('if try to link an object and not the proxy of versionable', () => {
+ const obj = { test: 1 };
+ makeVersionable(obj);
+ expect((): void => {
+ memory.attach(obj);
+ }).to.throw('already');
+ });
+ it('if try to add as attribute an object and not the proxy of versionable', () => {
+ const obj = { test: 1 };
+ const ref = makeVersionable({ toto: undefined });
+ makeVersionable(obj);
+ expect((): void => {
+ ref.toto = obj;
+ }).to.throw('already');
+ });
+ it('if try to link a versionable object to 2 memories', () => {
+ const memoryTest = new Memory();
+ memoryTest.create('1');
+ memoryTest.switchTo('1');
+ const obj = new VersionableObject();
+ memoryTest.attach(obj);
+ const array = new VersionableArray();
+ memoryTest.attach(array);
+ const set = new VersionableSet();
+ memoryTest.attach(set);
+
+ expect((): void => {
+ memory.attach(obj);
+ }).to.throw('other memory', 'object');
+ expect((): void => {
+ memory.attach(array);
+ }).to.throw('other memory', 'array');
+ expect((): void => {
+ memory.attach(set);
+ }).to.throw('other memory', 'set');
+ });
+ it('if try to link a versionable from an other memory in versionable', () => {
+ const memoryTest = new Memory();
+ memoryTest.create('1');
+ memoryTest.switchTo('1');
+ const root = new VersionableObject();
+ memory.attach(root);
+ const obj = new VersionableObject();
+ memoryTest.attach(obj);
+ const array = new VersionableArray();
+ memoryTest.attach(array);
+ const set = new VersionableSet();
+ memoryTest.attach(set);
+
+ expect((): void => {
+ root['x+y'] = obj;
+ }).to.throw('other memory', 'object');
+ expect((): void => {
+ root['x+y'] = array;
+ }).to.throw('other memory', 'array');
+ expect((): void => {
+ root['x+y'] = set;
+ }).to.throw('other memory', 'set');
+ });
+ it('if try to makeVersionable on a object already versionable with the old ref', () => {
+ const obj = {};
+ makeVersionable(obj);
+ expect((): void => {
+ makeVersionable(obj);
+ }).to.throw('proxy', 'object');
+ const array = [];
+ makeVersionable(array);
+ expect((): void => {
+ makeVersionable(array);
+ }).to.throw('proxy', 'array');
+ const set = new Set();
+ makeVersionable(set);
+ expect((): void => {
+ makeVersionable(set);
+ }).to.throw('proxy', 'set');
+ });
+ it('if try to link an object in versionable', () => {
+ const obj = new VersionableArray();
+ expect((): void => {
+ obj['t+h'] = {};
+ }).to.throw('VersionableObject', 'object');
+
+ const array = new VersionableArray();
+ expect((): void => {
+ array.push({});
+ }).to.throw('VersionableObject', 'array');
+
+ const set = new VersionableSet();
+ expect((): void => {
+ set.add({});
+ }).to.throw('VersionableObject', 'set');
+ });
+ it('if try to link an object in custom class constructor', () => {
+ class SuperObject extends VersionableObject {
+ obj: object;
+ constructor() {
+ super();
+ this.obj = {};
+ }
+ }
+ expect((): SuperObject => new SuperObject()).to.throw('VersionableObject');
+ });
+ it('if try to link an object in custom class', () => {
+ class SuperObject extends VersionableObject {
+ obj: object;
+ myMethod(): void {
+ this.obj = {};
+ }
+ }
+ const instance = new SuperObject();
+ expect((): void => {
+ instance.myMethod();
+ }).to.throw('VersionableObject');
+ });
+ it('if try to edit attribute in a froozen slice', () => {
+ const obj = new VersionableObject();
+ memory.attach(obj);
+ memory.switchTo('1');
+ memory.create('1-1');
+ memory.switchTo('1');
+ expect((): number => (obj['H' + 1] = 4)).to.throw('can not update', 'object');
+ memory.remove('1-1');
+ });
+ it('if try to edit array in a froozen slice', () => {
+ const memory = new Memory();
+ const array = new VersionableArray();
+ memory.attach(array);
+ memory.create('1');
+ memory.switchTo('1');
+ memory.create('2');
+ expect((): number => array.push(4)).to.throw('can not update', 'array');
+ expect((): number => (array[1] = 3)).to.throw('can not update', 'array');
+ memory.switchTo('1');
+ memory.remove('2');
+ });
+ it('if try to edit set in a froozen slice', () => {
+ const memory = new Memory();
+ const set = new VersionableSet();
+ memory.attach(set);
+ memory.create('1');
+ memory.switchTo('1');
+ memory.create('1-1');
+ memory.switchTo('1');
+ expect((): VersionableSet => set.add(4)).to.throw('can not update', 'add');
+ expect((): boolean => set.delete(4)).to.throw('can not update', 'delete');
+ expect((): VersionableSet => set.clear()).to.throw('can not update', 'clear');
+ memory.remove('1-1');
+ });
+ it('if try to compress/snapshot with wrong slice', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-0');
+ memory.switchTo('test-0');
+ memory.create('test-1');
+ memory.switchTo('test-1');
+ memory.create('test-2');
+ memory.switchTo('test-2');
+ expect((): void => memory.snapshot('test-2', 'test', 'snap')).to.throw('merge');
+ });
+ });
+
+ describe('create versionable', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+
+ it('link a object marked as not versionable', () => {
+ const obj = new VersionableObject();
+ const context = {};
+ markNotVersionable(context);
+ memory.attach(obj);
+ // do not throw
+ obj['g+k'] = context;
+ });
+ it('makeVersionable an object after Object.assing by a versionable', () => {
+ const versionable = makeVersionable({ test: 1 });
+ // do not throw
+ const obj = makeVersionable(Object.assign({}, versionable));
+ expect(obj.test).to.equal(1);
+ obj.test = 3;
+ expect(obj.test).to.equal(3);
+ expect(versionable.test).to.equal(1, 'Original object value');
+ });
+ it('custom class/object', () => {
+ class SuperObject extends VersionableObject {
+ my: number;
+ toto: number;
+ myMethod(num: number): void {
+ this.my = num;
+ }
+ }
+ const obj = new SuperObject();
+ obj.toto = 99;
+ expect(obj.my).to.equal(
+ undefined,
+ 'Should keep undefined value on versionable',
+ );
+ expect(obj.toto).to.equal(99, 'Should set a value on unlinked versionable');
+ memory.attach(obj);
+ expect(obj.my).to.equal(
+ undefined,
+ 'Should keep undefined value when link to memory',
+ );
+ expect(obj.toto).to.equal(99, 'Should keep value when link to memory');
+ obj.myMethod(42);
+ expect(obj.my).to.equal(42, 'Should use custom method on versionable');
+ expect(obj.toto).to.equal(99, 'Should keep same value');
+ });
+ it('custom Array', () => {
+ class SuperArray extends VersionableArray {
+ myMethod(num: number): void {
+ this.push(num);
+ }
+ }
+ const obj = new SuperArray();
+ obj.push(1);
+ obj.myMethod(99);
+ expect(obj.join(',')).to.equal('1,99');
+ expect(obj.length).to.equal(2);
+ memory.attach(obj);
+ expect(obj.join(',')).to.equal('1,99');
+ expect(obj.length).to.equal(2);
+ obj.myMethod(42);
+ expect(obj.join(',')).to.equal('1,99,42');
+ expect(obj.length).to.equal(3);
+ });
+ it('use proxy of versionable array', () => {
+ const obj = makeVersionable({ test: [] });
+ const array = new VersionableArray();
+ const proxy = new Proxy(array, {});
+ memory.attach(obj);
+ obj.test = proxy;
+ expect(obj.test).to.equal(proxy);
+ obj.test.push(9);
+ expect(array).to.deep.equal([9]);
+ });
+ it('custom Set who extend versionableSet', () => {
+ class SuperSet extends VersionableSet {
+ myMethod(num: number): void {
+ this.add(num);
+ }
+ }
+ const obj = new SuperSet();
+ obj.add(1);
+ obj.myMethod(99);
+ function join(obj: SuperSet): string {
+ const array = [];
+ obj.forEach((value: number): number => array.push(value));
+ return array.join(',');
+ }
+ expect(join(obj)).to.equal('1,99');
+ expect(obj.size).to.equal(2);
+ memory.attach(obj);
+ expect(join(obj)).to.equal('1,99');
+ expect(obj.size).to.equal(2);
+ obj.myMethod(42);
+ expect(join(obj)).to.equal('1,99,42');
+ expect(obj.size).to.equal(3);
+ });
+ it('Set with default value', () => {
+ const obj = makeVersionable({
+ a: 1,
+ b: new Set([{}]),
+ c: 3,
+ });
+ const b = obj.b;
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(obj);
+ memory.create('test');
+ memory.switchTo('test');
+ obj.b = new VersionableSet([1]);
+ expect([...obj.b]).to.deep.equal([1]);
+ memory.switchTo('1');
+ expect(obj.b).to.equal(b, 'switch object');
+ expect(obj.b.size).to.equal(1, 'one object');
+ expect([...obj.b]).to.deep.equal(
+ [{}],
+ 'should have the set who contains an object',
+ );
+ memory.switchTo('test');
+ expect(obj.b.size).to.equal(1, 'one number');
+ expect([...obj.b]).to.deep.equal(
+ [1],
+ 'should have the set who contains a number',
+ );
+ });
+ it('create versionableSet with array value to construct it', () => {
+ const set = new VersionableSet([1, 2, 3]);
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(set);
+ expect(set.size).to.equal(3);
+ });
+ it('create versionableSet with customSet value to construct it', () => {
+ class TestSet extends Set {
+ method(): number {
+ this.add(5);
+ return 3;
+ }
+ }
+ const set = new TestSet();
+ set.add(3);
+ const proxy = (new VersionableSet(set) as unknown) as TestSet;
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(proxy);
+ expect(proxy.size).to.equal(1);
+ expect(proxy.method()).to.equal(3);
+ });
+ it('create versionableSet with versionableSet', () => {
+ const set = new VersionableSet();
+ set.add(3);
+ expect(set.size).to.equal(1);
+ const set2 = new VersionableSet(set);
+ set2.add(4);
+ expect(set2.size).to.equal(2);
+ expect([...set2.values()]).to.deep.equal([3, 4]);
+ set.add(5);
+ expect(set2.size).to.equal(2);
+ });
+ it('custom class who contains proxy of Versionable', () => {
+ class SuperObject extends VersionableObject {
+ obj: object;
+ ref = 0;
+ constructor() {
+ super();
+ this.obj = proxifyVersionable(new VersionableObject(), {
+ set: (obj: object, prop: string, value: number): boolean => {
+ obj[prop] = value;
+ if (value === 42) {
+ this.ref++;
+ }
+ return true;
+ },
+ });
+ }
+ }
+ const instance = new SuperObject();
+ expect(instance.ref).to.equal(0);
+ instance.obj['A'.toString()] = 99;
+ expect(instance.ref).to.equal(0);
+ instance.obj['A'.toString()] = 42;
+ expect(instance.ref).to.equal(1);
+
+ memory.attach(instance);
+
+ expect(instance.obj).to.be.an.instanceof(VersionableObject);
+ expect(instance.obj['A'.toString()]).to.equal(42);
+ expect(instance.ref).to.equal(1);
+ instance.obj['B'.toString()] = 99;
+ expect(instance.ref).to.equal(1);
+ instance.obj['B'.toString()] = 42;
+ expect(instance.ref).to.equal(2);
+ });
+ it('custom class who contains get overwrite of Versionable', () => {
+ class SuperObject extends VersionableObject {
+ obj: Record;
+ constructor() {
+ super();
+ this.obj = new VersionableObject() as Record;
+ Object.defineProperty(this.obj, 'truc', {
+ get: (): number => {
+ return 42;
+ },
+ set: (): void => {
+ return;
+ },
+ });
+ }
+
+ get getter(): Record {
+ return this.obj;
+ }
+ }
+ const instance = new SuperObject();
+ instance.obj.A = 99;
+ expect(instance.obj.A).to.equal(99);
+ expect(instance.obj.truc).to.equal(42);
+ instance.obj.truc = 99;
+ expect(instance.obj.truc).to.equal(42);
+ expect(instance.obj).to.equal(instance.getter);
+
+ memory.attach(instance);
+
+ expect(instance.obj).to.be.an.instanceof(VersionableObject);
+ instance.obj.B = 99;
+ expect(instance.obj.B).to.equal(99);
+ expect(instance.obj.truc).to.equal(42);
+ instance.obj.truc = 99;
+ expect(instance.obj.truc).to.equal(42);
+ expect(instance.obj).to.equal(instance.getter);
+ });
+ });
+
+ describe('make versionable', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+
+ it('object', () => {
+ const obj: Record = {
+ a: 1,
+ b: 2,
+ c: 3,
+ };
+ Object.defineProperty(obj, 'z', {
+ get() {
+ return 42;
+ },
+ enumerable: false,
+ configurable: false,
+ });
+ const proxy = makeVersionable(obj);
+ memory.attach(proxy);
+ expect(proxy.a).to.equal(1);
+ expect(proxy.b).to.equal(2);
+ expect(proxy.c).to.equal(3);
+ expect(proxy.z).to.equal(42);
+ });
+ it('object who contains Object', () => {
+ const obj = makeVersionable({
+ a: 1,
+ b: {
+ x: 1,
+ y: 2,
+ },
+ c: 3,
+ });
+ memory.attach(obj);
+ expect(obj.a).to.equal(1);
+ expect(obj.b).to.be.a('object');
+ expect(Object.keys(obj.b).join()).to.equal('x,y');
+ expect(obj.b.x).to.equal(1);
+ expect(obj.b.y).to.equal(2);
+ expect(obj.c).to.equal(3);
+ });
+ it('object who contains Object who contains Object', () => {
+ const obj = makeVersionable({
+ a: 1,
+ b: {
+ x: 1,
+ y: {
+ e: 1,
+ y: 2,
+ },
+ },
+ c: 3,
+ });
+ memory.attach(obj);
+ expect(obj.b).to.be.a('object');
+ expect(Object.keys(obj.b).join()).to.equal('x,y');
+ expect(obj.b.x).to.equal(1);
+ expect(obj.b.y).to.be.a('object');
+ expect(Object.keys(obj.b.y).join()).to.equal('e,y');
+ expect(obj.b.y.e).to.equal(1);
+ expect(obj.b.y.y).to.equal(2);
+ });
+ it('object who contains Set', () => {
+ const obj = makeVersionable({
+ a: 1,
+ b: new Set(['x', 'y', 'z']),
+ c: 3,
+ });
+ memory.attach(obj);
+ expect(obj.a).to.equal(1);
+ expect(obj.b).to.be.instanceOf(Set);
+ expect(obj.b.has('x')).to.equal(true);
+ expect(obj.b.has('y')).to.equal(true);
+ expect(obj.b.has('z')).to.equal(true);
+ expect(obj.b.has('h')).to.equal(false);
+ expect(obj.c).to.equal(3);
+ });
+ it('object who contains Array', () => {
+ const obj = makeVersionable({
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: 3,
+ });
+ memory.attach(obj);
+ expect(obj.a).to.equal(1);
+ expect(obj.b).to.be.instanceOf(Array);
+ expect(obj.b.indexOf('x')).to.equal(0, "should find 'x' at 0");
+ expect(obj.b.indexOf('y')).to.equal(1, "should find 'y' at 0");
+ expect(obj.b.indexOf('z')).to.equal(2, "should find 'z' at 0");
+ expect(obj.b.indexOf('h')).to.equal(-1, "should not find 'h'");
+ let str = '';
+ obj.b.forEach(v => {
+ str += v;
+ });
+ expect(str).to.equal('xyz', 'should use forEach');
+ expect(obj.c).to.equal(3);
+
+ memory.switchTo('');
+ expect(obj).to.deep.equal({}, 'should not find object in other memory slice');
+ memory.switchTo('1');
+ expect(obj).to.deep.equal(
+ {
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: 3,
+ },
+ 'Should have all values',
+ );
+ });
+ it('complexe', () => {
+ const full = makeVersionable({
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: new Set(['x', 'y', 'z']),
+ d: {
+ x: 1,
+ y: {
+ e: 1,
+ y: 2,
+ },
+ },
+ e: [],
+ f: {},
+ g: new Set(),
+ h: 'a',
+ i: [{ 'a': 1 }, 55],
+ j: new Set([{ 'b': 1 }]),
+ k: [['a'], ['b', 'c', 'd'], ['x', 'y', 'z']],
+ });
+
+ expect(!!full.i[0][memoryProxyPramsKey].ID).to.equal(
+ true,
+ 'proxify object in array',
+ );
+ const list = [];
+ full.j.forEach((item: object) => {
+ list.push(item);
+ });
+ expect(!!list[0][memoryProxyPramsKey].ID).to.equal(
+ true,
+ 'proxify object in set',
+ );
+ expect(!!full.k[0][memoryProxyPramsKey].ID).to.equal(
+ true,
+ 'proxify array in array',
+ );
+
+ memory.attach(full);
+
+ memory.create('1-a');
+ memory.switchTo('1-a');
+ full.k[1][3] = 'e';
+ expect(full.k[1]).to.deep.equal(
+ ['b', 'c', 'd', 'e'],
+ 'should link to memory and change the array in array',
+ );
+ memory.switchTo('1');
+ expect(full.k[1]).to.deep.equal(
+ ['b', 'c', 'd'],
+ 'should have the previous value in the array in array',
+ );
+ });
+ it('make versionable a class who extends Set class', () => {
+ class TestSet extends Set {
+ method(): number {
+ this.add(5);
+ return 3;
+ }
+ }
+ let set = new TestSet();
+ expect(set.size).to.equal(0, 'before link, set is empty');
+ expect(set.method()).to.equal(3, 'before link, call method');
+ expect(set.size).to.equal(1, 'before link, set contains the value');
+
+ set = makeVersionable(new TestSet());
+ expect(set.size).to.equal(0, 'make versionable, set is empty');
+ expect(set.method()).to.equal(3, 'make versionable, call method');
+ expect(set.size).to.equal(1, 'make versionable, set contains the value');
+
+ set = makeVersionable(new TestSet());
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(set);
+ expect(set.size).to.equal(0, 'linked, set is empty');
+ expect(set.method()).to.equal(3, 'linked, call method');
+ expect(set.size).to.equal(1, 'linked, set contains the value');
+ });
+ });
+
+ describe('specific getter', () => {
+ it('object getter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ obj = makeVersionable({
+ get test() {
+ if (obj) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ return test;
+ },
+ });
+ expect(obj.test).to.equal(1);
+ });
+ it('custom class getter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ class Custom extends VersionableObject {
+ get test(): number {
+ if (obj) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ return test;
+ }
+ }
+ obj = new Custom();
+ expect(obj.test).to.equal(1);
+ });
+ it('extended custom class getter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ class Custom extends VersionableObject {
+ get test(): number {
+ if (obj) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ return test;
+ }
+ }
+ class CustomCustom extends Custom {}
+ obj = new CustomCustom();
+ expect(obj.test).to.equal(1);
+ });
+ it('overwrite toString method', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ class Custom extends VersionableObject {
+ toString(): string {
+ return '5';
+ }
+ }
+ class CustomCustom extends Custom {}
+ obj = new CustomCustom();
+ expect(typeof obj.toString).to.equal('function');
+ expect(obj.toString()).to.equal('5');
+ });
+ it('get constructor', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ class Custom extends VersionableObject {
+ getConstructor(): string {
+ return this.constructor.name;
+ }
+ }
+ class CustomCustom extends Custom {}
+ obj = new CustomCustom();
+ expect(obj.getConstructor()).to.equal('CustomCustom');
+ });
+ it.skip('array getter should keep proxy for "this" (use defineProperty)', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const obj = new VersionableArray() as any;
+ let test = 0;
+ Object.defineProperty(obj, 'test', {
+ get() {
+ if (obj) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ return test;
+ },
+ });
+ expect(obj.test).to.equal(1);
+ });
+ it('array slice', () => {
+ const memory = new Memory();
+ const array = makeVersionable([1, 2, 3, 4, 5]);
+ memory.attach(array);
+ const newArray = array.slice();
+ expect(newArray).to.deep.equal([1, 2, 3, 4, 5]);
+ expect(newArray[memoryProxyPramsKey]).to.equal(undefined);
+ });
+ it('array slice on VersionableArray', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ const newArray = array.slice();
+ expect(newArray).to.deep.equal([1, 2, 3, 4, 5]);
+ expect(newArray[memoryProxyPramsKey].ID).to.not.equal(
+ array[memoryProxyPramsKey].ID,
+ );
+ });
+ it('array splice', () => {
+ const memory = new Memory();
+ const array = makeVersionable([1, 2, 3, 4, 5]);
+ memory.attach(array);
+ const newArray = array.splice(1, 3);
+ expect(array).to.deep.equal([1, 5]);
+ expect(newArray).to.deep.equal([2, 3, 4]);
+ expect(newArray[memoryProxyPramsKey]).to.equal(undefined);
+ });
+ it('array splice on VersionableArray', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ const newArray = array.splice(1, 3);
+ expect(array).to.deep.equal([1, 5]);
+ expect(newArray).to.deep.equal([2, 3, 4]);
+ expect(newArray[memoryProxyPramsKey].ID).to.not.equal(
+ array[memoryProxyPramsKey].ID,
+ );
+ });
+ it('array forEach', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ const values = [];
+ const indexes = [];
+ const arrays = [];
+ const res = array.forEach((value, index, array) => {
+ values.push(value);
+ indexes.push(index);
+ arrays.push(array);
+ return 9;
+ });
+ expect(res).to.equal(undefined);
+ expect(values).to.deep.equal([1, 2, 3, 4, 5]);
+ expect(indexes).to.deep.equal([0, 1, 2, 3, 4]);
+ expect(arrays).to.deep.equal([array, array, array, array, array]);
+ });
+ it('array map', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ const values = [];
+ const indexes = [];
+ const arrays = [];
+ const res = array.map((value: number, index, array) => {
+ values.push(value);
+ indexes.push(index);
+ arrays.push(array);
+ return 8 + value;
+ });
+ expect(res[memoryProxyPramsKey]).to.equal(undefined);
+ expect(res).to.deep.equal([9, 10, 11, 12, 13]);
+ expect(values).to.deep.equal([1, 2, 3, 4, 5]);
+ expect(indexes).to.deep.equal([0, 1, 2, 3, 4]);
+ expect(arrays).to.deep.equal([array, array, array, array, array]);
+ });
+ it('array filter', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ const values = [];
+ const indexes = [];
+ const arrays = [];
+ const res = array.filter((value: number, index, array) => {
+ values.push(value);
+ indexes.push(index);
+ arrays.push(array);
+ return !!(value % 2);
+ });
+ expect(res[memoryProxyPramsKey]).to.equal(undefined);
+ expect(res).to.deep.equal([1, 3, 5]);
+ expect(values).to.deep.equal([1, 2, 3, 4, 5]);
+ expect(indexes).to.deep.equal([0, 1, 2, 3, 4]);
+ expect(arrays).to.deep.equal([array, array, array, array, array]);
+ });
+ it('array indexOf', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+
+ const result = [];
+ const array = new VersionableArray();
+ for (let k = 0; k < 20; k++) {
+ array.push(k);
+ result.push(k);
+ }
+ memory.attach(array);
+ for (let k = 20; k < 40; k++) {
+ array.push(k);
+ result.push(k);
+ }
+
+ for (let k = 0, len = array.length; k < len; k++) {
+ expect(array.indexOf(k)).to.equal(k);
+ }
+ expect(array.indexOf(-99)).to.equal(-1);
+
+ array[3] = -3;
+
+ expect(array.indexOf(3)).to.equal(-1);
+ expect(array.indexOf(-3)).to.equal(3);
+ });
+ it('array indexOf value several times', () => {
+ const array = new VersionableArray(0, 1, 2, 3, 1, 4);
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+
+ expect(array.indexOf(1)).to.equal(1, 'should return the first index');
+ array[1] = 42;
+ expect(array.indexOf(1)).to.equal(4, 'should return the other index');
+ expect(array.indexOf(4)).to.equal(5, 'should the index');
+ array[1] = 4;
+ expect(array.indexOf(4)).to.equal(1, 'should the newest index');
+ });
+ });
+
+ describe('specific setter', () => {
+ it('object setter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ obj = makeVersionable({
+ get test() {
+ return 0;
+ },
+ set test(x: number) {
+ test++;
+ expect(this).to.equal(obj);
+ },
+ });
+ obj.test = 1;
+ expect(test).to.equal(1);
+ });
+ it('custom class setter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ class Custom extends VersionableObject {
+ get test(): number {
+ return 0;
+ }
+ set test(x: number) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ }
+ obj = new Custom();
+ obj.test = 1;
+ expect(test).to.equal(1);
+ });
+ it('extended custom class setter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let obj: any = null;
+ let test = 0;
+ class Custom extends VersionableObject {
+ get test(): number {
+ return 0;
+ }
+ set test(x: number) {
+ test++;
+ expect(this).to.equal(obj);
+ }
+ }
+ class CustomCustom extends Custom {}
+ obj = new CustomCustom();
+ obj.test = 1;
+ expect(test).to.equal(1);
+ });
+ it('array setter should keep proxy for "this"', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const obj = new VersionableArray() as any;
+ let test = 0;
+ Object.defineProperty(obj, 'test', {
+ get() {
+ return 0;
+ },
+ set() {
+ test++;
+ expect(this).to.equal(obj);
+ },
+ });
+ obj.test = 1;
+ expect(test).to.equal(1);
+ });
+ it('use Object.assing on versionable object', () => {
+ const versionable = makeVersionable({ test: 1 });
+ const obj = makeVersionable({ test: 5 });
+ Object.assign(obj, versionable);
+ expect(obj[memoryProxyPramsKey]).to.not.equal(versionable[memoryProxyPramsKey]);
+ expect(obj.test).to.equal(1);
+ obj.test = 3;
+ expect(obj.test).to.equal(3);
+ expect(versionable.test).to.equal(1, 'Original object value');
+ });
+ it('use Object.assing on versionable array', () => {
+ const versionable = makeVersionable([1, 2, 3]);
+ const array = makeVersionable([]);
+ Object.assign(array, versionable);
+ expect(array[memoryProxyPramsKey]).to.not.equal(
+ versionable[memoryProxyPramsKey],
+ );
+ expect(array).to.deep.equal([1, 2, 3]);
+ array[1] = 42;
+ expect(array[1]).to.equal(42);
+ expect(versionable[1]).to.equal(2, 'Original object value');
+ });
+ it('array changes with splice', () => {
+ const memory = new Memory();
+ const array = makeVersionable(['x', 'y', 'z']);
+ memory.attach(array);
+ memory.create('1');
+ memory.create('test');
+ memory.switchTo('test');
+ array.splice(1, 0, 'B'); // ['x', 'B', 'y', 'z']
+ array.push('z'); // ['x', 'B', 'y', 'z', 'z']
+ array.splice(3, 0, 'A'); // ['x', 'B', 'y', 'A', 'z', 'z']
+
+ expect(array.slice()).to.deep.equal(['x', 'B', 'y', 'A', 'z', 'z']);
+
+ array.splice(1, 2); // ['x', 'A', 'z', 'z']
+ array[2] = 'y'; // ['x', 'A', 'y', 'z']
+
+ expect(array.slice()).to.deep.equal(['x', 'A', 'y', 'z'], 'before switch');
+
+ memory.switchTo('1');
+ memory.switchTo('test');
+
+ expect(array.slice()).to.deep.equal(['x', 'A', 'y', 'z']);
+ });
+ it('array changes with splice without second value', () => {
+ const memory = new Memory();
+ const array = makeVersionable(['x', 'y', 'z']);
+ memory.attach(array);
+ memory.create('1');
+ memory.create('test');
+ memory.switchTo('test');
+ array.splice(1);
+ memory.switchTo('1');
+ memory.switchTo('test');
+ expect(array.slice()).to.deep.equal(['x']);
+ });
+ });
+
+ describe('specific updates', () => {
+ const memory = new Memory();
+ const full = makeVersionable({
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: new Set(['x', 'y', 'z']),
+ d: {
+ x: 1,
+ y: {
+ e: 1,
+ y: 2,
+ },
+ },
+ e: [],
+ f: {},
+ g: new Set(),
+ h: 'a',
+ i: [{ 'a': 1 }, 55],
+ j: new Set([{ 'b': 1 }]) as Set,
+ });
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(full);
+
+ it('remove an attribute', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-b');
+ memory.create('test-a');
+ memory.switchTo('test-a');
+
+ full.d.y.e = 3;
+ delete full.d.y.y;
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.d.y.e = 5;
+ full.d.y.y = 6;
+
+ memory.switchTo('test-a-1');
+
+ full.d.y.e = 7;
+
+ expect(full.d.y).to.deep.equal({ e: 7 }, 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect(full.d.y).to.deep.equal(
+ { e: 3 },
+ 'after swith check if memory writed on new slice',
+ );
+ });
+ it('remove twice an attribute', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-a');
+ memory.create('test-b');
+ memory.switchTo('test-a');
+
+ full.d.y.e = 3;
+ delete full.d.y.y;
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.d.y.e = 5;
+ full.d.y.y = 6;
+
+ memory.switchTo('test-a-1');
+
+ delete full.d.y.y;
+
+ expect(full.d.y).to.deep.equal({ e: 3 }, 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect(full.d.y).to.deep.equal(
+ { e: 3 },
+ 'after swith check if memory writed on new slice',
+ );
+ });
+ it('update array properties', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ const array = new VersionableArray();
+
+ const bibi = function toto(): number {
+ return 1;
+ };
+ array['a+b'] = bibi;
+
+ memory.attach(array);
+
+ memory.create('test-a');
+ memory.switchTo('test-a');
+
+ const toto = function toto(): number {
+ return 1;
+ };
+ array['x+y'] = toto;
+
+ memory.switchTo('test');
+ expect(Object.keys(array)).to.deep.equal(['a+b'], 'has origin key');
+ expect(array['x+y']).to.equal(undefined, "don't add method on other slice");
+ expect(array['a+b']).to.equal(bibi, 'add method in the linked slice');
+ memory.switchTo('test-a');
+ expect(Object.keys(array)).to.deep.equal(
+ ['a+b', 'x+y'],
+ 'has origin and additional keys',
+ );
+ expect(array['x+y']).to.equal(toto, 'add method on this slice');
+ expect(array['a+b']).to.equal(bibi, 'add method in the child of linked slice');
+ });
+ it('update array properties with object', () => {
+ const obj0 = new VersionableObject({ z: 42 });
+ const obj1 = new VersionableObject({ a: 1 });
+ const obj2 = new VersionableObject({ b: 2 });
+ const array = new VersionableArray();
+ array['x+y'] = obj0;
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ expect(array['x+y']).to.equal(obj0, 'add an object as property');
+ array['x+y'] = obj1;
+ expect(array['x+y']).to.equal(obj1, 'add an object as property');
+ array['x+y'] = obj2;
+ expect(array['x+y']).to.equal(obj2, 'change the property with an other object');
+ delete array['x+y'];
+ expect(array).to.deep.equal([], 'remove the property');
+ });
+ it('remove array properties without memory', () => {
+ const array = new VersionableArray();
+ array['x+y'] = 1;
+ expect(array['x+y']).to.equal(1, 'should have the default property');
+ delete array['x+y'];
+ expect(array['x+y']).to.equal(undefined, 'should remove the property');
+ });
+ it('remove array properties in memory', () => {
+ const array = new VersionableArray();
+ array['x+y'] = 1;
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ expect(array['x+y']).to.equal(1, 'should have the default property');
+ delete array['x+y'];
+ expect(array['x+y']).to.equal(undefined, 'should remove the property');
+ memory.create('next');
+ memory.switchTo('next');
+ array['x+y'] = 2;
+ expect(array['x+y']).to.equal(2, 'should update the property');
+ delete array['x+y'];
+ expect(array['x+y']).to.equal(undefined, 'should remove again the property');
+ });
+ it('create array with value only at index 5 and 10', () => {
+ const array = new VersionableArray();
+ const res = [];
+ const obj = new VersionableObject({ a: 1 });
+ res[5] = obj;
+ res[10] = obj;
+ array[5] = obj;
+ array[10] = obj;
+ expect(array).to.deep.equal(res, 'before link');
+ const memory = new Memory();
+ memory.create('a');
+ memory.switchTo('a');
+ memory.create('b');
+ memory.switchTo('b');
+ memory.attach(array);
+ expect(array).to.deep.equal(res, 'after link');
+ memory.switchTo('a');
+ memory.switchTo('b');
+ expect(array).to.deep.equal(res, 'after switch slices');
+ });
+ it('add item in array at index 5', () => {
+ const array = new VersionableArray();
+ const res = [];
+ const obj = new VersionableObject({ a: 1 });
+ res[5] = obj;
+ res[10] = obj;
+ const memory = new Memory();
+ memory.create('a');
+ memory.switchTo('a');
+ memory.attach(array);
+ memory.create('b');
+ memory.switchTo('b');
+ array[5] = obj;
+ array[10] = obj;
+ expect(array).to.deep.equal(res, 'after update');
+ memory.switchTo('a');
+ memory.switchTo('b');
+ expect(array).to.deep.equal(res, 'after switch slices');
+ });
+ it('change array length', () => {
+ const array = new VersionableArray(1, 2, 3);
+ expect(array).to.deep.equal([1, 2, 3]);
+
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ expect(array).to.deep.equal([1, 2, 3]);
+ array.length = 1;
+ expect(array).to.deep.equal([1]);
+ });
+ it('use set in array versionableSet', () => {
+ const set1 = new VersionableSet();
+ const set2 = new VersionableSet();
+ const array = new VersionableArray(set1, set2);
+ expect(array).to.deep.equal([set1, set2]);
+
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ expect(array).to.deep.equal([set1, set2]);
+ array.length = 1;
+ expect(array).to.deep.equal([set1]);
+ });
+ it('unshift object in array', () => {
+ const obj = new VersionableObject({ a: 1 });
+ const array = new VersionableArray();
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ array.unshift(obj);
+ expect(array).to.deep.equal([obj]);
+ });
+ it('shift object in array', () => {
+ const obj = new VersionableObject();
+ const array = new VersionableArray(obj);
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+ const shifted = array.shift();
+ expect(array).to.deep.equal([]);
+ expect(shifted).to.equal(obj);
+ });
+ it('update array in order', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ const result = [];
+ const array = new VersionableArray();
+ for (let k = 0; k < 5000; k++) {
+ array.push(k);
+ result.push(k);
+ }
+
+ memory.attach(array);
+ expect(array).to.deep.equal(result, 'link this array');
+
+ for (let k = 0; k < 100; k++) {
+ array.unshift(k);
+ result.unshift(k);
+ }
+
+ array.unshift(999);
+ result.unshift(999);
+ array.push(42);
+ result.push(42);
+
+ memory.create('test-a');
+ memory.switchTo('test-a');
+
+ array.push(2000);
+
+ memory.switchTo('test');
+
+ expect(array).to.deep.equal(result);
+ });
+ it('array multi splice in same memory slice', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+
+ const array = makeVersionable([1, 2, 3]);
+ memory.attach(array);
+
+ memory.create('test-1');
+ memory.switchTo('test-1');
+
+ array.splice(2, 0, 6, 7);
+ array.splice(1, 0, 4, 5);
+ expect(array.slice()).to.deep.equal([1, 4, 5, 2, 6, 7, 3]);
+
+ const sub = array.splice(1, 5);
+ expect(array.slice()).to.deep.equal([1, 3]);
+ expect(sub).to.deep.equal([4, 5, 2, 6, 7]);
+
+ memory.switchTo('test');
+ memory.switchTo('test-1');
+ expect(array.slice()).to.deep.equal([1, 3], 'Test internal memory');
+ });
+ it('default value for array', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ memory.create('a');
+ memory.switchTo('a');
+ array[1] = 9;
+ expect(array).to.deep.equal([1, 9, 3, 4, 5]);
+ memory.switchTo('');
+ memory.create('b');
+ memory.switchTo('b');
+ expect(array).to.deep.equal([1, 2, 3, 4, 5]);
+ });
+ it('delete item in array', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ delete array[0];
+ array[0] = 1;
+ delete array[1];
+ array[1] = 9;
+ delete array[1];
+ expect(array).to.deep.equal([1, undefined, 3, 4, 5]);
+ });
+ it('delete item in array and switch memory', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+
+ memory.create('1-1');
+ memory.switchTo('1-1');
+ delete array[1];
+
+ expect(array).to.deep.equal([1, undefined, 3, 4, 5], 'remove one item');
+ const array1 = array.slice();
+
+ memory.switchTo('test');
+ memory.create('1-2');
+ memory.switchTo('1-2');
+ delete array[0];
+ array[0] = 1;
+ delete array[1];
+ array[1] = 9;
+ delete array[1];
+ const array2 = array.slice();
+
+ expect(array2).to.deep.equal(array1, 'array have same value');
+ });
+ it('array push and pop in same slide memory have a clean memory slice values', () => {
+ const memory = new Memory();
+ const array = new VersionableArray(1, 2, 3, 4, 5);
+ memory.attach(array);
+ memory.create('1');
+ memory.switchTo('1');
+ array.push(9);
+ array.push(9);
+ array.push(9);
+ array.pop();
+ array.pop();
+ const currentSlice = memory._currentSlice.data;
+ expect(currentSlice[array[memoryProxyPramsKey].ID]).to.deep.equal({
+ props: {},
+ patch: { '094': 9 },
+ });
+ });
+ it('use splice and switch memory', () => {
+ const memory = new Memory();
+ const array = makeVersionable(['x', 'y', 'z']);
+ memory.attach(array);
+
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('1-1');
+ memory.switchTo('1-1');
+ array.splice(1, 0, 'A');
+
+ expect(array.slice()).to.deep.equal(['x', 'A', 'y', 'z']);
+
+ memory.switchTo('test');
+ memory.create('1-2');
+ memory.switchTo('1-2');
+ array.splice(1, 0, 'B');
+
+ expect(array.slice()).to.deep.equal(['x', 'B', 'y', 'z']);
+ });
+ it('move item but finish at the same index keep same order', () => {
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+
+ const array = makeVersionable([1, 2, 3]);
+ memory.attach(array);
+
+ memory.create('test-1');
+ memory.switchTo('test-1');
+
+ array.splice(2, 0, 6, 7);
+ array.splice(1, 0, 4, 5);
+ array.splice(1, 5);
+
+ array.splice(1, 0, 2);
+ expect(array.slice()).to.deep.equal([1, 2, 3]);
+
+ memory.switchTo('test');
+ memory.switchTo('test-1');
+
+ expect(array.slice()).to.deep.equal([1, 2, 3], 'Test internal memory');
+ });
+ it('create object in a slice and read it in other', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-a');
+ memory.switchTo('test-a');
+ const obj = new VersionableObject();
+ obj['x+y'] = 3;
+ memory.attach(obj);
+ const array = new VersionableArray();
+ array.push(3);
+ memory.attach(array);
+ const set = new VersionableSet();
+ set.add(3);
+ memory.attach(set);
+ memory.switchTo('test');
+ expect(obj['x+y']).to.equal(undefined);
+ expect(array).to.deep.equal([]);
+ expect(set).to.deep.equal(new Set());
+ });
+ it('remove attribute on un-linked object', () => {
+ const obj = makeVersionable({ a: 1, b: 2 });
+ delete obj.a;
+ expect(obj).to.deep.equal({ b: 2 }, 'before link');
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(obj);
+ expect(obj).to.deep.equal({ b: 2 }, 'after link');
+ });
+ it('remove object in set', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-a');
+ memory.switchTo('test-a');
+ let jObj: object;
+ full.j.forEach((obj: object) => {
+ jObj = obj;
+ });
+ full.j.delete(new VersionableObject());
+ expect(full.j).to.deep.equal(
+ new Set([jObj]),
+ 'delete an object not contains in set',
+ );
+ full.j.delete(jObj);
+ expect(full.j).to.deep.equal(new Set(), 'delete the object');
+ full.j.clear();
+ expect(full.j).to.deep.equal(new Set(), 'clear empty set');
+ full.j.add(jObj);
+ full.j.clear();
+ expect(full.j).to.deep.equal(new Set(), 'clear owner slice set');
+ memory.switchTo('test');
+ expect(full.j).to.deep.equal(new Set([jObj]), 'other slice value');
+ memory.create('test-b');
+ memory.switchTo('test-b');
+ full.j.clear();
+ expect(full.j).to.deep.equal(new Set(), 'clear set');
+ });
+ it('add function in set', () => {
+ const set = new VersionableSet();
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(set);
+ function a(): void {
+ return;
+ }
+ function b(): void {
+ return;
+ }
+ set.add(a);
+ expect(set).to.deep.equal(new Set([a]));
+ memory.create('other');
+ memory.switchTo('other');
+ set.add(b);
+ expect(set).to.deep.equal(new Set([a, b]));
+ });
+ it('clear versionableSet', () => {
+ let set = new VersionableSet([1, 2, 3]);
+ expect(set.size).to.equal(3);
+ expect(set.has(1)).to.equal(true);
+ set.clear();
+ expect(set.size).to.equal(0);
+ expect(set.has(1)).to.equal(false);
+
+ set = new VersionableSet([1, 2, 3]);
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(set);
+ set.clear();
+ expect(set.size).to.equal(0, 'linked');
+ expect(set.has(1)).to.equal(false, 'linked');
+ });
+ it('replace attribute by set', () => {
+ const obj = makeVersionable({
+ set: new Set([1, 2, 3]),
+ });
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(obj);
+
+ const set = new VersionableSet([4, 5, 6]);
+
+ expect(obj).to.deep.equal({
+ set: new Set([1, 2, 3]),
+ });
+ memory.create('other');
+ memory.switchTo('other');
+ obj.set = set as Set;
+ expect(obj).to.deep.equal({
+ set: new Set([4, 5, 6]),
+ });
+ memory.switchTo('test');
+ expect(obj).to.deep.equal(
+ {
+ set: new Set([1, 2, 3]),
+ },
+ 'check previous value',
+ );
+ memory.switchTo('other');
+ expect(obj).to.deep.equal(
+ {
+ set: new Set([4, 5, 6]),
+ },
+ 'check again',
+ );
+ });
+ });
+
+ describe('mutliple updates', () => {
+ const memory = new Memory();
+ const full = makeVersionable({
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: new Set(['x', 'y', 'z']),
+ d: {
+ x: 1,
+ y: {
+ e: 1,
+ y: 2,
+ },
+ },
+ e: [],
+ });
+
+ it('Link object', () => {
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(full);
+ });
+ it('should update an object (1)', () => {
+ full.a = 42;
+ expect(full.a).to.equal(42);
+ });
+ it('should update an object in object (1)', () => {
+ full.d.y.e = 42;
+ expect(full.d.y.e).to.equal(42);
+ });
+ it('should update a set in object (1)', () => {
+ full.c.delete('y');
+ full.c.add('aaa');
+
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal('x,z,aaa');
+ });
+ it('should update an array in object (1)', () => {
+ full.b.push('t');
+ full.b.splice(1, 2);
+ expect(full.b.join()).to.equal('x,t');
+ });
+ it('should have all object values (1)', () => {
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 42,
+ b: ['x', 't'],
+ c: new Set(['x', 'z', 'aaa']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 2,
+ },
+ },
+ e: [],
+ }),
+ );
+ });
+
+ it('create slice memory (1)', () => {
+ memory.create('1-1');
+ memory.create('1-2');
+ });
+
+ it('should have the same value in the created slide', () => {
+ memory.switchTo('1');
+ memory.switchTo('1-1');
+ expect(full.a).to.equal(42, "number 'a'");
+ expect(full.d.y.e).to.equal(42, "number in object 'd.y.e'");
+ expect(full.b).to.deep.equal(
+ ['x', 't'],
+ "push 't' and splice(1, 2) from ['x', 'y', 'z']",
+ );
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal(
+ 'x,z,aaa',
+ "delete 'y' and add 'aaa' in Set(['x', 'y', 'z'])",
+ );
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 42,
+ b: ['x', 't'],
+ c: new Set(['x', 'z', 'aaa']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 2,
+ },
+ },
+ e: [],
+ }),
+ );
+ memory.switchTo('1');
+ });
+
+ it('should have memory slice values', () => {
+ memory.switchTo('1');
+ memory.switchTo('1-1');
+ full.a = 3;
+ expect(full.a).to.equal(3);
+ memory.switchTo('1-2');
+ expect(full.a).to.equal(42);
+ memory.switchTo('1');
+ });
+ it('should switch memory slice (1 -> 1-1)', () => {
+ memory.switchTo('1-1');
+ });
+ it('should update an object (1-1)', () => {
+ full.a = 5;
+ expect(full.a).to.equal(5);
+ });
+ it('should update an object in object (1-1)', () => {
+ full.d.y.y = 5;
+ expect(full.d.y.y).to.equal(5);
+ });
+ it('should update a set in object (1-1)', () => {
+ full.c.delete('aaa');
+ full.c.add('bbb');
+ full.c.add('ccc');
+
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal('x,z,bbb,ccc');
+ });
+ it('should update an array in object (1-1)', () => {
+ full.b.unshift('o');
+ expect(full.b.join()).to.equal('o,x,t', "unshift 'o' in ['x', 't']");
+ });
+ it('should have all object values (1-1)', () => {
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal(
+ 'x,z,bbb,ccc',
+ "delete 'aaa' and add 'bbb' + 'ccc' in Set(['x' , 'z', 'aaa'])",
+ );
+
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 5,
+ b: ['o', 'x', 't'],
+ c: new Set(['x', 'z', 'bbb', 'ccc']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 5,
+ },
+ },
+ e: [],
+ }),
+ );
+ });
+ it('should switch to an other memory slice (1-1 -> 1-2)', () => {
+ memory.switchTo('1-2');
+ });
+ it('should read the content without previous slice updates (1-2)', () => {
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 42,
+ b: ['x', 't'],
+ c: new Set(['x', 'z', 'aaa']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 2,
+ },
+ },
+ e: [],
+ }),
+ );
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal('x,z,aaa');
+ });
+ it('should update an object (1-2)', () => {
+ full.a = 3;
+ expect(full.a).to.equal(3);
+ });
+ it('should update an object in object (1-2)', () => {
+ full.d.y.y = 9;
+ expect(full.d.y.y).to.equal(9);
+ });
+ it('should update a set in object (1-2)', () => {
+ full.c.delete('x');
+
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal('z,aaa');
+ });
+ it('should update an array in object (1-2)', () => {
+ full.b.splice(1, 1, 'OO');
+ expect(full.b.join()).to.equal('x,OO');
+ });
+ it('should have all object values (1-2)', () => {
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 3,
+ b: ['x', 'OO'],
+ c: new Set(['z', 'aaa']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 9,
+ },
+ },
+ e: [],
+ }),
+ );
+ });
+ it('should add a key in memory and have all object values (1-2)', () => {
+ memory.attach(full);
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 3,
+ b: ['x', 'OO'],
+ c: new Set(['z', 'aaa']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 9,
+ },
+ },
+ e: [],
+ }),
+ );
+ });
+
+ it('should switch to an other memory slice (1-2 -> 1-1)', () => {
+ memory.switchTo('1-1');
+ });
+
+ it('should have again all object values (1-1)', () => {
+ expect(full.b.join()).to.equal('o,x,t', "unshift 'o' in ['x', 't']");
+ const items = [];
+ full.c.forEach(v => {
+ items.push(v);
+ });
+ expect(items.join()).to.equal(
+ 'x,z,bbb,ccc',
+ "delete 'aaa' and add 'bbb' + 'ccc' in Set(['x' , 'z', 'aaa'])",
+ );
+
+ expect(JSON.stringify(full)).to.equal(
+ JSON.stringify({
+ a: 5,
+ b: ['o', 'x', 't'],
+ c: new Set(['x', 'z', 'bbb', 'ccc']),
+ d: {
+ x: 1,
+ y: {
+ e: 42,
+ y: 5,
+ },
+ },
+ e: [],
+ }),
+ );
+ });
+ });
+
+ describe('update without read values', () => {
+ const memory = new Memory();
+ const full = makeVersionable({
+ a: 1,
+ b: ['x', 'y', 'z'],
+ c: new Set(['x', 'y', 'z']),
+ d: {
+ x: 1,
+ y: {
+ e: 1,
+ y: 2,
+ },
+ },
+ e: [],
+ f: {},
+ g: new Set(),
+ h: 'a',
+ i: [{ 'a': 1 }, 55],
+ j: new Set([{ 'b': 1 }]) as Set,
+ });
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(full);
+
+ it('update object after switch without read values', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-a');
+ memory.create('test-b');
+ memory.switchTo('test-a');
+
+ full.d.y.e = 3;
+ full.d.y.y = 4;
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.d.y.e = 5;
+ full.d.y.y = 6;
+
+ memory.switchTo('test-a-1');
+
+ full.d.y.e = 7;
+
+ expect(full.d.y).to.deep.equal({
+ e: 7,
+ y: 4,
+ });
+ });
+ it('check if attribute in object without read values', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-b');
+ memory.create('test-a');
+ memory.switchTo('test-a');
+
+ delete full.d.y.y;
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.d.y.y = 6;
+
+ memory.switchTo('test-a-1');
+
+ expect('y' in full.d.y).to.equal(false, 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect('y' in full.d.y).to.equal(
+ false,
+ 'after swith check if memory writed on new slice',
+ );
+
+ memory.switchTo('test-b');
+
+ expect('y' in full.d.y).to.equal(
+ true,
+ 'check if the reload work for "in" instruction',
+ );
+ });
+ it('update array after switch without read values', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-a');
+ memory.create('test-b');
+ memory.switchTo('test-a');
+
+ const obj = new VersionableObject();
+ full.e.push(obj, obj, obj);
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.e.push(new VersionableArray());
+
+ memory.switchTo('test-a-1');
+
+ const set = new VersionableSet();
+ full.e[1] = set;
+
+ expect(full.e).to.deep.equal([obj, set, obj], 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect(full.e).to.deep.equal(
+ [obj, obj, obj],
+ 'after swith check if memory writed on new slice',
+ );
+ });
+ it('update array length after switch without read values', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-a');
+ memory.create('test-b');
+ memory.switchTo('test-a');
+
+ const obj = new VersionableObject();
+ const array = new VersionableArray();
+ full.e.push(obj, array);
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.e.push(33);
+
+ memory.switchTo('test-a-1');
+
+ full.e.length = 1;
+ const set = makeVersionable({ a: 1 });
+ full.e.push(set);
+
+ expect(full.e).to.deep.equal([obj, set], 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect(full.e).to.deep.equal(
+ [obj, array],
+ 'after swith check if memory writed on new slice',
+ );
+ });
+ it('update set after switch without read values', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-a');
+ memory.create('test-b');
+ memory.switchTo('test-a');
+
+ full.c.add('a');
+
+ memory.create('test-a-1');
+ memory.switchTo('test-b');
+
+ full.c.delete('y');
+
+ memory.switchTo('test-a-1');
+
+ full.c.clear();
+
+ expect(full.c).to.deep.equal(new Set(), 'after a change without read');
+
+ memory.switchTo('test-a');
+
+ expect(full.c).to.deep.equal(
+ new Set(['x', 'y', 'z', 'a']),
+ 'after swith check if memory writed on new slice',
+ );
+ });
+ it('update set and call building methods', () => {
+ memory.switchTo('1');
+ memory.remove('test'); // use remove before create to avoid previous test error propagation
+ memory.create('test');
+ memory.switchTo('test');
+ memory.create('test-b');
+ memory.create('test-a');
+ memory.switchTo('test-a');
+ const set = full.j;
+ set.add(new VersionableObject());
+ memory.switchTo('test-b');
+ let iter = set.values().next();
+ expect(iter.value).to.deep.equal({ 'b': 1 });
+ memory.switchTo('test-a');
+ memory.switchTo('test-b');
+ iter = set.keys().next();
+ expect(iter.value).to.deep.equal({ 'b': 1 });
+ memory.switchTo('test-a');
+ memory.switchTo('test-b');
+ iter = set.entries().next();
+ expect(iter.value).to.deep.equal([{ 'b': 1 }, { 'b': 1 }]);
+ });
+ });
+
+ describe('memory slices', () => {
+ it('remove slice and children (check = no error)', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+
+ memory.create('test-0');
+ memory.create('test-b-0');
+ memory.switchTo('test-0');
+ memory.create('test-c-0');
+
+ memory.switchTo('1');
+ memory.remove('test');
+
+ memory.create('test-0');
+ memory.create('test-b-0');
+ memory.create('test-c-0');
+ });
+ it('remove slice avoid memory leak', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.create('2');
+ memory.create('3');
+ const obj = new VersionableObject();
+
+ memory.switchTo('1');
+ const array1 = new VersionableArray();
+ memory.attach(array1);
+
+ array1.push(obj);
+
+ memory.switchTo('2');
+ const array2 = new VersionableArray();
+ memory.attach(array2);
+
+ array2.push(obj);
+
+ memory.switchTo('3');
+
+ expect(memory._proxies[obj[memoryProxyPramsKey].ID]).to.deep.equal(obj);
+
+ memory.remove('1');
+
+ expect(memory._proxies[obj[memoryProxyPramsKey].ID]).to.deep.equal(obj);
+
+ memory.remove('2');
+
+ expect(memory._proxies[obj[memoryProxyPramsKey].ID]).to.deep.equal(undefined);
+ });
+ });
+
+ describe('snapshot', () => {
+ it('create a snapshot from 8 slices', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+ for (let k = 0; k < 8; k++) {
+ memory.create('test-' + k);
+ memory.switchTo('test-' + k);
+ }
+ memory.create('other');
+ memory.switchTo('other');
+ expect(memory.getPath('other').join()).to.deep.equal(
+ 'other,test-7,test-6,test-5,test-4,test-3,test-2,test-1,test-0,test,1',
+ );
+ memory.snapshot('test-0', 'test-7', 'snap');
+ memory.switchTo('snap');
+ memory.create('after-snap');
+
+ expect(memory.getPath('after-snap').join()).to.deep.equal(
+ 'after-snap,snap,test,1',
+ );
+ expect(memory.getPath('after-snap', true).join()).to.deep.equal(
+ 'after-snap,snap,test-6,test-5,test-4,test-3,test-2,test-1,test-0,test,1',
+ );
+ expect(memory.getPath('other').join()).to.deep.equal('other,snap,test,1');
+ expect(memory.getPath('test-7').join()).to.deep.equal(
+ 'test-7,test-6,test-5,test-4,test-3,test-2,test-1,test-0,test,1',
+ );
+ });
+ it('remove slice and children after snapshot (check = no error)', () => {
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.create('test');
+ memory.switchTo('test');
+ for (let k = 0; k < 8; k++) {
+ memory.create('test-' + k);
+ memory.switchTo('test-' + k);
+ }
+ memory.create('other');
+ memory.switchTo('other');
+ memory.snapshot('test-0', 'test-7', 'snap');
+ memory.switchTo('1');
+ memory.remove('test');
+
+ // can re-create wihtout error (tested previously)
+ for (let k = 0; k < 8; k++) {
+ memory.create('test-' + k);
+ memory.switchTo('test-' + k);
+ }
+ memory.create('other');
+ memory.switchTo('other');
+ memory.snapshot('test-0', 'test-7', 'snap');
+ });
+ it('snapshot array changes', () => {
+ const memory = new Memory();
+ memory._numberOfFlatSlices = 20;
+ memory._numberOfSlicePerSnapshot = 8;
+
+ memory.create('1');
+ memory.switchTo('1');
+ const array = new VersionableArray();
+ memory.attach(array);
+ array.push(0);
+
+ memory.create('test');
+ memory.switchTo('test');
+ array.push(1);
+ memory.create('test-0');
+ memory.switchTo('test-0');
+ array.push(2);
+ memory.create('test-1');
+ memory.switchTo('test-1');
+ array.push(3);
+ memory.create('test-2');
+ memory.switchTo('test-2');
+ array.push(4);
+
+ const ID = Object.keys(memory._slices.test.data)[0];
+ let patch = memory._slices.test.data[ID];
+ expect(Object.values(patch.patch)).to.deep.equal([1]);
+
+ memory.snapshot('test', 'test-1', 'snap');
+
+ expect(Object.keys(memory._slices.snap.data).length).to.equal(1);
+ patch = memory._slices.snap.data[ID];
+ expect(Object.values(patch.patch)).to.deep.equal([1, 2, 3]);
+ });
+ it('snapshot set changes', () => {
+ const memory = new Memory();
+ memory._numberOfFlatSlices = 20;
+ memory._numberOfSlicePerSnapshot = 8;
+
+ memory.create('1');
+ memory.switchTo('1');
+ const set = new VersionableSet();
+ memory.attach(set);
+ set.add(0);
+ set.add(-1);
+
+ memory.create('test');
+ memory.switchTo('test');
+ set.delete(-1);
+ set.add(1);
+ set.add(2);
+ memory.create('test-0');
+ memory.switchTo('test-0');
+ set.delete(0);
+ memory.create('test-1');
+ memory.switchTo('test-1');
+ set.delete(2);
+ set.add(3);
+ set.add(-1);
+ memory.create('test-2');
+ memory.switchTo('test-2');
+ set.add(3);
+
+ const ID = Object.keys(memory._slices.test.data)[0];
+ let patch = memory._slices.test.data[ID];
+ expect(patch.add.size).to.deep.equal(2);
+ expect(patch.delete.size).to.deep.equal(1);
+
+ memory.snapshot('test', 'test-1', 'snap');
+
+ expect(Object.keys(memory._slices.snap.data).length).to.equal(1);
+ patch = memory._slices.snap.data[ID];
+
+ const add = [];
+ patch.add.forEach((item: number) => add.push(item));
+ expect(add).to.deep.equal([1, 3]);
+ const remove = [];
+ patch.delete.forEach((item: number) => remove.push(item));
+ expect(remove).to.deep.equal([0]);
+ });
+ it('snapshot object changes', () => {
+ const memory = new Memory();
+ memory._numberOfFlatSlices = 20;
+ memory._numberOfSlicePerSnapshot = 8;
+
+ memory.create('1');
+ memory.switchTo('1');
+ const obj = new VersionableObject();
+ memory.attach(obj);
+ obj['x+y'] = 1;
+
+ memory.create('test');
+ memory.switchTo('test');
+ obj['a-z'] = new VersionableObject();
+ const ID1 = obj['a-z'][memoryProxyPramsKey].ID;
+ obj['x+y'] = 2;
+ memory.create('test-0');
+ memory.switchTo('test-0');
+ obj['a+b'] = 0;
+ memory.create('test-1');
+ memory.switchTo('test-1');
+ obj['a-z'] = new VersionableArray();
+ const ID2 = obj['a-z'][memoryProxyPramsKey].ID;
+ obj['a+b'] = 3;
+ obj['x+y'] = 1;
+ memory.create('test-2');
+ memory.switchTo('test-2');
+ obj['x+y'] = 10;
+ obj['w+u'] = 10;
+
+ const ID = Object.keys(memory._slices.test.data)[0];
+ let patch = memory._slices.test.data[ID];
+
+ expect(patch).to.deep.equal({ props: { 'a-z': ID1, 'x+y': 2 } });
+
+ memory.snapshot('test', 'test-1', 'snap');
+
+ expect(Object.keys(memory._slices.snap.data).length).to.equal(1);
+ patch = memory._slices.snap.data[ID];
+
+ expect(patch).to.deep.equal({
+ props: { 'a-z': ID2, 'x+y': 1, 'a+b': 3 },
+ });
+ });
+ it('auto-snapshot', () => {
+ const memory = new Memory();
+ memory._numberOfFlatSlices = 20;
+ memory._numberOfSlicePerSnapshot = 8;
+
+ const array = new VersionableArray();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(array);
+
+ for (let k = 0; k < 100; k++) {
+ memory.create('test-' + k);
+ memory.switchTo('test-' + k);
+ array.push(k);
+ }
+
+ const sliceKeys = Object.keys(memory._slices);
+ expect(sliceKeys.length).to.equal(1 + 100 + (100 - 20) / 8);
+ });
+ it('compress array changes', () => {
+ const memory = new Memory();
+ memory._numberOfFlatSlices = 20;
+ memory._numberOfSlicePerSnapshot = 8;
+
+ memory.create('1');
+ memory.switchTo('1');
+ const array = new VersionableArray();
+ memory.attach(array);
+ array.push(0);
+
+ memory.create('test');
+ memory.switchTo('test');
+ array.push(1);
+ memory.create('test-0');
+ memory.switchTo('test-0');
+ array.push(2);
+ const obj = new VersionableObject();
+ memory.attach(obj);
+ obj['x+y'] = 3;
+ memory.create('test-1');
+ memory.switchTo('test-1');
+ array.push(3);
+ memory.create('test-2');
+ memory.switchTo('test-2');
+ array.push(4);
+
+ const ID = Object.keys(memory._slices.test.data)[0];
+ let patch = memory._slices.test.data[ID];
+ expect(Object.values(patch.patch)).to.deep.equal([1]);
+ expect(Object.keys(memory._slices).length).to.equal(6);
+
+ memory.compress('test', 'test-1');
+
+ expect(Object.keys(memory._slices).length).to.equal(4);
+ expect(Object.keys(memory._slices.test.data).length).to.equal(2);
+ patch = memory._slices.test.data[ID];
+ expect(Object.values(patch.patch)).to.deep.equal([1, 2, 3]);
+ patch = memory._slices.test.data[obj[memoryProxyPramsKey].ID];
+ expect(patch).to.deep.equal({ props: { 'x+y': 3 } });
+ });
+ });
+
+ describe('root & path', () => {
+ it('link to memory & markAsDiffRoot', () => {
+ const obj = makeVersionable({
+ toto: {
+ titi: {
+ m: 6,
+ },
+ },
+ tutu: {
+ a: 1,
+ },
+ });
+ markAsDiffRoot(obj.toto);
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(obj);
+ expect([...memory.getRoots(obj.toto.titi)]).to.deep.equal([obj.toto], 'titi');
+ expect([...memory.getRoots(obj.toto)]).to.deep.equal([obj.toto], 'toto');
+ expect([...memory.getRoots(obj.tutu)]).to.deep.equal([obj], 'tutu');
+ });
+ it('link to memory & markAsDiffRoot with change slice', () => {
+ const obj = makeVersionable({
+ toto: {
+ titi: {
+ m: 6,
+ },
+ tata: {},
+ },
+ tutu: {
+ a: 1,
+ },
+ });
+ markAsDiffRoot(obj.toto);
+ const memory = new Memory();
+ memory.create('1');
+ memory.switchTo('1');
+ memory.attach(obj);
+ memory.create('2');
+ memory.switchTo('2');
+ obj.toto.tata = obj.tutu;
+
+ expect(obj.toto.tata).to.deep.equal(obj.tutu);
+
+ expect([...memory.getRoots(obj.toto.titi)]).to.deep.equal([obj.toto], 'titi');
+ expect([...memory.getRoots(obj.toto)]).to.deep.equal([obj.toto], 'toto');
+ const test1 = [];
+ memory.getRoots(obj.tutu).forEach(obj => test1.push(obj));
+ expect(test1).to.deep.equal([obj.toto, obj], 'tutu');
+ const test2 = [];
+ memory.getRoots(obj.toto.tata).forEach(obj => test2.push(obj));
+ expect(test2).to.deep.equal([obj.toto, obj], 'tata');
+ });
+ it('should get the changed object', () => {
+ const tata = {
+ m: 6,
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const obj: any = makeVersionable({
+ toto: {
+ titi: {
+ tata: tata,
+ },
+ },
+ tutu: {
+ a: 1,
+ tata: tata,
+ },
+ vroum: {
+ b: 2,
+ },
+ array: [
+ { x: 1 },
+ { x: 2 },
+ { x: 3 },
+ { x: 4 },
+ { x: 5 },
+ { x: 6 },
+ { x: 7 },
+ { x: 8 },
+ ],
+ });
+ markAsDiffRoot(obj.toto);
+ markAsDiffRoot(obj.tutu);
+ const memory = new Memory();
+ memory.create('test');
+ memory.switchTo('test');
+ memory.attach(obj);
+ memory.create('test-1');
+ memory.switchTo('test-1');
+
+ expect(memory.getChangesLocations('test', 'test-1')).to.deep.equal({
+ add: [],
+ move: [],
+ remove: [],
+ update: [],
+ });
+
+ obj.toto.titi.tata.m = 4;
+ obj.tutu.a = 6;
+ const vroum = obj.vroum;
+ delete obj.vroum;
+ const x2 = obj.array[1];
+ obj.array.splice(1, 1);
+ obj.array.splice(3, 0, makeVersionable({ z: 1 }));
+ obj.yop = makeVersionable({ a: 1 });
+
+ expect(memory.getChangesLocations('test', 'test-1')).to.deep.equal({
+ add: [obj.array[3], obj.yop],
+ move: [],
+ remove: [vroum, x2],
+ update: [
+ [obj, ['vroum', 'yop']],
+ [obj.tutu, ['a']],
+ [obj.array, [1, 3]],
+ [obj.toto.titi.tata, ['m']],
+ ],
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/packages/core/src/Modifier.ts b/packages/core/src/Modifier.ts
index 04abbe680..b252ef862 100644
--- a/packages/core/src/Modifier.ts
+++ b/packages/core/src/Modifier.ts
@@ -1,13 +1,11 @@
import { Constructor } from '../../utils/src/utils';
import { VNode } from './VNodes/VNode';
+import { VersionableObject } from './Memory/VersionableObject';
-export type ModifierTypeguard = (
- modifier: Modifier,
- batch: VNode[],
-) => modifier is T;
+export type ModifierTypeguard = (modifier: Modifier) => modifier is T;
export type ModifierPredicate = T extends Modifier
? Constructor | ModifierTypeguard
- : (modifier: Modifier, batch: VNode[]) => boolean;
+ : (modifier: Modifier) => boolean;
interface ModifierConstructor {
new >(...args: ConstructorParameters): this;
@@ -15,7 +13,7 @@ interface ModifierConstructor {
export interface Modifier {
constructor: ModifierConstructor & this;
}
-export class Modifier {
+export class Modifier extends VersionableObject {
preserve = true;
get name(): string {
return '';
diff --git a/packages/core/src/Modifiers.ts b/packages/core/src/Modifiers.ts
index 866d9ce0b..6c225997f 100644
--- a/packages/core/src/Modifiers.ts
+++ b/packages/core/src/Modifiers.ts
@@ -1,9 +1,12 @@
import { Modifier } from './Modifier';
import { Constructor, isConstructor } from '../../utils/src/utils';
+import { VersionableObject } from './Memory/VersionableObject';
+import { VersionableArray } from './Memory/VersionableArray';
-export class Modifiers {
+export class Modifiers extends VersionableObject {
private _contents: Modifier[];
constructor(...modifiers: Array>) {
+ super();
const clonedModifiers = modifiers.map(mod => {
return mod instanceof Modifier ? mod.clone() : mod;
});
@@ -49,7 +52,7 @@ export class Modifiers {
*/
append(...modifiers: Array>): void {
if (modifiers.length && !this._contents) {
- this._contents = [];
+ this._contents = new VersionableArray();
}
for (const modifier of modifiers) {
if (modifier instanceof Modifier) {
@@ -67,7 +70,7 @@ export class Modifiers {
*/
prepend(...modifiers: Array>): void {
if (modifiers.length && !this._contents) {
- this._contents = [];
+ this._contents = new VersionableArray();
}
for (const modifier of [...modifiers].reverse()) {
if (modifier instanceof Modifier) {
diff --git a/packages/core/src/VNodes/AbstractNode.ts b/packages/core/src/VNodes/AbstractNode.ts
index 109eb447c..3a9b28eae 100644
--- a/packages/core/src/VNodes/AbstractNode.ts
+++ b/packages/core/src/VNodes/AbstractNode.ts
@@ -1,4 +1,4 @@
-import { VNode, RelativePosition, Predicate, Typeguard, isLeaf } from './VNode';
+import { VNode, RelativePosition, Predicate, isLeaf } from './VNode';
import {
Constructor,
nodeLength,
@@ -10,6 +10,7 @@ import { AtomicNode } from './AtomicNode';
import { Modifiers } from '../Modifiers';
import { EventMixin } from '../../../utils/src/EventMixin';
import { Modifier } from '../Modifier';
+import { markAsDiffRoot } from '../Memory/Memory';
export interface AbstractNodeParams {
modifiers?: Modifiers | Array>;
@@ -47,7 +48,7 @@ export abstract class AbstractNode extends EventMixin {
* Can be overridden with a `Mode`.
*/
breakable = true;
- parent: VNode;
+ parent: ContainerNode;
modifiers = new Modifiers();
childVNodes: VNode[];
/**
@@ -71,6 +72,7 @@ export abstract class AbstractNode extends EventMixin {
this.modifiers.append(...params.modifiers);
}
}
+ markAsDiffRoot(this);
}
get name(): string {
@@ -141,20 +143,6 @@ export abstract class AbstractNode extends EventMixin {
get length(): number {
return this.children().length;
}
- /**
- * Return whether this node is an instance of the given VNode class.
- *
- * @param predicate The subclass of VNode to test this node against.
- */
- is(predicate: Constructor | Typeguard): this is T;
- is(predicate: Predicate): false;
- is(predicate: Predicate): boolean {
- if (AbstractNode.isConstructor(predicate)) {
- return this instanceof predicate;
- } else {
- return predicate(this as VNode);
- }
- }
/**
* Test this node against the given predicate.
*
@@ -728,6 +716,7 @@ export abstract class AbstractNode extends EventMixin {
return __repr;
}
}
+
export interface AbstractNode {
constructor: new >(...args: ConstructorParameters) => this;
}
diff --git a/packages/core/src/VNodes/ContainerNode.ts b/packages/core/src/VNodes/ContainerNode.ts
index 3edffa626..972604e69 100644
--- a/packages/core/src/VNodes/ContainerNode.ts
+++ b/packages/core/src/VNodes/ContainerNode.ts
@@ -1,10 +1,10 @@
import { AbstractNode } from './AbstractNode';
import { VNode, Predicate, isLeaf } from './VNode';
import { ChildError } from '../../../utils/src/errors';
+import { VersionableArray } from '../Memory/VersionableArray';
export class ContainerNode extends AbstractNode {
- parent: ContainerNode;
- readonly childVNodes: VNode[] = [];
+ readonly childVNodes: VNode[] = new VersionableArray();
//--------------------------------------------------------------------------
// Browsing children.
@@ -16,15 +16,19 @@ export class ContainerNode extends AbstractNode {
children(predicate?: Predicate): T[];
children(predicate?: Predicate): VNode[];
children(predicate?: Predicate): VNode[] {
- return this.childVNodes.filter(child => {
- return child.tangible && child.test(predicate);
+ const children: VNode[] = [];
+ this.childVNodes.forEach(child => {
+ if (child.tangible && (!predicate || child.test(predicate))) {
+ children.push(child);
+ }
});
+ return children;
}
/**
* See {@link AbstractNode.hasChildren}.
*/
hasChildren(): boolean {
- return this.children().length > 0;
+ return !!this.childVNodes.find(child => child.tangible);
}
/**
* See {@link AbstractNode.nthChild}.
@@ -39,7 +43,7 @@ export class ContainerNode extends AbstractNode {
firstChild(predicate?: Predicate): VNode;
firstChild(predicate?: Predicate): VNode {
let child = this.childVNodes[0];
- while (child && !(child.tangible && child.test(predicate))) {
+ while (child && !(child.tangible && (!predicate || child.test(predicate)))) {
child = child.nextSibling();
}
return child;
@@ -51,7 +55,7 @@ export class ContainerNode extends AbstractNode {
lastChild(predicate?: Predicate): VNode;
lastChild(predicate?: Predicate): VNode {
let child = this.childVNodes[this.childVNodes.length - 1];
- while (child && !(child.tangible && child.test(predicate))) {
+ while (child && !(child.tangible && (!predicate || child.test(predicate)))) {
child = child.previousSibling();
}
return child;
@@ -63,7 +67,7 @@ export class ContainerNode extends AbstractNode {
firstLeaf(predicate?: Predicate): VNode;
firstLeaf(predicate?: Predicate): VNode {
const isValidLeaf = (node: VNode): boolean => {
- return isLeaf(node) && node.test(predicate);
+ return isLeaf(node) && (!predicate || node.test(predicate));
};
if (isValidLeaf(this)) {
return this;
@@ -78,7 +82,7 @@ export class ContainerNode extends AbstractNode {
lastLeaf(predicate?: Predicate): VNode;
lastLeaf(predicate?: Predicate): VNode {
const isValidLeaf = (node: VNode): boolean => {
- return isLeaf(node) && node.test(predicate);
+ return isLeaf(node) && (!predicate || node.test(predicate));
};
if (isValidLeaf(this)) {
return this;
@@ -93,7 +97,7 @@ export class ContainerNode extends AbstractNode {
firstDescendant(predicate?: Predicate): VNode;
firstDescendant(predicate?: Predicate): VNode {
let firstDescendant = this.firstChild();
- while (firstDescendant && !firstDescendant.test(predicate)) {
+ while (firstDescendant && predicate && !firstDescendant.test(predicate)) {
firstDescendant = this._descendantAfter(firstDescendant);
}
return firstDescendant;
@@ -108,7 +112,7 @@ export class ContainerNode extends AbstractNode {
while (lastDescendant && lastDescendant.hasChildren()) {
lastDescendant = lastDescendant.lastChild();
}
- while (lastDescendant && !lastDescendant.test(predicate)) {
+ while (lastDescendant && predicate && !lastDescendant.test(predicate)) {
lastDescendant = this._descendantBefore(lastDescendant);
}
return lastDescendant;
@@ -123,7 +127,7 @@ export class ContainerNode extends AbstractNode {
const stack = [...this.childVNodes];
while (stack.length) {
const node = stack.shift();
- if (node.tangible && node.test(predicate)) {
+ if (node.tangible && (!predicate || node.test(predicate))) {
descendants.push(node);
}
if (node instanceof ContainerNode) {
@@ -226,9 +230,11 @@ export class ContainerNode extends AbstractNode {
return this;
}
const duplicate = this.clone();
- const index = child.parent.childVNodes.indexOf(child);
- while (this.childVNodes.length > index) {
- duplicate.append(this.childVNodes[index]);
+ const index = this.childVNodes.indexOf(child);
+ const children = this.childVNodes.splice(index);
+ duplicate.childVNodes.push(...children);
+ for (const child of children) {
+ child.parent = duplicate;
}
this.after(duplicate);
return duplicate;
diff --git a/packages/core/src/VRange.ts b/packages/core/src/VRange.ts
index 30aefab4d..a93df222a 100644
--- a/packages/core/src/VRange.ts
+++ b/packages/core/src/VRange.ts
@@ -240,7 +240,7 @@ export class VRange {
} else if (position === RelativePosition.AFTER) {
reference = reference.lastLeaf();
}
- if (reference.is(ContainerNode) && !reference.hasChildren()) {
+ if (reference instanceof ContainerNode && !reference.hasChildren()) {
reference.prepend(this.start);
} else if (position === RelativePosition.AFTER && reference !== this.end) {
// We check that `reference` isn't `this.end` to avoid a backward
@@ -266,7 +266,7 @@ export class VRange {
} else if (position === RelativePosition.AFTER) {
reference = reference.lastLeaf();
}
- if (reference.is(ContainerNode) && !reference.hasChildren()) {
+ if (reference instanceof ContainerNode && !reference.hasChildren()) {
reference.append(this.end);
} else if (position === RelativePosition.BEFORE && reference !== this.start) {
// We check that `reference` isn't `this.start` to avoid a backward
@@ -366,7 +366,7 @@ export class VRange {
empty(): void {
const removableNodes = this.selectedNodes(node => {
// TODO: Replace Table check with complex table selection support.
- return this.mode.is(node, RuleProperty.EDITABLE) && !node.is(TableCellNode);
+ return this.mode.is(node, RuleProperty.EDITABLE) && !(node instanceof TableCellNode);
});
// Remove selected nodes without touching the start range's ancestors.
const startAncestors = this.start.ancestors();
@@ -407,23 +407,3 @@ export class VRange {
this.end.remove();
}
}
-
-/**
- * Create a temporary range corresponding to the given boundary points and
- * call the given callback with the newly created range as argument. The
- * range is automatically destroyed after calling the callback.
- *
- * @param bounds The points corresponding to the range boundaries.
- * @param callback The callback to call with the newly created range.
- */
-export async function withRange(
- editor: JWEditor,
- bounds: [Point, Point],
- callback: (range: VRange) => T,
- mode?: Mode,
-): Promise {
- const range = new VRange(editor, bounds, mode);
- const result = await callback(range);
- range.remove();
- return result;
-}
diff --git a/packages/core/test/ContextManager.test.ts b/packages/core/test/ContextManager.test.ts
index 91062c081..20624fd26 100644
--- a/packages/core/test/ContextManager.test.ts
+++ b/packages/core/test/ContextManager.test.ts
@@ -288,16 +288,18 @@ describe('core', () => {
contentBefore:
'' +
'',
- stepFunction: (editor: BasicEditor) => {
+ stepFunction: async (editor: BasicEditor) => {
const callback = (): void => {};
const check = (context: CheckingContext): boolean => !!context;
const checkSpy1 = sinon.spy(check);
const checkSpy2 = sinon.spy(check);
const newSelection = new VSelection(editor);
- const domEngine = editor.plugins.get(Layout).engines.dom;
- const editable = domEngine.components.get('editable')[0];
- newSelection.setAt(editable);
+ await editor.execCommand(async () => {
+ const domEngine = editor.plugins.get(Layout).engines.dom;
+ const editable = domEngine.components.get('editable')[0];
+ newSelection.setAt(editable);
+ });
const commands: CommandImplementation[] = [
{
title: 'paragraph',
diff --git a/packages/core/test/JWeditor.test.ts b/packages/core/test/JWeditor.test.ts
index 02b7a53c0..1fc5199ae 100644
--- a/packages/core/test/JWeditor.test.ts
+++ b/packages/core/test/JWeditor.test.ts
@@ -234,7 +234,7 @@ describe('core', () => {
await testEditor(BasicEditor, {
contentBefore: 'ab[]
',
stepFunction: async editor => {
- await editor.execCustomCommand(async () => {
+ await editor.execCommand(async () => {
const layout = editor.plugins.get(Layout);
const domEngine = layout.engines.dom;
domEngine.components
diff --git a/packages/core/test/MemoryVDocument.perf.ts b/packages/core/test/MemoryVDocument.perf.ts
new file mode 100644
index 000000000..f1c269394
--- /dev/null
+++ b/packages/core/test/MemoryVDocument.perf.ts
@@ -0,0 +1,112 @@
+/* eslint-disable max-nested-callbacks */
+import { expect } from 'chai';
+import { BasicEditor } from '../../bundle-basic-editor/BasicEditor';
+import { DomEditable } from '../../plugin-dom-editable/src/DomEditable';
+import { DomLayout } from '../../plugin-dom-layout/src/DomLayout';
+import { Layout } from '../../plugin-layout/src/Layout';
+import { DomLayoutEngine } from '../../plugin-dom-layout/src/DomLayoutEngine';
+
+describe('test performances', () => {
+ describe('stores', () => {
+ describe('Memory / VDocument', () => {
+ let wrapper: HTMLElement;
+ let editor: BasicEditor;
+
+ beforeEach(async () => {
+ wrapper = document.createElement('test-wrapper');
+ wrapper.style.display = 'block';
+ document.body.appendChild(wrapper);
+ const root = document.createElement('div');
+ root.innerHTML = `Jabberwocky
+ by Lewis Carroll
+ ’Twas brillig, and the slithy toves
+ Did gyre and gimble in the wabe:
+ All mimsy were the borogoves,
+ And the mome raths outgrabe.
+
+ “Beware the Jabberwock, my son!
+ The jaws that bite, the claws that catch!
+ Beware the Jubjub bird, and shun
+ The frumious Bandersnatch!”
+
+ He took his vorpal sword in hand;
+ Long time the manxome foe he sought—
+ So rested he by the Tumtum tree
+ And stood awhile in thought.
+
+ And, as in uffish thought he stood,
+ The Jabberwock, with eyes of flame,
+ Came whiffling through the tulgey wood,
+ And burbled as it came!
+
+ One, two! One, two! And through and through
+ The vorpal blade went snicker-snack!
+ He left it dead, and with its head
+ He went galumphing back.
+
+ “And hast thou slain the Jabberwock?
+ Come to my arms, my beamish boy!
+ O frabjous day! Callooh! Callay!”
+ He chortled in his joy.
+
+ ’Twas brillig, and the slithy toves
+ Did gyre and gimble in the wabe:
+ All mimsy were the borogoves,
+ And the mome raths outgrabe.
`;
+ wrapper.appendChild(root);
+
+ editor = new BasicEditor();
+ editor.configure(DomLayout, { location: [root, 'replace'] });
+ editor.configure(DomEditable, { source: root });
+ await editor.start();
+ });
+ afterEach(async () => {
+ editor.stop();
+ document.body.removeChild(wrapper);
+ });
+
+ it('should split a paragraph in two', async () => {
+ // Parse the editable in the internal format of the editor.
+ const memory = editor.memory;
+ const domEngine = editor.plugins.get(Layout).engines.dom as DomLayoutEngine;
+ const editable = domEngine.components.get('editable')[0];
+ editor.selection.setAt(editable.children()[2].children()[500]);
+
+ memory.attach(editable);
+ memory.create('root').switchTo('root');
+
+ expect(editable.children().length).to.equal(3);
+ memory.create('test').switchTo('test');
+ await editor.execCommand('insertParagraphBreak');
+ expect(editable.children().length).to.equal(4);
+
+ const t1 = [];
+ const t2 = [];
+ for (let k = 2; k < 26; k++) {
+ let d = Date.now();
+ memory
+ .switchTo('root')
+ .create(k.toString())
+ .switchTo(k.toString());
+ t1.push(Date.now() - d);
+
+ d = Date.now();
+ await editor.execCommand('insertParagraphBreak');
+ t2.push(Date.now() - d);
+ }
+
+ // We remove the first load because it does not represent time in
+ // use. In fact, time is much longer because the functions and
+ // object are not yet loaded. The loading test is done separately.
+ t1.shift();
+ t2.shift();
+
+ const averageInsert = Math.round(t2.reduce((a, b) => a + b) / t2.length);
+ expect(averageInsert).to.lessThan(30, 'Time to compute the insert paragraph');
+
+ const averageSwitch = Math.round(t1.reduce((a, b) => a + b) / t1.length);
+ expect(averageSwitch).to.lessThan(1, 'Time to switch the memory');
+ });
+ });
+ });
+});
diff --git a/packages/core/test/Mode.test.ts b/packages/core/test/Mode.test.ts
index 5b1d7ba6a..1749a4c93 100644
--- a/packages/core/test/Mode.test.ts
+++ b/packages/core/test/Mode.test.ts
@@ -2,7 +2,7 @@ import JWEditor from '../src/JWEditor';
import { ContainerNode } from '../src/VNodes/ContainerNode';
import { expect } from 'chai';
import { ModeDefinition, Mode, RuleProperty } from '../src/Mode';
-import { withRange, VRange } from '../src/VRange';
+import { VRange } from '../src/VRange';
describe('core', () => {
describe('Modes', () => {
@@ -66,12 +66,11 @@ describe('core', () => {
],
});
// With default mode.
- await withRange(editor, VRange.at(root), async range => {
+ await editor.withRange(VRange.at(root), async range => {
expect(range.mode.is(range.startContainer, RuleProperty.EDITABLE)).to.be.true;
});
// With special mode
- await withRange(
- editor,
+ await editor.withRange(
VRange.at(root),
async range => {
expect(range.mode.is(range.startContainer, RuleProperty.EDITABLE)).to.be
diff --git a/packages/core/test/VDocument.test.ts b/packages/core/test/VDocument.test.ts
index b130c552e..5e442fa11 100644
--- a/packages/core/test/VDocument.test.ts
+++ b/packages/core/test/VDocument.test.ts
@@ -1,14 +1,15 @@
import JWEditor from '../src/JWEditor';
import { testEditor } from '../../utils/src/testUtils';
import { Char } from '../../plugin-char/src/Char';
-import { VRange, withRange } from '../src/VRange';
+import { VRange } from '../src/VRange';
import { RelativePosition, Point } from '../src/VNodes/VNode';
import { BasicEditor } from '../../bundle-basic-editor/BasicEditor';
import { Core } from '../../core/src/Core';
import { Layout } from '../../plugin-layout/src/Layout';
-const deleteForward = async (editor: JWEditor): Promise =>
+const deleteForward = async (editor: JWEditor): Promise => {
await editor.execCommand('deleteForward');
+};
const deleteBackward = async (editor: JWEditor): Promise =>
await editor.execCommand('deleteBackward');
const insertParagraphBreak = async (editor: JWEditor): Promise =>
@@ -555,9 +556,11 @@ describe('VDocument', () => {
stepFunction: (editor: JWEditor) => {
const domEngine = editor.plugins.get(Layout).engines.dom;
const editable = domEngine.components.get('editable')[0];
- editable.firstChild().breakable = false;
- editable.lastChild().breakable = false;
- return deleteForward(editor);
+ return editor.execCommand(() => {
+ editable.firstChild().breakable = false;
+ editable.lastChild().breakable = false;
+ return deleteForward(editor);
+ });
},
contentAfter: 'a[
]f
',
});
@@ -1523,7 +1526,7 @@ describe('VDocument', () => {
const domEngine = editor.plugins.get(Layout).engines.dom;
const editable = domEngine.components.get('editable')[0];
const aNode = editable.next(node => node.name === 'a');
- await withRange(editor, VRange.at(aNode), async range => {
+ await editor.withRange(VRange.at(aNode), async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1542,7 +1545,7 @@ describe('VDocument', () => {
const domEngine = editor.plugins.get(Layout).engines.dom;
const editable = domEngine.components.get('editable')[0];
const aNode = editable.next(node => node.name === 'a');
- await withRange(editor, VRange.selecting(aNode, aNode), async range => {
+ await editor.withRange(VRange.selecting(aNode, aNode), async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1565,7 +1568,7 @@ describe('VDocument', () => {
[aNode, RelativePosition.BEFORE],
[aNode, RelativePosition.BEFORE],
];
- await withRange(editor, rangeParams, async range => {
+ await editor.withRange(rangeParams, async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1588,7 +1591,7 @@ describe('VDocument', () => {
[aNode, RelativePosition.BEFORE],
[aNode, RelativePosition.AFTER],
];
- await withRange(editor, rangeParams, async range => {
+ await editor.withRange(rangeParams, async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1611,7 +1614,7 @@ describe('VDocument', () => {
[aNode, RelativePosition.AFTER],
[aNode, RelativePosition.AFTER],
];
- await withRange(editor, rangeParams, async range => {
+ await editor.withRange(rangeParams, async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1635,7 +1638,7 @@ describe('VDocument', () => {
[aNode, RelativePosition.AFTER],
[bNode, RelativePosition.BEFORE],
];
- await withRange(editor, rangeParams, async range => {
+ await editor.withRange(rangeParams, async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
@@ -1659,7 +1662,7 @@ describe('VDocument', () => {
[aNode, RelativePosition.AFTER],
[bNode, RelativePosition.AFTER],
];
- await withRange(editor, rangeParams, async range => {
+ await editor.withRange(rangeParams, async range => {
await editor.execCommand('insertText', {
text: 'c',
context: {
diff --git a/packages/core/test/VNodes.test.ts b/packages/core/test/VNodes.test.ts
index 898fc25b0..a061fdb44 100644
--- a/packages/core/test/VNodes.test.ts
+++ b/packages/core/test/VNodes.test.ts
@@ -30,7 +30,7 @@ describe('core', () => {
it('should create an unknown element', async () => {
for (let i = 1; i <= 6; i++) {
const vNode = new VElement({ htmlTag: 'UNKNOWN-ELEMENT' });
- expect(vNode.is(AtomicNode)).to.equal(false);
+ expect(vNode instanceof AtomicNode).to.equal(false);
expect(vNode.htmlTag).to.equal('UNKNOWN-ELEMENT');
}
});
@@ -49,7 +49,7 @@ describe('core', () => {
it('should create a marker node', async () => {
const markerNode = new MarkerNode();
expect(markerNode.tangible).to.equal(false);
- expect(markerNode.is(AtomicNode)).to.equal(true);
+ expect(markerNode instanceof AtomicNode).to.equal(true);
});
});
});
@@ -95,11 +95,11 @@ describe('core', () => {
describe('constructor', () => {
it('should create an AtomicNode', async () => {
const atomic = new AtomicNode();
- expect(atomic.is(AtomicNode)).to.equal(true);
+ expect(atomic instanceof AtomicNode).to.equal(true);
});
it('should create a ContainerNode', async () => {
const container = new ContainerNode();
- expect(container.is(AtomicNode)).to.equal(false);
+ expect(container instanceof AtomicNode).to.equal(false);
});
});
describe('toString', () => {
@@ -930,7 +930,9 @@ describe('core', () => {
const editable = domEngine.components.get('editable')[0];
const a = editable.firstLeaf();
const ancestors = a.ancestors(ancestor => {
- return !ancestor.is(HeadingNode) || ancestor.level !== 1;
+ return (
+ !(ancestor instanceof HeadingNode) || ancestor.level !== 1
+ );
});
expect(ancestors.map(ancestor => ancestor.name)).to.deep.equal([
'ParagraphNode',
@@ -973,7 +975,8 @@ describe('core', () => {
const editable = domEngine.components.get('editable')[0];
const descendants = editable.descendants(
descendant =>
- !descendant.is(HeadingNode) || descendant.level !== 2,
+ !(descendant instanceof HeadingNode) ||
+ descendant.level !== 2,
);
expect(
descendants.map(descendant => descendant.name),
diff --git a/packages/core/test/VRange.test.ts b/packages/core/test/VRange.test.ts
index 6b73f1e34..304061b0c 100644
--- a/packages/core/test/VRange.test.ts
+++ b/packages/core/test/VRange.test.ts
@@ -9,10 +9,12 @@ describe('VRange', () => {
contentBefore: 'abcde[fghijklmno pqr stu vw xy]z ',
stepFunction: async (editor: BasicEditor) => {
editor.dispatcher.registerCommand('refresh', { handler: () => {} });
- const nodes = editor.selection.range.split();
- const domEngine = editor.plugins.get(Layout).engines.dom;
- const editable = domEngine.components.get('editable')[0];
- editable.lastChild().after(nodes[0]);
+ await editor.execCommand(async () => {
+ const nodes = editor.selection.range.split();
+ const domEngine = editor.plugins.get(Layout).engines.dom;
+ const editable = domEngine.components.get('editable')[0];
+ editable.lastChild().after(nodes[0]);
+ });
await editor.execCommand('refresh');
},
contentAfter:
diff --git a/packages/plugin-align/test/Align.test.ts b/packages/plugin-align/test/Align.test.ts
index 98c389500..6a098a576 100644
--- a/packages/plugin-align/test/Align.test.ts
+++ b/packages/plugin-align/test/Align.test.ts
@@ -40,8 +40,10 @@ describePlugin(Align, testEditor => {
const domEngine = editor.plugins.get(Layout).engines.dom;
const editable = domEngine.components.get('editable')[0];
const root = editable;
- root.lastChild().editable = false;
- return align(AlignType.LEFT)(editor);
+ return editor.execCommand(() => {
+ root.lastChild().editable = false;
+ return align(AlignType.LEFT)(editor);
+ });
},
contentAfter: 'ab
c[]d
',
});
diff --git a/packages/plugin-blockquote/src/BlockquoteXmlDomParser.ts b/packages/plugin-blockquote/src/BlockquoteXmlDomParser.ts
index c4e6c64f8..311e3b686 100644
--- a/packages/plugin-blockquote/src/BlockquoteXmlDomParser.ts
+++ b/packages/plugin-blockquote/src/BlockquoteXmlDomParser.ts
@@ -13,7 +13,10 @@ export class BlockquoteXmlDomParser extends AbstractParser {
async parse(item: Element): Promise {
const blockquote = new BlockquoteNode();
- blockquote.modifiers.append(this.engine.parseAttributes(item));
+ const attributes = this.engine.parseAttributes(item);
+ if (attributes.length) {
+ blockquote.modifiers.append(attributes);
+ }
const nodes = await this.engine.parse(...item.childNodes);
blockquote.append(...nodes);
return [blockquote];
diff --git a/packages/plugin-bold/src/BoldXmlDomParser.ts b/packages/plugin-bold/src/BoldXmlDomParser.ts
index 63cc5352d..dc4b7631c 100644
--- a/packages/plugin-bold/src/BoldXmlDomParser.ts
+++ b/packages/plugin-bold/src/BoldXmlDomParser.ts
@@ -15,7 +15,10 @@ export class BoldXmlDomParser extends FormatXmlDomParser {
*/
async parse(item: Element): Promise {
const bold = new BoldFormat(nodeName(item) as 'B' | 'STRONG');
- bold.modifiers.append(this.engine.parseAttributes(item));
+ const attributes = this.engine.parseAttributes(item);
+ if (attributes.length) {
+ bold.modifiers.append(attributes);
+ }
const children = await this.engine.parse(...item.childNodes);
this.applyFormat(bold, children);
diff --git a/packages/plugin-char/src/Char.ts b/packages/plugin-char/src/Char.ts
index ebf028a86..a36319208 100644
--- a/packages/plugin-char/src/Char.ts
+++ b/packages/plugin-char/src/Char.ts
@@ -50,10 +50,18 @@ export class Char extends JWPlugin
let modifiers = inline.getCurrentModifiers(range);
// Ony preserved modifiers are applied at the start of a container.
const previousSibling = range.start.previousSibling();
- if (!previousSibling) {
- modifiers = new Modifiers(...modifiers.filter(mod => mod.preserve));
+ if (!previousSibling && modifiers) {
+ const preservedModifiers = modifiers.filter(mod => mod.preserve);
+ if (preservedModifiers.length) {
+ modifiers = new Modifiers(...preservedModifiers);
+ } else {
+ modifiers = null;
+ }
}
if (params.formats) {
+ if (!modifiers) {
+ modifiers = new Modifiers();
+ }
modifiers.set(...params.formats.map(format => format.clone()));
}
const style = inline.getCurrentStyle(range);
@@ -64,10 +72,14 @@ export class Char extends JWPlugin
// Split the text into CHAR nodes and insert them at the range.
const characters = text.split('');
const charNodes = characters.map(char => {
- return new CharNode({ char: char, modifiers: modifiers.clone() });
+ if (modifiers) {
+ return new CharNode({ char: char, modifiers: modifiers.clone() });
+ } else {
+ return new CharNode({ char: char });
+ }
});
charNodes.forEach(charNode => {
- if (style.length) {
+ if (style?.length) {
charNode.modifiers.get(Attributes).style = style;
}
range.start.before(charNode);
@@ -75,6 +87,5 @@ export class Char extends JWPlugin
if (params.select && charNodes.length) {
this.editor.selection.select(charNodes[0], charNodes[charNodes.length - 1]);
}
- inline.resetCache();
}
}
diff --git a/packages/plugin-char/src/CharDomObjectRenderer.ts b/packages/plugin-char/src/CharDomObjectRenderer.ts
index c356feb42..800875cde 100644
--- a/packages/plugin-char/src/CharDomObjectRenderer.ts
+++ b/packages/plugin-char/src/CharDomObjectRenderer.ts
@@ -6,22 +6,29 @@ import {
} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
import { NodeRenderer } from '../../plugin-renderer/src/NodeRenderer';
import { Predicate } from '../../core/src/VNodes/VNode';
+import { RenderingEngineWorker } from '../../plugin-renderer/src/RenderingEngineCache';
export class CharDomObjectRenderer extends NodeRenderer {
static id = DomObjectRenderingEngine.id;
engine: DomObjectRenderingEngine;
predicate: Predicate = CharNode;
- async render(charNode: CharNode): Promise {
- return this._renderText([charNode]);
+ async render(charNode: CharNode, worker: RenderingEngineWorker): Promise {
+ return this._renderText([charNode], worker);
}
- async renderBatch(charNodes: CharNode[]): Promise {
+ async renderBatch(
+ charNodes: CharNode[],
+ worker: RenderingEngineWorker,
+ ): Promise {
const domObjects: DomObject[] = [];
- const domObject = this._renderText(charNodes);
+ const domObject = this._renderText(charNodes, worker);
for (let i = 0; i < charNodes.length; i++) domObjects.push(domObject);
return domObjects;
}
- private _renderText(charNodes: CharNode[]): DomObject {
+ private _renderText(
+ charNodes: CharNode[],
+ worker: RenderingEngineWorker,
+ ): DomObject {
// Create textObject.
const texts = [];
for (const charNode of charNodes) {
@@ -36,15 +43,15 @@ export class CharDomObjectRenderer extends NodeRenderer {
// Render block edge spaces as non-breakable space (otherwise browsers
// won't render them).
const previous = charNodes[0].previousSibling();
- if (!previous || !previous.is(InlineNode)) {
+ if (!previous || !(previous instanceof InlineNode)) {
texts[0] = texts[0].replace(/^ /g, '\u00A0');
}
const next = charNodes[charNodes.length - 1].nextSibling();
- if (!next || !next.is(InlineNode)) {
+ if (!next || !(next instanceof InlineNode)) {
texts[texts.length - 1] = texts[texts.length - 1].replace(/^ /g, '\u00A0');
}
const textObject = { text: texts.join('') };
- this.engine.locate(charNodes, textObject);
+ worker.locate(charNodes, textObject);
return textObject;
}
}
diff --git a/packages/plugin-char/test/Char.test.ts b/packages/plugin-char/test/Char.test.ts
index 71ae26d2a..6de1c4d52 100644
--- a/packages/plugin-char/test/Char.test.ts
+++ b/packages/plugin-char/test/Char.test.ts
@@ -60,14 +60,14 @@ describePlugin(Char, testEditor => {
it('should create a CharNode', async () => {
const c = new CharNode({ char: ' ' });
expect(c.char).to.equal(' ');
- expect(c.is(AtomicNode)).to.equal(true);
+ expect(c instanceof AtomicNode).to.equal(true);
expect(c.modifiers.length).to.equal(0);
expect(c.length).to.equal(1);
});
it('should create a CharNode with format', async () => {
const c = new CharNode({ char: ' ', modifiers: new Modifiers(BoldFormat) });
expect(c.char).to.equal(' ');
- expect(c.is(AtomicNode)).to.equal(true);
+ expect(c instanceof AtomicNode).to.equal(true);
expect(c.modifiers.length).to.equal(1);
expect(c.modifiers.map(m => m.constructor.name)).to.deep.equal(['BoldFormat']);
});
diff --git a/packages/plugin-char/test/CharDomObjectRenderer.test.ts b/packages/plugin-char/test/CharDomObjectRenderer.test.ts
index a637f02f8..4db80fd06 100644
--- a/packages/plugin-char/test/CharDomObjectRenderer.test.ts
+++ b/packages/plugin-char/test/CharDomObjectRenderer.test.ts
@@ -4,7 +4,10 @@ import { Char } from '../src/Char';
import { CharNode } from '../src/CharNode';
import { Renderer } from '../../plugin-renderer/src/Renderer';
import { ContainerNode } from '../../core/src/VNodes/ContainerNode';
-import { DomObject } from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
+import {
+ DomObject,
+ DomObjectRenderingEngine,
+} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
import { DomLayout } from '../../plugin-dom-layout/src/DomLayout';
import { VNode } from '../../core/src/VNodes/VNode';
@@ -30,13 +33,12 @@ describe('CharDomObjectRenderer', () => {
root.append(new CharNode({ char: 'b' }));
const renderer = editor.plugins.get(Renderer);
- const rendered = await renderer.render('dom/object', char);
+ const engine = renderer.engines['dom/object'] as DomObjectRenderingEngine;
+ const cache = await engine.render(root.childVNodes);
+ const rendered = cache.renderings.get(char);
expect(rendered).to.deep.equal({ text: 'a \u00A0b' });
- const locations = renderer.engines['dom/object'].locations as Map<
- DomObject,
- VNode[]
- >;
+ const locations = cache.locations as Map;
expect(rendered && locations.get(rendered)).to.deep.equal(root.childVNodes);
});
it('should insert 2 spaces and 2 nbsp instead of 4 spaces', async () => {
@@ -50,13 +52,12 @@ describe('CharDomObjectRenderer', () => {
root.append(new CharNode({ char: 'b' }));
const renderer = editor.plugins.get(Renderer);
- const rendered = await renderer.render('dom/object', char);
+ const engine = renderer.engines['dom/object'] as DomObjectRenderingEngine;
+ const cache = await engine.render(root.childVNodes);
+ const rendered = cache.renderings.get(char);
expect(rendered).to.deep.equal({ text: 'a \u00A0 \u00A0b' });
- const locations = renderer.engines['dom/object'].locations as Map<
- DomObject,
- VNode[]
- >;
+ const locations = cache.locations as Map;
expect(rendered && locations.get(rendered)).to.deep.equal(root.children());
});
});
diff --git a/packages/plugin-color/src/Color.ts b/packages/plugin-color/src/Color.ts
index cc3de883b..a44e43c89 100644
--- a/packages/plugin-color/src/Color.ts
+++ b/packages/plugin-color/src/Color.ts
@@ -109,7 +109,7 @@ export class Color extends JWPlugin {
// Uncolor the children and their formats as well.
for (const child of node.children()) {
child.modifiers.find(Attributes)?.style.remove(this.styleName);
- if (child.is(InlineNode)) {
+ if (child instanceof InlineNode) {
for (const format of child.modifiers.filter(Format)) {
format.modifiers.find(Attributes)?.style.remove(this.styleName);
}
diff --git a/packages/plugin-devtools/src/components/DevToolsComponent.ts b/packages/plugin-devtools/src/components/DevToolsComponent.ts
index a97bc4571..ccfc8b84b 100644
--- a/packages/plugin-devtools/src/components/DevToolsComponent.ts
+++ b/packages/plugin-devtools/src/components/DevToolsComponent.ts
@@ -39,7 +39,8 @@ export class DevToolsComponent extends OwlComponent {
_heightOnLastMousedown: number;
async willStart(): Promise {
- this.env.editor.dispatcher.registerCommandHook('*', this.refresh.bind(this));
+ this.env.editor.dispatcher.registerCommandHook('*', this.addCommand.bind(this));
+ this.env.editor.dispatcher.registerCommandHook('@commit', this.render.bind(this));
return super.willStart();
}
willUnmount(): void {
@@ -69,12 +70,10 @@ export class DevToolsComponent extends OwlComponent {
(this.inspectorRef.comp as InspectorComponent)?.inspectDom();
}
/**
- * Refresh this component with respect to the recent dispatching of the
- * given command with the given arguments.
+ * Add the recent dispatching of the given command with the given arguments.
*/
- refresh(params: CommandParams, id: CommandIdentifier): void {
+ addCommand(params: CommandParams, id: CommandIdentifier): void {
this.state.commands.push([id, params]);
- this.render();
}
/**
* Drag the DevTools to resize them
diff --git a/packages/plugin-devtools/src/components/InfoComponent.ts b/packages/plugin-devtools/src/components/InfoComponent.ts
index 440076c30..ee3481aef 100644
--- a/packages/plugin-devtools/src/components/InfoComponent.ts
+++ b/packages/plugin-devtools/src/components/InfoComponent.ts
@@ -50,9 +50,9 @@ export class InfoComponent extends OwlComponent<{}> {
propRepr(vNode: VNode, propName: string): string {
let prop = vNode[propName];
if (propName === 'atomic') {
- if (vNode.is(AtomicNode)) {
+ if (vNode instanceof AtomicNode) {
return 'true';
- } else if (vNode.is(ContainerNode)) {
+ } else if (vNode instanceof ContainerNode) {
return 'false';
} else {
return '?';
diff --git a/packages/plugin-devtools/src/components/InspectorComponent.ts b/packages/plugin-devtools/src/components/InspectorComponent.ts
index 3a4b72de0..49b616602 100644
--- a/packages/plugin-devtools/src/components/InspectorComponent.ts
+++ b/packages/plugin-devtools/src/components/InspectorComponent.ts
@@ -7,6 +7,7 @@ import { Layout } from '../../../plugin-layout/src/Layout';
import { DomLayoutEngine } from '../../../plugin-dom-layout/src/DomLayoutEngine';
import { caretPositionFromPoint } from '../../../utils/src/polyfill';
import { CharNode } from '../../../plugin-char/src/CharNode';
+import { nodeName } from '../../../utils/src/utils';
const hoverStyle = 'box-shadow: inset 0 0 0 100vh rgba(95, 146, 204, 0.5); cursor: pointer;';
@@ -31,9 +32,11 @@ export class InspectorComponent extends OwlComponent {
selectedID: this.domEngine.components.get('editable')[0]?.id || this.domEngine.root.id,
};
selectedNode = this.getNode(this.state.selectedID);
+ private _inspecting = new Set();
constructor(parent?: OwlComponent<{}>, props?: InspectorComponentProps) {
super(parent, props);
+ this._onInspectorMouseEnter = this._onInspectorMouseEnter.bind(this);
this._onInspectorMouseMove = this._onInspectorMouseMove.bind(this);
this._onInspectorMouseLeave = this._onInspectorMouseLeave.bind(this);
this._onInspectorMouseDown = this._onInspectorMouseDown.bind(this);
@@ -126,17 +129,32 @@ export class InspectorComponent extends OwlComponent {
*
* @param ev
*/
- inspectDom(): void {
- window.addEventListener('mousemove', this._onInspectorMouseMove, true);
- window.addEventListener('mouseleave', this._onInspectorMouseLeave, true);
- window.addEventListener('mousedown', this._onInspectorMouseDown, true);
- window.addEventListener('click', this._onInspectorClick, true);
+ inspectDom(doc: Document | ShadowRoot = document): void {
+ if (!this._inspecting.has(doc)) {
+ this._inspecting.add(doc);
+ doc.addEventListener('mouseenter', this._onInspectorMouseEnter, true);
+ doc.addEventListener('mousemove', this._onInspectorMouseMove, true);
+ doc.addEventListener('mouseleave', this._onInspectorMouseLeave, true);
+ doc.addEventListener('mousedown', this._onInspectorMouseDown, true);
+ doc.addEventListener('click', this._onInspectorClick, true);
+ }
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
+ private async _onInspectorMouseEnter(ev: MouseEvent): Promise {
+ const el = ev.target as Element;
+ if (nodeName(el) === 'IFRAME') {
+ const doc = (ev.target as HTMLIFrameElement).contentWindow?.document;
+ if (doc) {
+ this.inspectDom(doc);
+ }
+ } else if (el.shadowRoot) {
+ this.inspectDom(el.shadowRoot);
+ }
+ }
/**
* Add a class to highlight the targeted node (like to VNode).
*
@@ -151,7 +169,11 @@ export class InspectorComponent extends OwlComponent {
this._hoveredTargets = [];
const elements: HTMLElement[] = [];
- for (const node of this._getNodeFromPosition(ev.clientX, ev.clientY)) {
+ for (const node of this._getNodeFromPosition(
+ ev.clientX,
+ ev.clientY,
+ (ev.target as Node).ownerDocument,
+ )) {
for (const domNode of this.domEngine.getDomNodes(node)) {
const element = domNode instanceof HTMLElement ? domNode : domNode.parentElement;
if (!elements.includes(element)) {
@@ -192,15 +214,15 @@ export class InspectorComponent extends OwlComponent {
ev.stopImmediatePropagation();
ev.preventDefault();
}
- private _getNodeFromPosition(clientX: number, clientY: number): VNode[] {
- const caretPosition = caretPositionFromPoint(clientX, clientY);
+ private _getNodeFromPosition(clientX: number, clientY: number, doc: Document): VNode[] {
+ const caretPosition = caretPositionFromPoint(clientX, clientY, doc);
let node = caretPosition?.offsetNode;
let nodes = [];
while (!nodes.length && node) {
nodes = this.domEngine.getNodes(node);
node = node.parentNode;
}
- if (nodes.length && nodes[0].is(CharNode) && nodes[caretPosition.offset]) {
+ if (nodes[0] instanceof CharNode && nodes[caretPosition.offset]) {
return [nodes[caretPosition.offset]];
}
return nodes;
@@ -211,10 +233,14 @@ export class InspectorComponent extends OwlComponent {
* @param ev
*/
private async _onInspectorClick(ev: MouseEvent): Promise {
- window.removeEventListener('mousemove', this._onInspectorMouseMove, true);
- window.removeEventListener('mouseleave', this._onInspectorMouseLeave, true);
- window.removeEventListener('mousedown', this._onInspectorMouseDown, true);
- window.removeEventListener('click', this._onInspectorClick, true);
+ for (const doc of this._inspecting) {
+ doc.removeEventListener('mouseenter', this._onInspectorMouseEnter, true);
+ doc.removeEventListener('mousemove', this._onInspectorMouseMove, true);
+ doc.removeEventListener('mouseleave', this._onInspectorMouseLeave, true);
+ doc.removeEventListener('mousedown', this._onInspectorMouseDown, true);
+ doc.removeEventListener('click', this._onInspectorClick, true);
+ }
+ this._inspecting.clear();
ev.stopImmediatePropagation();
ev.preventDefault();
for (const inspected of this._hoveredTargets) {
@@ -222,7 +248,11 @@ export class InspectorComponent extends OwlComponent {
}
this._hoveredTargets = [];
- const nodes = this._getNodeFromPosition(ev.clientX, ev.clientY);
+ const nodes = this._getNodeFromPosition(
+ ev.clientX,
+ ev.clientY,
+ (ev.target as Node).ownerDocument,
+ );
if (nodes.length) {
this.state.selectedID = nodes[0].id;
this.selectedNode = this.getNode(this.state.selectedID);
diff --git a/packages/plugin-devtools/src/components/TreeComponent.ts b/packages/plugin-devtools/src/components/TreeComponent.ts
index 1e4a4e12e..9d361602b 100644
--- a/packages/plugin-devtools/src/components/TreeComponent.ts
+++ b/packages/plugin-devtools/src/components/TreeComponent.ts
@@ -126,7 +126,10 @@ export class TreeComponent extends OwlComponent {
* @param node
*/
isFormat(node: VNode, formatName: string): boolean {
- return node.is(InlineNode) && !!node.modifiers.find(format => format.name === formatName);
+ return (
+ node instanceof InlineNode &&
+ !!node.modifiers.find(format => format.name === formatName)
+ );
}
//--------------------------------------------------------------------------
@@ -156,7 +159,7 @@ export class TreeComponent extends OwlComponent {
*/
_getSelectionMarkersAncestors(): Set {
const selectionMarkersAncestors = new Set();
- let ancestor = this.env.editor.selection.anchor.parent;
+ let ancestor: VNode = this.env.editor.selection.anchor.parent;
while (ancestor) {
selectionMarkersAncestors.add(ancestor.id);
ancestor = ancestor.parent;
diff --git a/packages/plugin-devtools/test/devtools.test.ts b/packages/plugin-devtools/test/devtools.test.ts
index eb0ebe856..0404d7457 100644
--- a/packages/plugin-devtools/test/devtools.test.ts
+++ b/packages/plugin-devtools/test/devtools.test.ts
@@ -302,7 +302,7 @@ describe('Plugin: DevTools', () => {
'"' +
'length 3 ' +
'atomic false ' +
- 'modifiers [ Attributes: {} ] ' +
+ 'modifiers [] ' +
'total length 3 ' +
'text content ' +
root.children()[1].textContent +
@@ -906,7 +906,7 @@ describe('Plugin: DevTools', () => {
[...subpanel.querySelectorAll('devtools-td:not(.numbering)')].map(
td => td.textContent,
),
- ).to.deep.equal(['insertText', 'setSelection']);
+ ).to.deep.equal(['@commit', 'insertText', '@commit', 'setSelection']);
});
it('should select "hide"', async () => {
await openDevTools();
@@ -1092,7 +1092,7 @@ describe('Plugin: DevTools', () => {
const subpanel = wrapper
.querySelector('jw-devtools')
.querySelector('devtools-panel.active mainpane-contents');
- const line = subpanel.querySelector('.selectable-line');
+ const line = subpanel.querySelector('.selectable-line:nth-child(2)');
await click(line);
expect(line.classList.contains('selected')).to.equal(true);
@@ -1107,7 +1107,7 @@ describe('Plugin: DevTools', () => {
const subpanel = wrapper
.querySelector('jw-devtools')
.querySelector('devtools-panel.active mainpane-contents');
- const line = subpanel.querySelector('.selectable-line:nth-child(2)');
+ const line = subpanel.querySelector('.selectable-line:nth-child(4)');
await click(line);
expect(line.classList.contains('selected')).to.equal(true);
@@ -1141,7 +1141,7 @@ describe('Plugin: DevTools', () => {
const subpanel = wrapper
.querySelector('jw-devtools')
.querySelector('devtools-panel.active mainpane-contents');
- const line = subpanel.querySelector('.selectable-line:nth-child(2)');
+ const line = subpanel.querySelector('.selectable-line:nth-child(3)');
await click(line);
await keydown(line, 'ArrowUp');
@@ -1169,7 +1169,7 @@ describe('Plugin: DevTools', () => {
const subpanel = wrapper
.querySelector('jw-devtools')
.querySelector('devtools-panel.active mainpane-contents');
- const line = subpanel.querySelector('.selectable-line');
+ const line = subpanel.querySelector('.selectable-line:nth-child(3)');
await click(line);
await keydown(line, 'ArrowDown');
diff --git a/packages/plugin-dialog/src/DialogZoneDomObjectRenderer.ts b/packages/plugin-dialog/src/DialogZoneDomObjectRenderer.ts
index 42b13bf99..e5bf71a5f 100644
--- a/packages/plugin-dialog/src/DialogZoneDomObjectRenderer.ts
+++ b/packages/plugin-dialog/src/DialogZoneDomObjectRenderer.ts
@@ -11,6 +11,7 @@ import { MetadataNode } from '../../plugin-metadata/src/MetadataNode';
import template from '../assets/Dialog.xml';
import '../assets/Dialog.css';
+import { RenderingEngineWorker } from '../../plugin-renderer/src/RenderingEngineCache';
const container = document.createElement('jw-container');
container.innerHTML = template;
@@ -21,11 +22,17 @@ export class DialogZoneDomObjectRenderer extends NodeRenderer {
engine: DomObjectRenderingEngine;
predicate = DialogZoneNode;
- async render(node: DialogZoneNode): Promise {
+ async render(
+ node: DialogZoneNode,
+ worker: RenderingEngineWorker,
+ ): Promise {
const float = document.createElement('jw-dialog-container');
for (const child of node.childVNodes) {
- if (!node.hidden.get(child) && (child.tangible || child.is(MetadataNode))) {
- float.appendChild(await this._renderDialog(child));
+ if (child.tangible || child instanceof MetadataNode) {
+ if (!node.hidden?.[child.id]) {
+ float.appendChild(await this._renderDialog(child));
+ }
+ worker.depends(child, node);
}
}
return {
diff --git a/packages/plugin-dialog/test/Dialog.test.ts b/packages/plugin-dialog/test/Dialog.test.ts
index aa8763ae0..87d84211c 100644
--- a/packages/plugin-dialog/test/Dialog.test.ts
+++ b/packages/plugin-dialog/test/Dialog.test.ts
@@ -79,7 +79,9 @@ describe('Dialog', async () => {
});
it('should add a vNode in the dialog', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
[
'',
@@ -97,14 +99,18 @@ describe('Dialog', async () => {
);
});
it('should add a vNode in the dialog and hide it', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('hide', { componentId: 'aaa' });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
['', ' ', ' '].join(''),
);
});
it('should add a vNode in the dialog and show it', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('hide', { componentId: 'aaa' });
await editor.execCommand('show', { componentId: 'aaa' });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
@@ -124,7 +130,9 @@ describe('Dialog', async () => {
);
});
it('should add a vNode in dialog because dialog is the default zone', async () => {
- await editor.plugins.get(Layout).append('aaa', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'aaa' });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
[
@@ -143,10 +151,14 @@ describe('Dialog', async () => {
);
});
it('should add 2 vNodes in the dialog and show it', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('show', { componentId: 'aaa' });
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'bbb' });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
@@ -174,10 +186,14 @@ describe('Dialog', async () => {
);
});
it('should close 2 dialogs with the X button', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('show', { componentId: 'aaa' });
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'bbb' });
await click(
@@ -217,10 +233,14 @@ describe('Dialog', async () => {
expect(section.parent).to.instanceOf(DialogZoneNode);
});
it('should close 2 dialogs it with the backdrop', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('show', { componentId: 'aaa' });
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'bbb' });
await click(
@@ -259,7 +279,9 @@ describe('Dialog', async () => {
expect(section.parent).to.instanceOf(DialogZoneNode);
});
it('should close a dialog and re-open a dialog', async () => {
- await editor.plugins.get(Layout).append('aaa', 'float');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('aaa', 'float');
+ });
await editor.execCommand('show', { componentId: 'aaa' });
await click(
@@ -270,7 +292,9 @@ describe('Dialog', async () => {
['', ' ', ' '].join(''),
);
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'bbb' });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
@@ -291,7 +315,9 @@ describe('Dialog', async () => {
);
});
it('should not close the dialog if click in content', async () => {
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('show', { componentId: 'bbb' });
await click(Array.from(container.querySelectorAll('jw-dialog jw-content')).pop());
@@ -314,7 +340,9 @@ describe('Dialog', async () => {
);
});
it('should hide a vNode in dialog (without remove the vNode)', async () => {
- await editor.plugins.get(Layout).append('bbb', 'not available zone');
+ await editor.execCommand(() => {
+ return editor.plugins.get(Layout).append('bbb', 'not available zone');
+ });
await editor.execCommand('hide', { componentId: 'bbb' });
diff --git a/packages/plugin-divider/src/DividerXmlDomParser.ts b/packages/plugin-divider/src/DividerXmlDomParser.ts
index 44c7e640d..f32e8b8ba 100644
--- a/packages/plugin-divider/src/DividerXmlDomParser.ts
+++ b/packages/plugin-divider/src/DividerXmlDomParser.ts
@@ -13,7 +13,10 @@ export class DividerXmlDomParser extends AbstractParser {
async parse(item: Element): Promise {
const divider = new DividerNode();
- divider.modifiers.append(this.engine.parseAttributes(item));
+ const attributes = this.engine.parseAttributes(item);
+ if (attributes.length) {
+ divider.modifiers.append(attributes);
+ }
const nodes = await this.engine.parse(...item.childNodes);
divider.append(...nodes);
diff --git a/packages/plugin-dom-editable/src/DomEditable.ts b/packages/plugin-dom-editable/src/DomEditable.ts
index bcacfa5a6..b8d5537fe 100644
--- a/packages/plugin-dom-editable/src/DomEditable.ts
+++ b/packages/plugin-dom-editable/src/DomEditable.ts
@@ -37,6 +37,7 @@ export class DomEditable extends JWPl
async stop(): Promise {
this.eventNormalizer.destroy();
window.removeEventListener('keydown', this._processKeydown);
+ return super.stop();
}
/**
diff --git a/packages/plugin-dom-editable/test/DomEditable.test.ts b/packages/plugin-dom-editable/test/DomEditable.test.ts
index 6d0298b47..c57956219 100644
--- a/packages/plugin-dom-editable/test/DomEditable.test.ts
+++ b/packages/plugin-dom-editable/test/DomEditable.test.ts
@@ -180,8 +180,16 @@ describe('DomEditable', () => {
await editor.start();
commandNames = [];
const execCommand = editor.execCommand;
- editor.execCommand = async (commandName: string, params: object): Promise => {
- commandNames.push(commandName);
+ editor.execCommand = async (
+ commandName: string | (() => Promise | void),
+ params?: object,
+ ): Promise => {
+ if (typeof commandName === 'function') {
+ commandNames.push('@custom');
+ await commandName();
+ } else {
+ commandNames.push(commandName);
+ }
return execCommand.call(editor, commandName, params);
};
});
@@ -832,14 +840,14 @@ describe('DomEditable', () => {
await selectAllWithKeyA(container1);
await selectAllWithKeyA(container2);
- await editor.stop();
- await editor2.stop();
-
const params = {
context: editor.contextManager.defaultContext,
};
expect(execSpy.args).to.eql([['command-b', params]]);
expect(execSpy2.args).to.eql([['selectAll', {}]]);
+
+ await editor.stop();
+ await editor2.stop();
});
it('deleteContentBackward (SwiftKey) with special keymap', async () => {
section.innerHTML = 'abcd
';
@@ -875,8 +883,16 @@ describe('DomEditable', () => {
await editor.start();
commandNames = [];
const execCommand = editor.execCommand;
- editor.execCommand = async (commandName: string, params: object): Promise => {
- commandNames.push(commandName);
+ editor.execCommand = async (
+ commandName: string | (() => Promise | void),
+ params?: object,
+ ): Promise => {
+ if (typeof commandName === 'function') {
+ commandNames.push('@custom');
+ await commandName();
+ } else {
+ commandNames.push(commandName);
+ }
return execCommand.call(editor, commandName, params);
};
diff --git a/packages/plugin-dom-follow-range/test/FollowRange.test.ts b/packages/plugin-dom-follow-range/test/FollowRange.test.ts
index 7b2a3b5fd..0bd68e122 100644
--- a/packages/plugin-dom-follow-range/test/FollowRange.test.ts
+++ b/packages/plugin-dom-follow-range/test/FollowRange.test.ts
@@ -82,8 +82,11 @@ describe('FollowRange', async () => {
location: [section, 'replace'],
});
await editor.start();
- await editor.plugins.get(Layout).append('aaa', 'range');
- await editor.plugins.get(Layout).append('bbb', 'range');
+
+ await editor.execCommand(async () => {
+ await editor.plugins.get(Layout).append('aaa', 'range');
+ await editor.plugins.get(Layout).append('bbb', 'range');
+ });
expect(container.innerHTML.replace(/[\s\n]+/g, ' ')).to.equal(
[
'',
diff --git a/packages/plugin-dom-layout/src/ActionableDomObjectRenderer.ts b/packages/plugin-dom-layout/src/ActionableDomObjectRenderer.ts
index 1d8d71803..7d09d28d2 100644
--- a/packages/plugin-dom-layout/src/ActionableDomObjectRenderer.ts
+++ b/packages/plugin-dom-layout/src/ActionableDomObjectRenderer.ts
@@ -15,8 +15,17 @@ export class ActionableDomObjectRenderer extends NodeRenderer {
engine: DomObjectRenderingEngine;
predicate: Predicate = ActionableNode;
+ actionableNodes = new Map();
+
+ constructor(engine: DomObjectRenderingEngine) {
+ super(engine);
+ this.engine.editor.dispatcher.registerCommandHook(
+ '@commit',
+ this._updateActionables.bind(this),
+ );
+ }
+
async render(button: ActionableNode): Promise {
- let updateButton: () => void;
let handler: (ev: MouseEvent) => void;
const objectButton: DomObjectActionable = {
tag: 'BUTTON',
@@ -34,13 +43,11 @@ export class ActionableDomObjectRenderer extends NodeRenderer {
objectButton.handler();
};
el.addEventListener('click', handler);
- updateButton = this._updateButton.bind(this, button, el);
- updateButton();
- this.engine.editor.dispatcher.registerCommandHook('*', updateButton);
+ this.actionableNodes.set(button, el);
},
detach: (el: HTMLButtonElement): void => {
el.removeEventListener('click', handler);
- this.engine.editor.dispatcher.removeCommandHook('*', updateButton);
+ this.actionableNodes.delete(button);
},
};
const attributes = button.modifiers.find(Attributes);
@@ -55,33 +62,38 @@ export class ActionableDomObjectRenderer extends NodeRenderer {
return objectButton;
}
+
/**
* Update button rendering after the command if the value of selected or
* enabled change.
- *
- * @param button
- * @param element
*/
- private _updateButton(button: ActionableNode, element: HTMLButtonElement): void {
- const editor = this.engine.editor;
- const select = !!button.selected(editor);
- const enable = !!button.enabled(editor);
+ protected _updateActionables(): void {
+ const commandNames = this.engine.editor.memoryInfo.commandNames;
+ if (commandNames.length === 1 && commandNames.includes('insertText')) {
+ // By default the actionable buttons are not update for a text insertion.
+ return;
+ }
+ for (const [actionable, element] of this.actionableNodes) {
+ const editor = this.engine.editor;
+ const select = !!actionable.selected(editor);
+ const enable = !!actionable.enabled(editor);
- const attrSelected = element.getAttribute('aria-pressed');
- if (select.toString() !== attrSelected) {
- element.setAttribute('aria-pressed', select.toString());
- if (select) {
- element.classList.add('pressed');
- } else {
- element.classList.remove('pressed');
+ const attrSelected = element.getAttribute('aria-pressed');
+ if (select.toString() !== attrSelected) {
+ element.setAttribute('aria-pressed', select.toString());
+ if (select) {
+ element.classList.add('pressed');
+ } else {
+ element.classList.remove('pressed');
+ }
}
- }
- const domEnable = !element.getAttribute('disabled');
- if (enable !== domEnable) {
- if (enable) {
- element.removeAttribute('disabled');
- } else {
- element.setAttribute('disabled', 'true');
+ const domEnable = !element.getAttribute('disabled');
+ if (enable !== domEnable) {
+ if (enable) {
+ element.removeAttribute('disabled');
+ } else {
+ element.setAttribute('disabled', 'true');
+ }
}
}
}
diff --git a/packages/plugin-dom-layout/src/ActionableGroupSelectItemDomObjectRenderer.ts b/packages/plugin-dom-layout/src/ActionableGroupSelectItemDomObjectRenderer.ts
index 8fb967469..5bdacf145 100644
--- a/packages/plugin-dom-layout/src/ActionableGroupSelectItemDomObjectRenderer.ts
+++ b/packages/plugin-dom-layout/src/ActionableGroupSelectItemDomObjectRenderer.ts
@@ -11,16 +11,26 @@ import { VNode } from '../../core/src/VNodes/VNode';
import { LabelNode } from '../../plugin-layout/src/LabelNode';
import { ZoneNode } from '../../plugin-layout/src/ZoneNode';
import { DomObjectActionable } from './ActionableDomObjectRenderer';
+import { RenderingEngineWorker } from '../../plugin-renderer/src/RenderingEngineCache';
export class ActionableGroupSelectItemDomObjectRenderer extends NodeRenderer {
static id = DomObjectRenderingEngine.id;
engine: DomObjectRenderingEngine;
predicate = (node: VNode): boolean => node.ancestors(ActionableGroupNode).length >= 2;
- async render(item: VNode): Promise {
+ actionableNodes = new Map();
+
+ constructor(engine: DomObjectRenderingEngine) {
+ super(engine);
+ this.engine.editor.dispatcher.registerCommandHook(
+ '@commit',
+ this._updateActionables.bind(this),
+ );
+ }
+
+ async render(item: VNode, worker: RenderingEngineWorker): Promise {
let domObject: DomObject;
if (item instanceof ActionableNode) {
- let updateOption: () => void;
let handler: (ev: Event) => void;
const domObjectActionable: DomObjectActionable = {
tag: 'OPTION',
@@ -43,14 +53,12 @@ export class ActionableGroupSelectItemDomObjectRenderer extends NodeRenderer {
const select = el.closest('select') || el.parentElement;
select.removeEventListener('change', handler);
- this.engine.editor.dispatcher.removeCommandHook('*', updateOption);
+ this.actionableNodes.delete(item);
},
};
domObject = domObjectActionable;
@@ -88,7 +96,7 @@ export class ActionableGroupSelectItemDomObjectRenderer extends NodeRenderer extends JWPlugin {
@@ -53,7 +51,7 @@ export class DomLayout extends JWPl
domLocations: this._loadComponentLocations,
};
commandHooks = {
- '*': this.redraw,
+ '@commit': this._redraw,
};
constructor(editor: JWEditor, configuration: T) {
@@ -76,9 +74,6 @@ export class DomLayout extends JWPl
this._loadComponentLocations(this.configuration.locations || []);
domLayoutEngine.location = this.configuration.location;
await domLayoutEngine.start();
- if (this.configuration.afterRender) {
- await this.configuration.afterRender();
- }
window.addEventListener('keydown', this.processKeydown, true);
}
async stop(): Promise {
@@ -86,6 +81,7 @@ export class DomLayout extends JWPl
const layout = this.dependencies.get(Layout);
const domLayoutEngine = layout.engines.dom;
await domLayoutEngine.stop();
+ return super.stop();
}
//--------------------------------------------------------------------------
@@ -122,25 +118,6 @@ export class DomLayout extends JWPl
}
}
- async redraw(): Promise {
- // TODO: adapt when add memory
- // * redraw node with change
- // * redraw children if add or remove children (except for selection)
- const layout = this.dependencies.get(Layout);
- const domLayoutEngine = layout.engines.dom as DomLayoutEngine;
- const editables = domLayoutEngine.components.get('editable');
- if (editables?.length) {
- const nodes = [...editables];
- for (const node of nodes) {
- if (node instanceof ContainerNode) {
- nodes.push(...node.childVNodes);
- }
- }
- await domLayoutEngine.redraw(...nodes);
- await this.configuration.afterRender?.();
- }
- }
-
/**
* Return true if the target node is inside Jabberwock's main editable Zone.
*
@@ -202,4 +179,9 @@ export class DomLayout extends JWPl
}
return isAnchorDescendantOfTarget ? anchorNode : target;
}
+ private async _redraw(params: CommitParams): Promise {
+ const layout = this.dependencies.get(Layout);
+ const domLayoutEngine = layout.engines.dom as DomLayoutEngine;
+ await domLayoutEngine.redraw(params.changesLocations);
+ }
}
diff --git a/packages/plugin-dom-layout/src/DomLayoutEngine.ts b/packages/plugin-dom-layout/src/DomLayoutEngine.ts
index 41165ef29..cad7b1517 100644
--- a/packages/plugin-dom-layout/src/DomLayoutEngine.ts
+++ b/packages/plugin-dom-layout/src/DomLayoutEngine.ts
@@ -5,10 +5,8 @@ import {
DomZonePosition,
ComponentDefinition,
} from '../../plugin-layout/src/LayoutEngine';
-import { Renderer } from '../../plugin-renderer/src/Renderer';
-import { ZoneNode, ZoneIdentifier } from '../../plugin-layout/src/ZoneNode';
+import { ZoneNode } from '../../plugin-layout/src/ZoneNode';
import { Direction, VSelectionDescription } from '../../core/src/VSelection';
-import { ContainerNode } from '../../core/src/VNodes/ContainerNode';
import { DomSelectionDescription } from '../../plugin-dom-editable/src/EventNormalizer';
import JWEditor from '../../core/src/JWEditor';
import { DomReconciliationEngine } from './DomReconciliationEngine';
@@ -19,6 +17,11 @@ import {
} from '../../plugin-renderer-dom-object/src/DomObjectRenderingEngine';
import { VElement } from '../../core/src/VNodes/VElement';
import { flat } from '../../utils/src/utils';
+import { Modifier } from '../../core/src/Modifier';
+import { RenderingEngineCache } from '../../plugin-renderer/src/RenderingEngineCache';
+import { ChangesLocations } from '../../core/src/Memory/Memory';
+import { AbstractNode } from '../../core/src/VNodes/AbstractNode';
+import { Renderer } from '../../plugin-renderer/src/Renderer';
export type DomPoint = [Node, number];
export type DomLayoutLocation = [Node, DomZonePosition];
@@ -35,6 +38,8 @@ export class DomLayoutEngine extends LayoutEngine {
location: [Node, DomZonePosition];
locations: Record = {};
+ private _rendererCache: RenderingEngineCache;
+
defaultRootComponent: ComponentDefinition = {
id: 'editor',
async render(): Promise {
@@ -66,7 +71,6 @@ export class DomLayoutEngine extends LayoutEngine {
}
await super.start();
- await this.redraw();
}
async stop(): Promise {
for (const componentId in this.componentDefinitions) {
@@ -89,8 +93,10 @@ export class DomLayoutEngine extends LayoutEngine {
}
}
this.renderingMap = {};
+ this._markedForRedraw = new Set();
this.location = null;
this.locations = {};
+ this._rendererCache = null;
this._domReconciliationEngine.clear();
return super.stop();
}
@@ -115,150 +121,26 @@ export class DomLayoutEngine extends LayoutEngine {
getDomNodes(node: VNode): Node[] {
return this._domReconciliationEngine.toDom(node);
}
- /**
- * Redraw the layout component after insertion.
- * If the target zone is the root, prepare its location before redrawing.
- *
- * @override
- */
- async prepend(componentId: ComponentId, zoneId: ZoneIdentifier, props?: {}): Promise {
- const nodes = await super.prepend(componentId, zoneId, props);
- // Filter out children of nodes that we are already going to redraw.
- const nodeToRedraw = nodes.filter(node => !node.ancestor(n => nodes.includes(n)));
- for (const node of nodeToRedraw) {
- nodeToRedraw.push(...node.childVNodes);
- }
- // only used if the zone want to return a Node but hide the component (eg: a panel)
- // TODO: adapt when add memory
- await this.redraw(...nodeToRedraw);
- return nodes;
- }
- /**
- * Redraw the layout component after insertion.
- * If the target zone is the root, prepare its location before redrawing.
- *
- * @override
- */
- async append(componentId: ComponentId, zoneId: ZoneIdentifier, props?: {}): Promise {
- const nodes = await super.append(componentId, zoneId, props);
- // Filter out children of nodes that we are already going to redraw.
- const nodeToRedraw = nodes.filter(node => !node.ancestor(n => nodes.includes(n)));
- for (const node of nodeToRedraw) {
- nodeToRedraw.push(...node.childVNodes);
- }
- // only used if the zone want to return a Node but hide the component (eg: a panel)
- // TODO: adapt when add memory
- await this.redraw(...nodeToRedraw);
- return nodes;
- }
- /**
- * Redraw the layout component after removal.
- *
- * @override
- */
- async remove(componentId: ComponentId): Promise {
- const zones = await super.remove(componentId);
- // TODO: adapt when add memory
- await this.redraw(...zones);
- return zones;
- }
- /**
- * Redraw the layout component after showing the component.
- *
- * @override
- */
- async show(componentId: ComponentId): Promise {
- const nodes = await super.show(componentId);
- const nodeToRedraw = [...nodes];
- for (const node of nodeToRedraw) {
- nodeToRedraw.push(...node.childVNodes);
- }
- for (const node of nodes) {
- nodeToRedraw.push(node.ancestor(ZoneNode));
- }
- // TODO: adapt when add memory
- await this.redraw(...nodeToRedraw);
- return nodes;
- }
- /**
- * Redraw the layout component after hidding the component.
- *
- * @override
- */
- async hide(componentId: ComponentId): Promise {
- const nodes = await super.hide(componentId);
- const nodeToRedraw = [...nodes];
- for (const node of nodes) {
- nodeToRedraw.push(node.ancestor(ZoneNode));
- }
- // TODO: adapt when add memory
- await this.redraw(...nodeToRedraw);
- return nodes;
- }
- async redraw(...nodes: VNode[]): Promise {
- if (
- !this.editor.enableRender ||
- (this.editor.preventRenders && this.editor.preventRenders.size)
- ) {
- return;
- }
+ async redraw(params: ChangesLocations): Promise {
if (this._currentlyRedrawing) {
throw new Error('Double redraw detected');
}
this._currentlyRedrawing = true;
- if (nodes.length) {
- for (let node of nodes) {
- while (
- (this._domReconciliationEngine.getRenderedWith(node).length !== 1 ||
- !this._domReconciliationEngine.toDom(node).length) &&
- node.parent
- ) {
- // If the node are redererd with some other nodes then redraw parent.
- // If not in layout then redraw the parent.
- node = node.parent;
- if (!nodes.includes(node)) {
- nodes.push(node);
- }
- }
- }
- for (const node of [...nodes]) {
- // Add direct siblings nodes for batched nodes with format.
- const previous = node.previous();
- if (previous && !nodes.includes(previous)) {
- nodes.push(previous);
- }
- const next = node.next();
- if (next && !nodes.includes(next)) {
- nodes.push(next);
- }
- }
- } else {
- // Redraw all.
- for (const componentId in this.locations) {
- nodes.push(...this.components.get(componentId));
- }
- for (const node of nodes) {
- if (node instanceof ContainerNode) {
- nodes.push(...node.childVNodes);
- }
- }
- }
-
- nodes = nodes.filter(node => {
- const ancestor = node.ancestors(ZoneNode).pop();
- return ancestor?.managedZones.includes('root');
- });
+ const updatedNodes = [...this._getInvalidNodes(params)];
- // Render nodes.
- const renderer = this.editor.plugins.get(Renderer);
- const domObjects = await renderer.render('dom/object', nodes);
- const engine = renderer.engines['dom/object'] as DomObjectRenderingEngine;
+ const layout = this.editor.plugins.get(Renderer);
+ const engine = layout.engines['dom/object'] as DomObjectRenderingEngine;
+ const cache = (this._rendererCache = await engine.render(
+ updatedNodes,
+ this._rendererCache,
+ ));
this._domReconciliationEngine.update(
- domObjects || [],
- engine.locations,
- engine.from,
+ updatedNodes,
+ cache.renderings,
+ cache.locations,
+ cache.renderingDependent,
this._markedForRedraw,
);
this._markedForRedraw = new Set();
@@ -329,6 +211,302 @@ export class DomLayoutEngine extends LayoutEngine {
// Private
//--------------------------------------------------------------------------
+ /**
+ * Get the invalidated nodes in the rendering.
+ * Clear the renderer in cache for this node or modifier. The cache
+ * renderer is added only for performance at redrawing time. The
+ * invalidation are automatically made from memory changes.
+ */
+ private _getInvalidNodes(diff: ChangesLocations): Set {
+ const cache = this._rendererCache;
+ const remove = new Set();
+ const update = new Set();
+ const updatedModifiers = new Set();
+ const updatedSiblings = new Set();
+
+ const add = new Set();
+
+ // Add new nodes for redrawing it.
+ for (const object of diff.add) {
+ if (object instanceof AbstractNode) {
+ add.add(object as VNode);
+ } else if (object instanceof Modifier) {
+ updatedModifiers.add(object);
+ }
+ }
+
+ for (const node of add) {
+ if (!node.parent) {
+ add.delete(node);
+ remove.add(node);
+ for (const child of node.descendants()) {
+ add.delete(node);
+ remove.add(child);
+ }
+ }
+ }
+
+ if (cache) {
+ // Select the removed VNode and Modifiers.
+ const allRemove = new Set(diff.remove);
+ for (const object of diff.remove) {
+ if (object instanceof AbstractNode) {
+ remove.add(object as VNode);
+ } else {
+ if (object instanceof Modifier) {
+ updatedModifiers.add(object);
+ }
+ for (const [parent] of this.editor.memory.getParents(object)) {
+ if (parent instanceof AbstractNode) {
+ update.add(parent as VNode);
+ } else if (parent instanceof Modifier) {
+ updatedModifiers.add(parent);
+ }
+ }
+ }
+ }
+ const filterd = this._filterInRoot([...remove]);
+ for (const node of filterd.remove) {
+ update.delete(node);
+ remove.add(node);
+ for (const child of node.descendants()) {
+ remove.add(child);
+ }
+ }
+ for (const node of filterd.keep) {
+ update.add(node); // TODO: memory change to have real add and not add + move.
+ }
+
+ const needSiblings = new Set();
+
+ // Filter to keep only update not added or removed nodes.
+ const paramsUpdate: [object, string[] | number[] | void][] = [];
+ diff.update.filter(up => {
+ const object = up[0];
+ if (
+ up[1] &&
+ object instanceof AbstractNode &&
+ (up[1] as string[]).includes('parent') &&
+ !object.parent
+ ) {
+ remove.add(object as VNode);
+ for (const child of object.descendants()) {
+ remove.add(child);
+ }
+ } else if (!remove.has(object as VNode)) {
+ paramsUpdate.push(up);
+ }
+ });
+
+ // Select the updated VNode and Modifiers and the VNode siblings.
+ // From the parent, select the removed VNode siblings.
+ for (const [object, changes] of paramsUpdate) {
+ if (
+ allRemove.has(object) ||
+ update.has(object as VNode) ||
+ updatedModifiers.has(object as Modifier)
+ ) {
+ continue;
+ }
+ if (object instanceof AbstractNode) {
+ update.add(object as VNode);
+ needSiblings.add(object);
+ } else {
+ if (object instanceof Modifier) {
+ updatedModifiers.add(object);
+ }
+ for (const [parent, parentProp] of this.editor.memory.getParents(object)) {
+ if (parent instanceof AbstractNode) {
+ update.add(parent as VNode);
+ if (
+ changes &&
+ parentProp[0][0] === 'childVNodes' &&
+ typeof changes[0] === 'number'
+ ) {
+ // If change a children (add or remove) redraw the node and
+ // siblings.
+ const childVNodes = parent.childVNodes;
+ for (let i = 0; i < changes.length; i++) {
+ const index = changes[i] as number;
+ const child = childVNodes[index];
+ if (child) {
+ if (!add.has(child)) {
+ update.add(child);
+ }
+ if (changes[i - 1] !== index - 1) {
+ const previous = child.previousSibling();
+ if (
+ previous &&
+ !add.has(previous) &&
+ !update.has(previous)
+ ) {
+ updatedSiblings.add(previous);
+ }
+ }
+ if (changes[i + 1] !== index + 1) {
+ const next = child.nextSibling();
+ if (next && !add.has(next) && !update.has(next)) {
+ if (next) updatedSiblings.add(next);
+ }
+ }
+ } else {
+ const children = parent.children();
+ if (children.length) {
+ const last = children[children.length - 1];
+ if (last && !add.has(last) && !update.has(last)) {
+ updatedSiblings.add(last);
+ }
+ }
+ }
+ }
+ } else {
+ needSiblings.add(parent);
+ }
+ } else if (parent instanceof Modifier) {
+ updatedModifiers.add(parent);
+ }
+ }
+ }
+ }
+
+ // If any change invalidate the siblings.
+ for (const node of needSiblings) {
+ const next = node.nextSibling();
+ if (next) updatedSiblings.add(next);
+ const previous = node.previousSibling();
+ if (previous) updatedSiblings.add(previous);
+ }
+
+ // Invalidate compatible renderer cache.
+ for (const node of update) {
+ cache.cachedCompatibleRenderer.delete(node);
+ }
+
+ // Invalidate compatible renderer cache and modifier compare cache.
+ for (const modifier of updatedModifiers) {
+ cache.cachedCompatibleModifierRenderer.delete(modifier);
+ const id = cache.cachedModifierId.get(modifier);
+ if (id) {
+ const keys = cache.cachedIsSameAsModifierIds[id];
+ if (keys) {
+ for (const key in keys) {
+ delete cache.cachedIsSameAsModifier[key];
+ }
+ delete cache.cachedIsSameAsModifierIds[id];
+ }
+ }
+ }
+
+ // Add the siblings to invalidate the sibling groups.
+ for (const sibling of updatedSiblings) {
+ update.add(sibling);
+ }
+
+ // Get all linked and dependent VNodes and Modifiers to invalidate cache.
+ const treated = new Set();
+ const nodesOrModifiers = [...update, ...updatedModifiers];
+ const treatedItem = new Set(nodesOrModifiers);
+ for (const nodeOrModifier of nodesOrModifiers) {
+ const linkedRenderings = cache.nodeDependent.get(nodeOrModifier);
+ if (linkedRenderings) {
+ for (const link of linkedRenderings) {
+ if (!treated.has(link)) {
+ treated.add(link);
+ const from = cache.renderingDependent.get(link);
+ if (from) {
+ for (const n of from) {
+ if (!treatedItem.has(n)) {
+ // Add to invalid domObject origin nodes or modifiers.
+ nodesOrModifiers.push(n);
+ treatedItem.add(n);
+ }
+ }
+ }
+ }
+ }
+ }
+ const linkedNodes = cache.linkedNodes.get(nodeOrModifier);
+ if (linkedNodes) {
+ for (const node of linkedNodes) {
+ if (!treatedItem.has(node)) {
+ // Add to invalid linked nodes of linkes nodes.
+ nodesOrModifiers.push(node);
+ treatedItem.add(node);
+ }
+ }
+ }
+ if (nodeOrModifier instanceof AbstractNode) {
+ update.add(nodeOrModifier);
+ } else {
+ updatedModifiers.add(nodeOrModifier);
+ }
+ }
+
+ // Invalidate VNode cache origin, location and linked.
+ for (const node of update) {
+ cache.renderingPromises.delete(node);
+ const item = cache.renderings.get(node);
+ if (item) {
+ cache.renderingDependent.delete(item);
+ cache.locations.delete(item);
+ }
+ cache.renderings.delete(node);
+ cache.nodeDependent.delete(node);
+ cache.linkedNodes.delete(node);
+ }
+
+ // Remove all removed children and modifiers.
+ for (const node of remove) {
+ update.delete(node);
+ if (node.modifiers) {
+ // If the node is created after this memory slice (undo), the
+ // node has no values, no id, no modifiers...
+ node.modifiers.map(modifier => updatedModifiers.add(modifier));
+ }
+ }
+
+ // Invalidate Modifiers cache linked.
+ for (const modifier of updatedModifiers) {
+ cache.nodeDependent.delete(modifier);
+ }
+ }
+ for (const node of add) {
+ update.add(node);
+ }
+
+ // Render nodes.
+ return update;
+ }
+ private _filterInRoot(nodes: VNode[]): { keep: Set; remove: Set