mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-25 22:38:56 +08:00
feat(editor): flat block data (#9854)
Flat block data.
A new block type to flatten the block data
```typescript
// For developers
type Model = {
blocks: Record<string, {
flavour: string;
cells: Record<string, {
rowId: string;
colId: string;
text: Text;
}>;
cols: Record<string, {
align: string;
}>
rows: Record<string, {
backgroundColor: string;
}>
}>
}
// How it's saved in yjs
const yData = {
blocks: {
'blockId1': {
flavour: 'affine:table',
'prop:rows:row1:backgroundColor': 'white',
'prop:cols:col1:align': 'left',
'prop:cells:cell1:rowId': 'row1',
'prop:cells:cell1:colId': 'col1',
'prop:cells:cell1:text': YText,
prop:children: []
},
}
}
```
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import type { Doc as YDoc, YEvent } from 'yjs';
|
||||
import { UndoManager } from 'yjs';
|
||||
|
||||
import type { ProxyOptions } from './types';
|
||||
|
||||
export abstract class BaseReactiveYData<T, Y> {
|
||||
protected _getOrigin = (
|
||||
doc: YDoc
|
||||
): {
|
||||
doc: YDoc;
|
||||
proxy: true;
|
||||
|
||||
target: BaseReactiveYData<any, any>;
|
||||
} => {
|
||||
return {
|
||||
doc,
|
||||
proxy: true,
|
||||
target: this,
|
||||
};
|
||||
};
|
||||
|
||||
protected _onObserve = (event: YEvent<any>, handler: () => void) => {
|
||||
if (
|
||||
event.transaction.origin?.proxy !== true &&
|
||||
(!event.transaction.local ||
|
||||
event.transaction.origin instanceof UndoManager)
|
||||
) {
|
||||
handler();
|
||||
}
|
||||
|
||||
this._options?.onChange?.(this._proxy);
|
||||
};
|
||||
|
||||
protected abstract readonly _options?: ProxyOptions<T>;
|
||||
|
||||
protected abstract readonly _proxy: T;
|
||||
|
||||
protected _skipNext = false;
|
||||
|
||||
protected abstract readonly _source: T;
|
||||
|
||||
protected readonly _stashed = new Set<string | number>();
|
||||
|
||||
protected _transact = (doc: YDoc, fn: () => void) => {
|
||||
doc.transact(fn, this._getOrigin(doc));
|
||||
};
|
||||
|
||||
protected _updateWithSkip = (fn: () => void) => {
|
||||
if (this._skipNext) {
|
||||
return;
|
||||
}
|
||||
this._skipNext = true;
|
||||
fn();
|
||||
this._skipNext = false;
|
||||
};
|
||||
|
||||
protected abstract readonly _ySource: Y;
|
||||
|
||||
get proxy() {
|
||||
return this._proxy;
|
||||
}
|
||||
|
||||
abstract pop(prop: string | number): void;
|
||||
abstract stash(prop: string | number): void;
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import { type Slot } from '@blocksuite/global/utils';
|
||||
import { signal } from '@preact/signals-core';
|
||||
import {
|
||||
Array as YArray,
|
||||
Map as YMap,
|
||||
Text as YText,
|
||||
type YMapEvent,
|
||||
} from 'yjs';
|
||||
|
||||
import { BaseReactiveYData } from './base-reactive-data';
|
||||
import { Boxed, type OnBoxedChange } from './boxed';
|
||||
import { isPureObject } from './is-pure-object';
|
||||
import { native2Y, y2Native } from './native-y';
|
||||
import { ReactiveYArray } from './proxy';
|
||||
import { type OnTextChange, Text } from './text';
|
||||
import type { ProxyOptions, UnRecord } from './types';
|
||||
|
||||
const keyWithoutPrefix = (key: string) => key.replace(/(prop|sys):/, '');
|
||||
|
||||
type OnChange = (key: string, value: unknown) => void;
|
||||
type Transform = (key: string, value: unknown, origin: unknown) => unknown;
|
||||
|
||||
type CreateProxyOptions = {
|
||||
basePath?: string;
|
||||
onChange?: OnChange;
|
||||
transform?: Transform;
|
||||
onDispose: Slot;
|
||||
shouldByPassSignal: () => boolean;
|
||||
byPassSignalUpdate: (fn: () => void) => void;
|
||||
stashed: Set<string | number>;
|
||||
};
|
||||
|
||||
const proxySymbol = Symbol('proxy');
|
||||
|
||||
function isProxy(value: unknown): boolean {
|
||||
return proxySymbol in Object.getPrototypeOf(value);
|
||||
}
|
||||
|
||||
function markProxy(value: UnRecord): UnRecord {
|
||||
Object.setPrototypeOf(value, {
|
||||
[proxySymbol]: true,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function createProxy(
|
||||
yMap: YMap<unknown>,
|
||||
base: UnRecord,
|
||||
root: UnRecord,
|
||||
options: CreateProxyOptions
|
||||
): UnRecord {
|
||||
const {
|
||||
onDispose,
|
||||
shouldByPassSignal,
|
||||
byPassSignalUpdate,
|
||||
basePath,
|
||||
onChange,
|
||||
transform = (_key, value) => value,
|
||||
stashed,
|
||||
} = options;
|
||||
const isRoot = !basePath;
|
||||
|
||||
if (isProxy(base)) {
|
||||
return base;
|
||||
}
|
||||
|
||||
Object.entries(base).forEach(([key, value]) => {
|
||||
if (isPureObject(value) && !isProxy(value)) {
|
||||
const proxy = createProxy(yMap, value as UnRecord, root, {
|
||||
...options,
|
||||
basePath: `${basePath}.${key}`,
|
||||
});
|
||||
base[key] = proxy;
|
||||
}
|
||||
});
|
||||
|
||||
const proxy = new Proxy(base, {
|
||||
has: (target, p) => {
|
||||
return Reflect.has(target, p);
|
||||
},
|
||||
get: (target, p, receiver) => {
|
||||
return Reflect.get(target, p, receiver);
|
||||
},
|
||||
set: (target, p, value, receiver) => {
|
||||
if (typeof p === 'string') {
|
||||
const list: Array<() => void> = [];
|
||||
const fullPath = basePath ? `${basePath}.${p}` : p;
|
||||
const firstKey = fullPath.split('.')[0];
|
||||
if (!firstKey) {
|
||||
throw new Error(`Invalid key for: ${fullPath}`);
|
||||
}
|
||||
|
||||
const isStashed = stashed.has(firstKey);
|
||||
|
||||
const updateSignal = (value: unknown) => {
|
||||
if (shouldByPassSignal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signalKey = `${firstKey}$`;
|
||||
if (!(signalKey in root)) {
|
||||
if (!isRoot) {
|
||||
return;
|
||||
}
|
||||
const signalData = signal(value);
|
||||
root[signalKey] = signalData;
|
||||
onDispose.once(
|
||||
signalData.subscribe(next => {
|
||||
byPassSignalUpdate(() => {
|
||||
proxy[p] = next;
|
||||
onChange?.(firstKey, next);
|
||||
});
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
byPassSignalUpdate(() => {
|
||||
const prev = root[firstKey];
|
||||
const next = isRoot
|
||||
? value
|
||||
: isPureObject(prev)
|
||||
? { ...prev }
|
||||
: Array.isArray(prev)
|
||||
? [...prev]
|
||||
: prev;
|
||||
// @ts-expect-error allow magic props
|
||||
root[signalKey].value = next;
|
||||
onChange?.(firstKey, next);
|
||||
});
|
||||
};
|
||||
|
||||
if (isPureObject(value)) {
|
||||
const syncYMap = () => {
|
||||
yMap.forEach((_, key) => {
|
||||
if (keyWithoutPrefix(key).startsWith(fullPath)) {
|
||||
yMap.delete(key);
|
||||
}
|
||||
});
|
||||
const run = (obj: object, basePath: string) => {
|
||||
Object.entries(obj).forEach(([key, value]) => {
|
||||
const fullPath = basePath ? `${basePath}.${key}` : key;
|
||||
if (isPureObject(value)) {
|
||||
run(value, fullPath);
|
||||
} else {
|
||||
list.push(() => {
|
||||
yMap.set(`prop:${fullPath}`, value);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
run(value, fullPath);
|
||||
if (list.length) {
|
||||
yMap.doc?.transact(
|
||||
() => {
|
||||
list.forEach(fn => fn());
|
||||
},
|
||||
{ proxy: true }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isStashed) {
|
||||
syncYMap();
|
||||
}
|
||||
|
||||
const next = createProxy(yMap, value as UnRecord, root, {
|
||||
...options,
|
||||
basePath: fullPath,
|
||||
});
|
||||
|
||||
const result = Reflect.set(target, p, next, receiver);
|
||||
updateSignal(next);
|
||||
return result;
|
||||
}
|
||||
|
||||
const yValue = native2Y(value);
|
||||
const next = transform(firstKey, value, yValue);
|
||||
if (!isStashed) {
|
||||
yMap.doc?.transact(
|
||||
() => {
|
||||
yMap.set(`prop:${fullPath}`, yValue);
|
||||
},
|
||||
{ proxy: true }
|
||||
);
|
||||
}
|
||||
|
||||
const result = Reflect.set(target, p, next, receiver);
|
||||
updateSignal(next);
|
||||
return result;
|
||||
}
|
||||
return Reflect.set(target, p, value, receiver);
|
||||
},
|
||||
});
|
||||
|
||||
markProxy(proxy);
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
export class ReactiveFlatYMap extends BaseReactiveYData<
|
||||
UnRecord,
|
||||
YMap<unknown>
|
||||
> {
|
||||
protected readonly _proxy: UnRecord;
|
||||
protected readonly _source: UnRecord;
|
||||
protected readonly _options?: ProxyOptions<UnRecord>;
|
||||
|
||||
private readonly _observer = (event: YMapEvent<unknown>) => {
|
||||
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 = key.replace('prop:', '');
|
||||
const keys = keyName.split('.');
|
||||
const firstKey = keys[0];
|
||||
if (this._stashed.has(firstKey)) {
|
||||
return;
|
||||
}
|
||||
void keys.reduce((acc, key, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
acc[key] = y2Native(value);
|
||||
}
|
||||
return acc[key] as UnRecord;
|
||||
}, proxy as UnRecord);
|
||||
return;
|
||||
}
|
||||
if (type.action === 'delete') {
|
||||
const keyName: string = key.replace('prop:', '');
|
||||
const keys = keyName.split('.');
|
||||
const firstKey = keys[0];
|
||||
if (this._stashed.has(firstKey)) {
|
||||
return;
|
||||
}
|
||||
void keys.reduce((acc, key, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
delete acc[key];
|
||||
}
|
||||
return acc[key] as UnRecord;
|
||||
}, proxy as UnRecord);
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private readonly _transform = (
|
||||
key: string,
|
||||
value: unknown,
|
||||
origin: unknown
|
||||
) => {
|
||||
const onChange = this._getPropOnChange(key);
|
||||
if (value instanceof Text) {
|
||||
value.bind(onChange as OnTextChange);
|
||||
return value;
|
||||
}
|
||||
if (Boxed.is(origin)) {
|
||||
(value as Boxed).bind(onChange as OnBoxedChange);
|
||||
return value;
|
||||
}
|
||||
if (origin instanceof YArray) {
|
||||
const data = new ReactiveYArray(value as unknown[], origin, {
|
||||
onChange,
|
||||
});
|
||||
return data.proxy;
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
private readonly _getPropOnChange = (key: string) => {
|
||||
return () => {
|
||||
const value = this._proxy[key];
|
||||
this._onChange?.(key, value);
|
||||
};
|
||||
};
|
||||
|
||||
private readonly _createDefaultData = (): UnRecord => {
|
||||
const data: UnRecord = {};
|
||||
const transform = this._transform;
|
||||
Array.from(this._ySource.entries()).forEach(([key, value]) => {
|
||||
const keys = keyWithoutPrefix(key).split('.');
|
||||
const firstKey = keys[0];
|
||||
|
||||
let finalData = value;
|
||||
if (Boxed.is(value)) {
|
||||
finalData = transform(firstKey, new Boxed(value), value);
|
||||
} else if (value instanceof YArray) {
|
||||
finalData = transform(firstKey, value.toArray(), value);
|
||||
} else if (value instanceof YText) {
|
||||
finalData = transform(firstKey, new Text(value), value);
|
||||
} else if (value instanceof YMap) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ReactiveProxyError,
|
||||
'flatY2Native does not support Y.Map as value of Y.Map'
|
||||
);
|
||||
} else {
|
||||
finalData = transform(firstKey, value, value);
|
||||
}
|
||||
const allLength = keys.length;
|
||||
void keys.reduce((acc: UnRecord, key, index) => {
|
||||
if (!acc[key] && index !== allLength - 1) {
|
||||
const path = keys.slice(0, index + 1).join('.');
|
||||
const data = this._getProxy({} as UnRecord, path);
|
||||
acc[key] = data;
|
||||
}
|
||||
if (index === allLength - 1) {
|
||||
acc[key] = finalData;
|
||||
}
|
||||
return acc[key] as UnRecord;
|
||||
}, data);
|
||||
});
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
private readonly _getProxy = (source: UnRecord, path?: string): UnRecord => {
|
||||
return createProxy(this._ySource, source, source, {
|
||||
onDispose: this._onDispose,
|
||||
shouldByPassSignal: () => this._skipNext,
|
||||
byPassSignalUpdate: this._updateWithSkip,
|
||||
basePath: path,
|
||||
onChange: this._onChange,
|
||||
transform: this._transform,
|
||||
stashed: this._stashed,
|
||||
});
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly _ySource: YMap<unknown>,
|
||||
private readonly _onDispose: Slot,
|
||||
private readonly _onChange?: OnChange
|
||||
) {
|
||||
super();
|
||||
const source = this._createDefaultData();
|
||||
this._source = source;
|
||||
|
||||
const proxy = this._getProxy(source);
|
||||
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
const signalData = signal(value);
|
||||
source[`${key}$`] = signalData;
|
||||
_onDispose.once(
|
||||
signalData.subscribe(next => {
|
||||
this._updateWithSkip(() => {
|
||||
proxy[key] = next;
|
||||
this._onChange?.(key, next);
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
this._proxy = proxy;
|
||||
this._ySource.observe(this._observer);
|
||||
}
|
||||
|
||||
pop = (prop: string): void => {
|
||||
const value = this._source[prop];
|
||||
this._stashed.delete(prop);
|
||||
this._proxy[prop] = value;
|
||||
};
|
||||
|
||||
stash = (prop: string): void => {
|
||||
this._stashed.add(prop);
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
export * from './boxed.js';
|
||||
export * from './flat-native-y.js';
|
||||
export * from './is-pure-object.js';
|
||||
export * from './native-y.js';
|
||||
export * from './proxy.js';
|
||||
export * from './stash-pop.js';
|
||||
export * from './text.js';
|
||||
export * from './utils.js';
|
||||
export * from './types.js';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export function isPureObject(value: unknown): value is object {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
Object.prototype.toString.call(value) === '[object Object]' &&
|
||||
[Object, undefined, null].some(x => x === value.constructor)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { BaseReactiveYData } from './base-reactive-data';
|
||||
|
||||
export const proxies = new WeakMap<any, BaseReactiveYData<any, any>>();
|
||||
export const flatProxies = new WeakMap<any, BaseReactiveYData<any, any>>();
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Array as YArray, Map as YMap, Text as YText } from 'yjs';
|
||||
|
||||
import { Boxed } from './boxed.js';
|
||||
import { isPureObject } from './is-pure-object.js';
|
||||
import { Text } from './text.js';
|
||||
import type { Native2Y, TransformOptions } from './types.js';
|
||||
|
||||
export function native2Y<T>(
|
||||
value: T,
|
||||
{ deep = true, transform = x => x }: TransformOptions = {}
|
||||
): Native2Y<T> {
|
||||
if (value instanceof Boxed) {
|
||||
return transform(value.yMap, value) as Native2Y<T>;
|
||||
}
|
||||
if (value instanceof Text) {
|
||||
if (value.yText.doc) {
|
||||
return transform(value.yText.clone(), value) as Native2Y<T>;
|
||||
}
|
||||
return transform(value.yText, value) as Native2Y<T>;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const yArray: YArray<unknown> = new YArray<unknown>();
|
||||
const result = value.map(item => {
|
||||
return deep ? native2Y(item, { deep, transform }) : item;
|
||||
});
|
||||
yArray.insert(0, result);
|
||||
|
||||
return transform(yArray, value) as Native2Y<T>;
|
||||
}
|
||||
if (isPureObject(value)) {
|
||||
const yMap = new YMap<unknown>();
|
||||
Object.entries(value).forEach(([key, value]) => {
|
||||
yMap.set(key, deep ? native2Y(value, { deep, transform }) : value);
|
||||
});
|
||||
|
||||
return transform(yMap, value) as Native2Y<T>;
|
||||
}
|
||||
|
||||
return transform(value, value) as Native2Y<T>;
|
||||
}
|
||||
|
||||
export function y2Native(
|
||||
yAbstract: unknown,
|
||||
{ deep = true, transform = x => x }: TransformOptions = {}
|
||||
) {
|
||||
if (Boxed.is(yAbstract)) {
|
||||
const data = new Boxed(yAbstract);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YText) {
|
||||
const data = new Text(yAbstract);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YArray) {
|
||||
const data: unknown[] = yAbstract
|
||||
.toArray()
|
||||
.map(item => (deep ? y2Native(item, { deep, transform }) : item));
|
||||
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YMap) {
|
||||
const data: Record<string, unknown> = Object.fromEntries(
|
||||
Array.from(yAbstract.entries()).map(([key, value]) => {
|
||||
return [key, deep ? y2Native(value, { deep, transform }) : value] as [
|
||||
string,
|
||||
unknown,
|
||||
];
|
||||
})
|
||||
);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
|
||||
return transform(yAbstract, yAbstract);
|
||||
}
|
||||
@@ -2,16 +2,12 @@ import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||
import type { YArrayEvent, YMapEvent } from 'yjs';
|
||||
import { Array as YArray, Map as YMap } from 'yjs';
|
||||
|
||||
import { BaseReactiveYData } from './base-reactive-data.js';
|
||||
import { Boxed, type OnBoxedChange } from './boxed.js';
|
||||
import { proxies } from './memory.js';
|
||||
import { native2Y, y2Native } from './native-y.js';
|
||||
import { type OnTextChange, Text } from './text.js';
|
||||
import type { UnRecord } from './utils.js';
|
||||
import { BaseReactiveYData, native2Y, y2Native } from './utils.js';
|
||||
|
||||
export type ProxyOptions<T> = {
|
||||
onChange?: (data: T) => void;
|
||||
};
|
||||
|
||||
const proxies = new WeakMap<any, BaseReactiveYData<any, any>>();
|
||||
import type { ProxyOptions, TransformOptions, UnRecord } from './types.js';
|
||||
|
||||
export class ReactiveYArray extends BaseReactiveYData<
|
||||
unknown[],
|
||||
@@ -298,60 +294,34 @@ export function createYProxy<Data>(
|
||||
return proxies.get(yAbstract)!.proxy as Data;
|
||||
}
|
||||
|
||||
return y2Native(yAbstract, {
|
||||
transform: (value, origin) => {
|
||||
if (value instanceof Text) {
|
||||
value.bind(options.onChange as OnTextChange);
|
||||
return value;
|
||||
}
|
||||
if (Boxed.is(origin)) {
|
||||
(value as Boxed).bind(options.onChange as OnBoxedChange);
|
||||
return value;
|
||||
}
|
||||
if (origin instanceof YArray) {
|
||||
const data = new ReactiveYArray(
|
||||
value as unknown[],
|
||||
origin,
|
||||
options as ProxyOptions<unknown[]>
|
||||
);
|
||||
return data.proxy;
|
||||
}
|
||||
if (origin instanceof YMap) {
|
||||
const data = new ReactiveYMap(
|
||||
value as UnRecord,
|
||||
origin,
|
||||
options as ProxyOptions<UnRecord>
|
||||
);
|
||||
return data.proxy;
|
||||
}
|
||||
|
||||
const transform: TransformOptions['transform'] = (value, origin) => {
|
||||
if (value instanceof Text) {
|
||||
value.bind(options.onChange as OnTextChange);
|
||||
return value;
|
||||
},
|
||||
}) as Data;
|
||||
}
|
||||
}
|
||||
if (Boxed.is(origin)) {
|
||||
(value as Boxed).bind(options.onChange as OnBoxedChange);
|
||||
return value;
|
||||
}
|
||||
if (origin instanceof YArray) {
|
||||
const data = new ReactiveYArray(
|
||||
value as unknown[],
|
||||
origin,
|
||||
options as ProxyOptions<unknown[]>
|
||||
);
|
||||
return data.proxy;
|
||||
}
|
||||
if (origin instanceof YMap) {
|
||||
const data = new ReactiveYMap(
|
||||
value as UnRecord,
|
||||
origin,
|
||||
options as ProxyOptions<UnRecord>
|
||||
);
|
||||
return data.proxy;
|
||||
}
|
||||
|
||||
export function stashProp(yMap: YMap<unknown>, prop: string): void;
|
||||
export function stashProp(yMap: YArray<unknown>, prop: number): void;
|
||||
export function stashProp(yAbstract: unknown, prop: string | number) {
|
||||
const proxy = proxies.get(yAbstract);
|
||||
if (!proxy) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ReactiveProxyError,
|
||||
'YData is not subscribed before changes'
|
||||
);
|
||||
}
|
||||
proxy.stash(prop);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
export function popProp(yMap: YMap<unknown>, prop: string): void;
|
||||
export function popProp(yMap: YArray<unknown>, prop: number): void;
|
||||
export function popProp(yAbstract: unknown, prop: string | number) {
|
||||
const proxy = proxies.get(yAbstract);
|
||||
if (!proxy) {
|
||||
throw new BlockSuiteError(
|
||||
ErrorCode.ReactiveProxyError,
|
||||
'YData is not subscribed before changes'
|
||||
);
|
||||
}
|
||||
proxy.pop(prop);
|
||||
return y2Native(yAbstract, { transform }) as Data;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Array as YArray, Map as YMap } from 'yjs';
|
||||
|
||||
import { proxies } from './memory';
|
||||
|
||||
export function stashProp(yMap: YMap<unknown>, prop: string): void;
|
||||
export function stashProp(yMap: YArray<unknown>, prop: number): void;
|
||||
export function stashProp(yAbstract: unknown, prop: string | number) {
|
||||
const proxy = proxies.get(yAbstract);
|
||||
proxy?.stash(prop);
|
||||
}
|
||||
|
||||
export function popProp(yMap: YMap<unknown>, prop: string): void;
|
||||
export function popProp(yMap: YArray<unknown>, prop: number): void;
|
||||
export function popProp(yAbstract: unknown, prop: string | number) {
|
||||
const proxy = proxies.get(yAbstract);
|
||||
proxy?.pop(prop);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Array as YArray, Map as YMap } from 'yjs';
|
||||
|
||||
export type UnRecord = Record<string, unknown>;
|
||||
|
||||
export type Native2Y<T> =
|
||||
T extends Record<string, infer U>
|
||||
? YMap<U>
|
||||
: T extends Array<infer U>
|
||||
? YArray<U>
|
||||
: T;
|
||||
|
||||
export type TransformOptions = {
|
||||
deep?: boolean;
|
||||
transform?: (value: unknown, origin: unknown) => unknown;
|
||||
};
|
||||
|
||||
export type ProxyOptions<T> = {
|
||||
onChange?: (data: T) => void;
|
||||
};
|
||||
@@ -1,157 +0,0 @@
|
||||
import type { Doc as YDoc, YEvent } from 'yjs';
|
||||
import { Array as YArray, Map as YMap, Text as YText, UndoManager } from 'yjs';
|
||||
|
||||
import { Boxed } from './boxed.js';
|
||||
import type { ProxyOptions } from './proxy.js';
|
||||
import { Text } from './text.js';
|
||||
|
||||
export type Native2Y<T> =
|
||||
T extends Record<string, infer U>
|
||||
? YMap<U>
|
||||
: T extends Array<infer U>
|
||||
? YArray<U>
|
||||
: T;
|
||||
|
||||
export function isPureObject(value: unknown): value is object {
|
||||
return (
|
||||
value !== null &&
|
||||
typeof value === 'object' &&
|
||||
Object.prototype.toString.call(value) === '[object Object]' &&
|
||||
[Object, undefined, null].some(x => x === value.constructor)
|
||||
);
|
||||
}
|
||||
|
||||
type TransformOptions = {
|
||||
deep?: boolean;
|
||||
transform?: (value: unknown, origin: unknown) => unknown;
|
||||
};
|
||||
|
||||
export function native2Y<T>(
|
||||
value: T,
|
||||
{ deep = true, transform = x => x }: TransformOptions = {}
|
||||
): Native2Y<T> {
|
||||
if (value instanceof Boxed) {
|
||||
return value.yMap as Native2Y<T>;
|
||||
}
|
||||
if (value instanceof Text) {
|
||||
if (value.yText.doc) {
|
||||
return value.yText.clone() as Native2Y<T>;
|
||||
}
|
||||
return value.yText as Native2Y<T>;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
const yArray: YArray<unknown> = new YArray<unknown>();
|
||||
const result = value.map(item => {
|
||||
return deep ? native2Y(item, { deep, transform }) : item;
|
||||
});
|
||||
yArray.insert(0, result);
|
||||
|
||||
return yArray as Native2Y<T>;
|
||||
}
|
||||
if (isPureObject(value)) {
|
||||
const yMap = new YMap<unknown>();
|
||||
Object.entries(value).forEach(([key, value]) => {
|
||||
yMap.set(key, deep ? native2Y(value, { deep, transform }) : value);
|
||||
});
|
||||
|
||||
return yMap as Native2Y<T>;
|
||||
}
|
||||
|
||||
return value as Native2Y<T>;
|
||||
}
|
||||
|
||||
export function y2Native(
|
||||
yAbstract: unknown,
|
||||
{ deep = true, transform = x => x }: TransformOptions = {}
|
||||
) {
|
||||
if (Boxed.is(yAbstract)) {
|
||||
const data = new Boxed(yAbstract);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YText) {
|
||||
const data = new Text(yAbstract);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YArray) {
|
||||
const data: unknown[] = yAbstract
|
||||
.toArray()
|
||||
.map(item => (deep ? y2Native(item, { deep, transform }) : item));
|
||||
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
if (yAbstract instanceof YMap) {
|
||||
const data: Record<string, unknown> = Object.fromEntries(
|
||||
Array.from(yAbstract.entries()).map(([key, value]) => {
|
||||
return [key, deep ? y2Native(value, { deep, transform }) : value] as [
|
||||
string,
|
||||
unknown,
|
||||
];
|
||||
})
|
||||
);
|
||||
return transform(data, yAbstract);
|
||||
}
|
||||
|
||||
return transform(yAbstract, yAbstract);
|
||||
}
|
||||
|
||||
export type UnRecord = Record<string, unknown>;
|
||||
|
||||
export abstract class BaseReactiveYData<T, Y> {
|
||||
protected _getOrigin = (
|
||||
doc: YDoc
|
||||
): {
|
||||
doc: YDoc;
|
||||
proxy: true;
|
||||
|
||||
target: BaseReactiveYData<any, any>;
|
||||
} => {
|
||||
return {
|
||||
doc,
|
||||
proxy: true,
|
||||
target: this,
|
||||
};
|
||||
};
|
||||
|
||||
protected _onObserve = (event: YEvent<any>, handler: () => void) => {
|
||||
if (
|
||||
event.transaction.origin?.proxy !== true &&
|
||||
(!event.transaction.local ||
|
||||
event.transaction.origin instanceof UndoManager)
|
||||
) {
|
||||
handler();
|
||||
}
|
||||
|
||||
this._options.onChange?.(this._proxy);
|
||||
};
|
||||
|
||||
protected abstract readonly _options: ProxyOptions<T>;
|
||||
|
||||
protected abstract readonly _proxy: T;
|
||||
|
||||
protected _skipNext = false;
|
||||
|
||||
protected abstract readonly _source: T;
|
||||
|
||||
protected readonly _stashed = new Set<string | number>();
|
||||
|
||||
protected _transact = (doc: YDoc, fn: () => void) => {
|
||||
doc.transact(fn, this._getOrigin(doc));
|
||||
};
|
||||
|
||||
protected _updateWithSkip = (fn: () => void) => {
|
||||
this._skipNext = true;
|
||||
fn();
|
||||
this._skipNext = false;
|
||||
};
|
||||
|
||||
protected abstract readonly _ySource: Y;
|
||||
|
||||
get proxy() {
|
||||
return this._proxy;
|
||||
}
|
||||
|
||||
protected abstract _getProxy(): T;
|
||||
|
||||
abstract pop(prop: string | number): void;
|
||||
abstract stash(prop: string | number): void;
|
||||
}
|
||||
Reference in New Issue
Block a user