refactor(editor): separate yjs subscribe logic of flat model (#10863)

This commit is contained in:
Saul-Mirone
2025-03-14 12:31:39 +00:00
parent a3ce67a59d
commit 517817e66f
2 changed files with 126 additions and 73 deletions

View File

@@ -4,19 +4,13 @@ import { Array as YArray, type Map as YMap, type YMapEvent } from 'yjs';
import { BaseReactiveYData } from '../base-reactive-data';
import { Boxed, type OnBoxedChange } from '../boxed';
import { y2Native } from '../native-y';
import { ReactiveYArray } from '../proxy';
import { type OnTextChange, Text } from '../text';
import type { ProxyOptions, UnRecord } from '../types';
import { initializeData } from './initialize';
import { createProxy } from './proxy';
import type { OnChange } from './types';
import {
deleteEmptyObject,
getFirstKey,
isEmptyObject,
keyWithoutPrefix,
} from './utils';
import { getYEventHandler } from './y-event-handler';
export class ReactiveFlatYMap extends BaseReactiveYData<
UnRecord,
@@ -32,72 +26,14 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
const yMap = this._ySource;
const proxy = this._proxy;
this._onObserve(event, () => {
event.keysChanged.forEach(key => {
const type = event.changes.keys.get(key);
if (!type) {
return;
}
if (type.action === 'update' || type.action === 'add') {
const value = yMap.get(key);
const keyName: string = keyWithoutPrefix(key);
const firstKey = getFirstKey(keyName);
if (this._stashed.has(firstKey)) {
return;
}
this._updateWithYjsSkip(() => {
const keys = keyName.split('.');
void keys.reduce((acc, key, index, arr) => {
if (!acc[key] && index !== arr.length - 1) {
acc[key] = {};
}
if (index === arr.length - 1) {
acc[key] = y2Native(value, {
transform: (value, origin) => {
return this._transform(firstKey, value, origin);
},
});
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
this._onChange?.(firstKey, false);
return;
}
if (type.action === 'delete') {
const keyName: string = keyWithoutPrefix(key);
const firstKey = getFirstKey(keyName);
if (this._stashed.has(firstKey)) {
return;
}
this._updateWithYjsSkip(() => {
const keys = keyName.split('.');
void keys.reduce((acc, key, index) => {
if (index === keys.length - 1) {
delete acc[key];
let curr = acc;
let parentKey = keys[index - 1];
let parent = proxy as UnRecord;
let path = keys.slice(0, -2);
for (let i = keys.length - 2; i > 0; i--) {
for (const pathKey of path) {
parent = parent[pathKey] as UnRecord;
}
if (!isEmptyObject(curr)) {
break;
}
deleteEmptyObject(curr, parentKey, parent);
curr = parent;
parentKey = keys[i - 1];
path = path.slice(0, -1);
parent = proxy as UnRecord;
}
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
return;
}
getYEventHandler({
yMap,
proxy,
stashed: this._stashed,
updateWithYjsSkip: this._updateWithYjsSkip,
transform: this._transform,
onChange: this._onChange,
event,
});
});
};

View File

@@ -0,0 +1,117 @@
import type { Map as YMap, YMapEvent } from 'yjs';
import { y2Native } from '../native-y';
import type { UnRecord } from '../types';
import {
deleteEmptyObject,
getFirstKey,
isEmptyObject,
keyWithoutPrefix,
} from './utils';
type YEventHandlerOptions = {
event: YMapEvent<unknown>;
yMap: YMap<unknown>;
proxy: UnRecord;
stashed: Set<string | number>;
updateWithYjsSkip: (fn: () => void) => void;
transform: (key: string, value: unknown, origin: unknown) => unknown;
onChange?: (key: string, isAdd: boolean) => void;
};
// update proxy when yjs map changes
export const getYEventHandler = (options: YEventHandlerOptions) => {
const { event } = options;
const { keysChanged, changes } = event;
keysChanged.forEach(key => {
const type = changes.keys.get(key);
if (!type) return;
if (type.action === 'update' || type.action === 'add') {
return handleUpdateOrAdd(key, options);
}
if (type.action === 'delete') {
return handleDelete(key, options);
}
});
};
function isStashed(key: string, stashed: Set<string | number>) {
const keyName: string = keyWithoutPrefix(key);
const firstKey = getFirstKey(keyName);
return stashed.has(firstKey);
}
function handleUpdateOrAdd(
key: string,
{
yMap,
proxy,
stashed,
updateWithYjsSkip,
transform,
onChange,
}: YEventHandlerOptions
) {
if (isStashed(key, stashed)) {
return;
}
const keyName: string = keyWithoutPrefix(key);
const firstKey = getFirstKey(keyName);
updateWithYjsSkip(() => {
const value = yMap.get(key);
const keys = keyName.split('.');
void keys.reduce((acc, key, index, arr) => {
if (!acc[key] && index !== arr.length - 1) {
acc[key] = {};
}
if (index === arr.length - 1) {
acc[key] = y2Native(value, {
transform: (value, origin) => transform(firstKey, value, origin),
});
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
onChange?.(firstKey, false);
}
function handleDelete(
key: string,
{ proxy, stashed, updateWithYjsSkip, onChange }: YEventHandlerOptions
) {
if (isStashed(key, stashed)) {
return;
}
const keyName: string = keyWithoutPrefix(key);
const firstKey = getFirstKey(keyName);
updateWithYjsSkip(() => {
const keys = keyName.split('.');
void keys.reduce((acc, key, index) => {
if (index === keys.length - 1) {
delete acc[key];
let curr = acc;
let parentKey = keys[index - 1];
let parent = proxy as UnRecord;
let path = keys.slice(0, -2);
for (let i = keys.length - 2; i > 0; i--) {
for (const pathKey of path) {
parent = parent[pathKey] as UnRecord;
}
if (!isEmptyObject(curr)) {
break;
}
deleteEmptyObject(curr, parentKey, parent);
curr = parent;
parentKey = keys[i - 1];
path = path.slice(0, -1);
parent = proxy as UnRecord;
}
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
onChange?.(firstKey, false);
}