refactor(editor): improve implementation of flat model (#10848)
This commit is contained in:
@@ -2,7 +2,7 @@ import { Subject } from 'rxjs';
|
|||||||
import { describe, expect, test } from 'vitest';
|
import { describe, expect, test } from 'vitest';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { ReactiveFlatYMap } from '../reactive/flat-native-y.js';
|
import { ReactiveFlatYMap } from '../reactive/flat-native-y/index.js';
|
||||||
import type { Text } from '../reactive/index.js';
|
import type { Text } from '../reactive/index.js';
|
||||||
import { Boxed, createYProxy, popProp, stashProp } from '../reactive/index.js';
|
import { Boxed, createYProxy, popProp, stashProp } from '../reactive/index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { ReactiveFlatYMap } from '../../reactive/flat-native-y.js';
|
import { ReactiveFlatYMap } from '../../reactive/flat-native-y/index.js';
|
||||||
import type { Schema } from '../../schema/schema.js';
|
import type { Schema } from '../../schema/schema.js';
|
||||||
import type { Store } from '../store/store.js';
|
import type { Store } from '../store/store.js';
|
||||||
import { BlockModel } from './block-model.js';
|
import { BlockModel } from './block-model.js';
|
||||||
|
|||||||
@@ -1,522 +0,0 @@
|
|||||||
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
|
||||||
import { signal } from '@preact/signals-core';
|
|
||||||
import type { Subject } from 'rxjs';
|
|
||||||
import {
|
|
||||||
Array as YArray,
|
|
||||||
Map as YMap,
|
|
||||||
Text as YText,
|
|
||||||
type YMapEvent,
|
|
||||||
} from 'yjs';
|
|
||||||
|
|
||||||
import { SYS_KEYS } from '../consts';
|
|
||||||
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):/, '');
|
|
||||||
|
|
||||||
const keyWithPrefix = (key: string) =>
|
|
||||||
SYS_KEYS.has(key) ? `sys:${key}` : `prop:${key}`;
|
|
||||||
|
|
||||||
type OnChange = (key: string, isLocal: boolean) => void;
|
|
||||||
type Transform = (key: string, value: unknown, origin: unknown) => unknown;
|
|
||||||
|
|
||||||
type CreateProxyOptions = {
|
|
||||||
basePath?: string;
|
|
||||||
onChange?: OnChange;
|
|
||||||
transform: Transform;
|
|
||||||
onDispose: Subject<void>;
|
|
||||||
shouldByPassSignal: () => boolean;
|
|
||||||
shouldByPassYjs: () => boolean;
|
|
||||||
byPassSignalUpdate: (fn: () => void) => void;
|
|
||||||
stashed: Set<string | number>;
|
|
||||||
initialized: () => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
shouldByPassYjs,
|
|
||||||
byPassSignalUpdate,
|
|
||||||
basePath,
|
|
||||||
onChange,
|
|
||||||
initialized,
|
|
||||||
transform,
|
|
||||||
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 ? `${basePath}.${key}` : 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;
|
|
||||||
const unsubscribe = signalData.subscribe(next => {
|
|
||||||
if (!initialized()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byPassSignalUpdate(() => {
|
|
||||||
proxy[p] = next;
|
|
||||||
onChange?.(firstKey, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const subscription = onDispose.subscribe(() => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
unsubscribe();
|
|
||||||
});
|
|
||||||
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, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isPureObject(value)) {
|
|
||||||
const syncYMap = () => {
|
|
||||||
if (shouldByPassYjs()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
yMap.forEach((_, key) => {
|
|
||||||
if (initialized() && 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(() => {
|
|
||||||
if (value instanceof Text || Boxed.is(value)) {
|
|
||||||
value.bind(() => {
|
|
||||||
onChange?.(firstKey, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
yMap.set(keyWithPrefix(fullPath), native2Y(value));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
run(value, fullPath);
|
|
||||||
if (list.length && initialized()) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Text || Boxed.is(value)) {
|
|
||||||
value.bind(() => {
|
|
||||||
onChange?.(firstKey, true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const yValue = native2Y(value);
|
|
||||||
const next = transform(firstKey, value, yValue);
|
|
||||||
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
|
||||||
yMap.doc?.transact(
|
|
||||||
() => {
|
|
||||||
yMap.set(keyWithPrefix(fullPath), yValue);
|
|
||||||
},
|
|
||||||
{ proxy: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = Reflect.set(target, p, next, receiver);
|
|
||||||
updateSignal(next);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return Reflect.set(target, p, value, receiver);
|
|
||||||
},
|
|
||||||
deleteProperty: (target, p) => {
|
|
||||||
if (typeof p === 'string') {
|
|
||||||
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 = () => {
|
|
||||||
if (shouldByPassSignal()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const signalKey = `${firstKey}$`;
|
|
||||||
if (!(signalKey in root)) {
|
|
||||||
if (!isRoot) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
delete root[signalKey];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
byPassSignalUpdate(() => {
|
|
||||||
const prev = root[firstKey];
|
|
||||||
const next = isRoot
|
|
||||||
? prev
|
|
||||||
: isPureObject(prev)
|
|
||||||
? { ...prev }
|
|
||||||
: Array.isArray(prev)
|
|
||||||
? [...prev]
|
|
||||||
: prev;
|
|
||||||
// @ts-expect-error allow magic props
|
|
||||||
root[signalKey].value = next;
|
|
||||||
onChange?.(firstKey, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
|
||||||
yMap.doc?.transact(
|
|
||||||
() => {
|
|
||||||
const fullKey = keyWithPrefix(fullPath);
|
|
||||||
yMap.forEach((_, key) => {
|
|
||||||
if (key.startsWith(fullKey)) {
|
|
||||||
yMap.delete(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ proxy: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = Reflect.deleteProperty(target, p);
|
|
||||||
updateSignal();
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return Reflect.deleteProperty(target, p);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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 _initialized;
|
|
||||||
|
|
||||||
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 = keyWithoutPrefix(key);
|
|
||||||
const keys = keyName.split('.');
|
|
||||||
const firstKey = keys[0];
|
|
||||||
if (this._stashed.has(firstKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._updateWithYjsSkip(() => {
|
|
||||||
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 keys = keyName.split('.');
|
|
||||||
const firstKey = keys[0];
|
|
||||||
if (this._stashed.has(firstKey)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._updateWithYjsSkip(() => {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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 (_: unknown, isLocal: boolean) => {
|
|
||||||
this._onChange?.(key, isLocal);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _createDefaultData = (): UnRecord => {
|
|
||||||
const root: UnRecord = {};
|
|
||||||
const transform = this._transform;
|
|
||||||
Array.from(this._ySource.entries()).forEach(([key, value]) => {
|
|
||||||
if (key.startsWith('sys')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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) {
|
|
||||||
const next = new Text(value);
|
|
||||||
finalData = transform(firstKey, next, 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, root, path);
|
|
||||||
acc[key] = data;
|
|
||||||
}
|
|
||||||
if (index === allLength - 1) {
|
|
||||||
acc[key] = finalData;
|
|
||||||
}
|
|
||||||
return acc[key] as UnRecord;
|
|
||||||
}, root);
|
|
||||||
});
|
|
||||||
|
|
||||||
return root;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _byPassYjs = false;
|
|
||||||
|
|
||||||
private readonly _getProxy = (
|
|
||||||
source: UnRecord,
|
|
||||||
root: UnRecord,
|
|
||||||
path?: string
|
|
||||||
): UnRecord => {
|
|
||||||
return createProxy(this._ySource, source, root, {
|
|
||||||
onDispose: this._onDispose,
|
|
||||||
shouldByPassSignal: () => this._skipNext,
|
|
||||||
byPassSignalUpdate: this._updateWithSkip,
|
|
||||||
shouldByPassYjs: () => this._byPassYjs,
|
|
||||||
basePath: path,
|
|
||||||
onChange: this._onChange,
|
|
||||||
transform: this._transform,
|
|
||||||
stashed: this._stashed,
|
|
||||||
initialized: () => this._initialized,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly _updateWithYjsSkip = (fn: () => void) => {
|
|
||||||
this._byPassYjs = true;
|
|
||||||
fn();
|
|
||||||
this._byPassYjs = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected readonly _ySource: YMap<unknown>,
|
|
||||||
private readonly _onDispose: Subject<void>,
|
|
||||||
private readonly _onChange?: OnChange
|
|
||||||
) {
|
|
||||||
super();
|
|
||||||
this._initialized = false;
|
|
||||||
const source = this._createDefaultData();
|
|
||||||
this._source = source;
|
|
||||||
|
|
||||||
const proxy = this._getProxy(source, source);
|
|
||||||
|
|
||||||
Object.entries(source).forEach(([key, value]) => {
|
|
||||||
const signalData = signal(value);
|
|
||||||
source[`${key}$`] = signalData;
|
|
||||||
const unsubscribe = signalData.subscribe(next => {
|
|
||||||
if (!this._initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._updateWithSkip(() => {
|
|
||||||
proxy[key] = next;
|
|
||||||
this._onChange?.(key, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const subscription = _onDispose.subscribe(() => {
|
|
||||||
subscription.unsubscribe();
|
|
||||||
unsubscribe();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this._proxy = proxy;
|
|
||||||
this._ySource.observe(this._observer);
|
|
||||||
this._initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function isEmptyObject(obj: UnRecord): boolean {
|
|
||||||
return Object.keys(obj).length === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteEmptyObject(obj: UnRecord, key: string, parent: UnRecord): void {
|
|
||||||
if (isEmptyObject(obj)) {
|
|
||||||
delete parent[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
256
blocksuite/framework/store/src/reactive/flat-native-y/index.ts
Normal file
256
blocksuite/framework/store/src/reactive/flat-native-y/index.ts
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import { BlockSuiteError, ErrorCode } from '@blocksuite/global/exceptions';
|
||||||
|
import { signal } from '@preact/signals-core';
|
||||||
|
import type { Subject } from 'rxjs';
|
||||||
|
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 { y2Native } from '../native-y';
|
||||||
|
import { ReactiveYArray } from '../proxy';
|
||||||
|
import { type OnTextChange, Text } from '../text';
|
||||||
|
import type { ProxyOptions, UnRecord } from '../types';
|
||||||
|
import { createProxy } from './proxy';
|
||||||
|
import type { OnChange } from './types';
|
||||||
|
import {
|
||||||
|
deleteEmptyObject,
|
||||||
|
getFirstKey,
|
||||||
|
isEmptyObject,
|
||||||
|
keyWithoutPrefix,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export class ReactiveFlatYMap extends BaseReactiveYData<
|
||||||
|
UnRecord,
|
||||||
|
YMap<unknown>
|
||||||
|
> {
|
||||||
|
protected readonly _proxy: UnRecord;
|
||||||
|
protected readonly _source: UnRecord;
|
||||||
|
protected readonly _options?: ProxyOptions<UnRecord>;
|
||||||
|
|
||||||
|
private readonly _initialized;
|
||||||
|
|
||||||
|
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 = 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (_: unknown, isLocal: boolean) => {
|
||||||
|
this._onChange?.(key, isLocal);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _createDefaultData = (): UnRecord => {
|
||||||
|
const root: UnRecord = {};
|
||||||
|
const transform = this._transform;
|
||||||
|
Array.from(this._ySource.entries()).forEach(([key, value]) => {
|
||||||
|
if (key.startsWith('sys')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
const next = new Text(value);
|
||||||
|
finalData = transform(firstKey, next, 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, root, path);
|
||||||
|
acc[key] = data;
|
||||||
|
}
|
||||||
|
if (index === allLength - 1) {
|
||||||
|
acc[key] = finalData;
|
||||||
|
}
|
||||||
|
return acc[key] as UnRecord;
|
||||||
|
}, root);
|
||||||
|
});
|
||||||
|
|
||||||
|
return root;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _byPassYjs = false;
|
||||||
|
|
||||||
|
private readonly _getProxy = (
|
||||||
|
source: UnRecord,
|
||||||
|
root: UnRecord,
|
||||||
|
path?: string
|
||||||
|
): UnRecord => {
|
||||||
|
return createProxy({
|
||||||
|
yMap: this._ySource,
|
||||||
|
base: source,
|
||||||
|
root,
|
||||||
|
onDispose: this._onDispose,
|
||||||
|
shouldByPassSignal: () => this._skipNext,
|
||||||
|
byPassSignalUpdate: this._updateWithSkip,
|
||||||
|
shouldByPassYjs: () => this._byPassYjs,
|
||||||
|
basePath: path,
|
||||||
|
onChange: this._onChange,
|
||||||
|
transform: this._transform,
|
||||||
|
stashed: this._stashed,
|
||||||
|
initialized: () => this._initialized,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _updateWithYjsSkip = (fn: () => void) => {
|
||||||
|
this._byPassYjs = true;
|
||||||
|
fn();
|
||||||
|
this._byPassYjs = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly _ySource: YMap<unknown>,
|
||||||
|
private readonly _onDispose: Subject<void>,
|
||||||
|
private readonly _onChange?: OnChange
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
this._initialized = false;
|
||||||
|
const source = this._createDefaultData();
|
||||||
|
this._source = source;
|
||||||
|
|
||||||
|
const proxy = this._getProxy(source, source);
|
||||||
|
|
||||||
|
Object.entries(source).forEach(([key, value]) => {
|
||||||
|
const signalData = signal(value);
|
||||||
|
source[`${key}$`] = signalData;
|
||||||
|
const unsubscribe = signalData.subscribe(next => {
|
||||||
|
if (!this._initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._updateWithSkip(() => {
|
||||||
|
proxy[key] = next;
|
||||||
|
this._onChange?.(key, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const subscription = _onDispose.subscribe(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this._proxy = proxy;
|
||||||
|
this._ySource.observe(this._observer);
|
||||||
|
this._initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}
|
||||||
249
blocksuite/framework/store/src/reactive/flat-native-y/proxy.ts
Normal file
249
blocksuite/framework/store/src/reactive/flat-native-y/proxy.ts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import { signal } from '@preact/signals-core';
|
||||||
|
|
||||||
|
import { Boxed } from '../boxed';
|
||||||
|
import { isPureObject } from '../is-pure-object';
|
||||||
|
import { native2Y } from '../native-y';
|
||||||
|
import { Text } from '../text';
|
||||||
|
import type { UnRecord } from '../types';
|
||||||
|
import type { CreateProxyOptions } from './types';
|
||||||
|
import {
|
||||||
|
getFirstKey,
|
||||||
|
isProxy,
|
||||||
|
keyWithoutPrefix,
|
||||||
|
keyWithPrefix,
|
||||||
|
markProxy,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function initializeProxy(options: CreateProxyOptions) {
|
||||||
|
const { basePath, yMap, base, root } = options;
|
||||||
|
|
||||||
|
Object.entries(base).forEach(([key, value]) => {
|
||||||
|
if (isPureObject(value) && !isProxy(value)) {
|
||||||
|
const proxy = createProxy({
|
||||||
|
...options,
|
||||||
|
yMap,
|
||||||
|
base: value as UnRecord,
|
||||||
|
root,
|
||||||
|
basePath: basePath ? `${basePath}.${key}` : key,
|
||||||
|
});
|
||||||
|
base[key] = proxy;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProxy(options: CreateProxyOptions): UnRecord {
|
||||||
|
const {
|
||||||
|
yMap,
|
||||||
|
base,
|
||||||
|
root,
|
||||||
|
onDispose,
|
||||||
|
shouldByPassSignal,
|
||||||
|
shouldByPassYjs,
|
||||||
|
byPassSignalUpdate,
|
||||||
|
basePath,
|
||||||
|
onChange,
|
||||||
|
initialized,
|
||||||
|
transform,
|
||||||
|
stashed,
|
||||||
|
} = options;
|
||||||
|
const isRoot = !basePath;
|
||||||
|
|
||||||
|
if (isProxy(base)) {
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeProxy(options);
|
||||||
|
|
||||||
|
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 = getFirstKey(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;
|
||||||
|
const unsubscribe = signalData.subscribe(next => {
|
||||||
|
if (!initialized()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byPassSignalUpdate(() => {
|
||||||
|
proxy[p] = next;
|
||||||
|
onChange?.(firstKey, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const subscription = onDispose.subscribe(() => {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
unsubscribe();
|
||||||
|
});
|
||||||
|
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, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isPureObject(value)) {
|
||||||
|
const syncYMap = () => {
|
||||||
|
if (shouldByPassYjs()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yMap.forEach((_, key) => {
|
||||||
|
if (initialized() && 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(() => {
|
||||||
|
if (value instanceof Text || Boxed.is(value)) {
|
||||||
|
value.bind(() => {
|
||||||
|
onChange?.(firstKey, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
yMap.set(keyWithPrefix(fullPath), native2Y(value));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
run(value, fullPath);
|
||||||
|
if (list.length && initialized()) {
|
||||||
|
yMap.doc?.transact(
|
||||||
|
() => {
|
||||||
|
list.forEach(fn => fn());
|
||||||
|
},
|
||||||
|
{ proxy: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isStashed) {
|
||||||
|
syncYMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = createProxy({
|
||||||
|
...options,
|
||||||
|
basePath: fullPath,
|
||||||
|
yMap,
|
||||||
|
base: value as UnRecord,
|
||||||
|
root,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = Reflect.set(target, p, next, receiver);
|
||||||
|
updateSignal(next);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value instanceof Text || Boxed.is(value)) {
|
||||||
|
value.bind(() => {
|
||||||
|
onChange?.(firstKey, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const yValue = native2Y(value);
|
||||||
|
const next = transform(firstKey, value, yValue);
|
||||||
|
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
||||||
|
yMap.doc?.transact(
|
||||||
|
() => {
|
||||||
|
yMap.set(keyWithPrefix(fullPath), yValue);
|
||||||
|
},
|
||||||
|
{ proxy: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Reflect.set(target, p, next, receiver);
|
||||||
|
updateSignal(next);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return Reflect.set(target, p, value, receiver);
|
||||||
|
},
|
||||||
|
deleteProperty: (target, p) => {
|
||||||
|
if (typeof p === 'string') {
|
||||||
|
const fullPath = basePath ? `${basePath}.${p}` : p;
|
||||||
|
const firstKey = getFirstKey(fullPath);
|
||||||
|
const isStashed = stashed.has(firstKey);
|
||||||
|
|
||||||
|
const updateSignal = () => {
|
||||||
|
if (shouldByPassSignal()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const signalKey = `${firstKey}$`;
|
||||||
|
if (!(signalKey in root)) {
|
||||||
|
if (!isRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete root[signalKey];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
byPassSignalUpdate(() => {
|
||||||
|
const prev = root[firstKey];
|
||||||
|
const next = isRoot
|
||||||
|
? prev
|
||||||
|
: isPureObject(prev)
|
||||||
|
? { ...prev }
|
||||||
|
: Array.isArray(prev)
|
||||||
|
? [...prev]
|
||||||
|
: prev;
|
||||||
|
// @ts-expect-error allow magic props
|
||||||
|
root[signalKey].value = next;
|
||||||
|
onChange?.(firstKey, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isStashed && initialized() && !shouldByPassYjs()) {
|
||||||
|
yMap.doc?.transact(
|
||||||
|
() => {
|
||||||
|
const fullKey = keyWithPrefix(fullPath);
|
||||||
|
yMap.forEach((_, key) => {
|
||||||
|
if (key.startsWith(fullKey)) {
|
||||||
|
yMap.delete(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ proxy: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = Reflect.deleteProperty(target, p);
|
||||||
|
updateSignal();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return Reflect.deleteProperty(target, p);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
markProxy(proxy);
|
||||||
|
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import type { Subject } from 'rxjs';
|
||||||
|
import type { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
|
import type { UnRecord } from '../types';
|
||||||
|
|
||||||
|
export type OnChange = (key: string, isLocal: boolean) => void;
|
||||||
|
export type Transform = (
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
origin: unknown
|
||||||
|
) => unknown;
|
||||||
|
|
||||||
|
export type CreateProxyOptions = {
|
||||||
|
yMap: YMap<unknown>;
|
||||||
|
base: UnRecord;
|
||||||
|
root: UnRecord;
|
||||||
|
basePath?: string;
|
||||||
|
onChange?: OnChange;
|
||||||
|
transform: Transform;
|
||||||
|
onDispose: Subject<void>;
|
||||||
|
shouldByPassSignal: () => boolean;
|
||||||
|
shouldByPassYjs: () => boolean;
|
||||||
|
byPassSignalUpdate: (fn: () => void) => void;
|
||||||
|
stashed: Set<string | number>;
|
||||||
|
initialized: () => boolean;
|
||||||
|
};
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { SYS_KEYS } from '../../consts';
|
||||||
|
import type { UnRecord } from '../types';
|
||||||
|
|
||||||
|
export const keyWithoutPrefix = (key: string) => key.replace(/(prop|sys):/, '');
|
||||||
|
|
||||||
|
export const keyWithPrefix = (key: string) =>
|
||||||
|
SYS_KEYS.has(key) ? `sys:${key}` : `prop:${key}`;
|
||||||
|
|
||||||
|
const proxySymbol = Symbol('proxy');
|
||||||
|
|
||||||
|
export function isProxy(value: unknown): boolean {
|
||||||
|
return proxySymbol in Object.getPrototypeOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markProxy(value: UnRecord): UnRecord {
|
||||||
|
Object.setPrototypeOf(value, {
|
||||||
|
[proxySymbol]: true,
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmptyObject(obj: UnRecord): boolean {
|
||||||
|
return Object.keys(obj).length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteEmptyObject(
|
||||||
|
obj: UnRecord,
|
||||||
|
key: string,
|
||||||
|
parent: UnRecord
|
||||||
|
): void {
|
||||||
|
if (isEmptyObject(obj)) {
|
||||||
|
delete parent[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFirstKey(key: string) {
|
||||||
|
const result = key.split('.').at(0);
|
||||||
|
if (!result) {
|
||||||
|
throw new Error(`Invalid key for: ${key}`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export * from './boxed.js';
|
export * from './boxed.js';
|
||||||
export * from './flat-native-y.js';
|
export * from './flat-native-y/index.js';
|
||||||
export * from './is-pure-object.js';
|
export * from './is-pure-object.js';
|
||||||
export * from './native-y.js';
|
export * from './native-y.js';
|
||||||
export * from './proxy.js';
|
export * from './proxy.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user