diff --git a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts index 72b8785b54..71e3288979 100644 --- a/blocksuite/framework/store/src/__tests__/block.unit.spec.ts +++ b/blocksuite/framework/store/src/__tests__/block.unit.spec.ts @@ -267,20 +267,20 @@ test('on change', () => { const model = block.model as RootModel; model.title = internalPrimitives.Text('abc'); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'title'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'title', true); expect(model.title$.value.toDelta()).toEqual([{ insert: 'abc' }]); onPropsUpdated.mockClear(); model.title.insert('d', 1); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'title'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'title', true); expect(model.title$.value.toDelta()).toEqual([{ insert: 'adbc' }]); onPropsUpdated.mockClear(); model.boxed.getValue()!.set('foo', 0); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'boxed'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'boxed', true); expect(model.boxed$.value.getValue()!.toJSON()).toEqual({ foo: 0, }); @@ -343,7 +343,7 @@ test('deep sync', () => { const map = new Y.Map(); map.set('color', 'green'); getColsMap().set('3', map); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'cols'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'cols', true); expect(onColsUpdated).toHaveBeenCalledWith({ '1': { color: 'red' }, '2': { color: 'blue' }, @@ -356,7 +356,7 @@ test('deep sync', () => { onRowsUpdated.mockClear(); model.rows.push({ color: 'yellow' }); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'rows'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'rows', true); expect(onRowsUpdated).toHaveBeenCalledWith([{ color: 'yellow' }]); expect(onPropsUpdated).toHaveBeenCalledTimes(1); expect(onRowsUpdated).toHaveBeenCalledTimes(1); @@ -367,7 +367,7 @@ test('deep sync', () => { const row1 = getRowsArr().get(0) as Y.Map; row1.set('color', 'green'); expect(onRowsUpdated).toHaveBeenCalledWith([{ color: 'green' }]); - expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'rows'); + expect(onPropsUpdated).toHaveBeenCalledWith(expect.anything(), 'rows', true); expect(model.rows$.value).toEqual([{ color: 'green' }]); expect(onPropsUpdated).toHaveBeenCalledTimes(1); expect(onRowsUpdated).toHaveBeenCalledTimes(1); @@ -413,7 +413,7 @@ describe('flat', () => { }); expect(onColUpdated).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols', true); model.props.cols.a.color = 'black'; expect(yBlock.get('prop:cols.a.color')).toBe('black'); @@ -432,7 +432,7 @@ describe('flat', () => { }); expect(onColUpdated).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols', true); onChange.mockClear(); onColUpdated.mockClear(); @@ -442,7 +442,7 @@ describe('flat', () => { expect(model.props.cols$.value).toEqual({ a: {} }); expect(onColUpdated).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'cols', true); model.props.cols = { a: { color: 'red' }, @@ -462,7 +462,7 @@ describe('flat', () => { expect((yBlock.get('prop:title') as Y.Text).toJSON()).toBe('test'); expect(model.props.title$.value.toDelta()).toEqual([{ insert: 'test' }]); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'title'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'title', true); onChange.mockClear(); model.props.labels.push('test'); @@ -470,18 +470,18 @@ describe('flat', () => { expect(getLabels().toJSON()).toEqual(['test']); expect(model.props.labels$.value).toEqual(['test']); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels', true); onChange.mockClear(); model.props.labels$.value = ['test2']; expect(getLabels().toJSON()).toEqual(['test2']); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels', true); onChange.mockClear(); model.props.labels.splice(0, 1); expect(getLabels().toJSON()).toEqual([]); expect(model.props.labels$.value).toEqual([]); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'labels', true); model.props.textCols = { a: internalPrimitives.Text(), @@ -489,7 +489,7 @@ describe('flat', () => { onChange.mockClear(); model.props.textCols.a.insert('test', 0); expect(onChange).toHaveBeenCalledTimes(1); - expect(onChange).toHaveBeenCalledWith(expect.anything(), 'textCols'); + expect(onChange).toHaveBeenCalledWith(expect.anything(), 'textCols', true); expect((yBlock.get('prop:textCols.a') as Y.Text).toJSON()).toBe('test'); expect(model.props.textCols$.value.a.toDelta()).toEqual([ { insert: 'test' }, diff --git a/blocksuite/framework/store/src/model/block/block.ts b/blocksuite/framework/store/src/model/block/block.ts index 60c2d70118..cc9ed70a70 100644 --- a/blocksuite/framework/store/src/model/block/block.ts +++ b/blocksuite/framework/store/src/model/block/block.ts @@ -43,11 +43,11 @@ export class Block { ) { const onChange = !options.onChange ? undefined - : (key: string) => { + : (key: string, isLocal: boolean) => { if (!this._syncController || !this.model) { return; } - options.onChange?.(this, key); + options.onChange?.(this, key, isLocal); }; const flavour = yBlock.get('sys:flavour') as string; const blockSchema = this.schema.get(flavour); diff --git a/blocksuite/framework/store/src/model/block/flat-sync-controller.ts b/blocksuite/framework/store/src/model/block/flat-sync-controller.ts index ce60588204..4732c78151 100644 --- a/blocksuite/framework/store/src/model/block/flat-sync-controller.ts +++ b/blocksuite/framework/store/src/model/block/flat-sync-controller.ts @@ -25,7 +25,7 @@ export class FlatSyncController { readonly schema: Schema, readonly yBlock: YBlock, readonly doc?: Store, - readonly onChange?: (key: string) => void + readonly onChange?: (key: string, isLocal: boolean) => void ) { const { id, flavour, version, yChildren, props } = this._parseYBlock(); diff --git a/blocksuite/framework/store/src/model/block/sync-controller.ts b/blocksuite/framework/store/src/model/block/sync-controller.ts index 5d9edb0698..da48a4d04b 100644 --- a/blocksuite/framework/store/src/model/block/sync-controller.ts +++ b/blocksuite/framework/store/src/model/block/sync-controller.ts @@ -42,6 +42,13 @@ export class SyncController { if (!type) { return; } + const isLocal = + !this.yBlock.doc || + !event.transaction.origin || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this.yBlock.doc.clientID; if (type.action === 'update' || type.action === 'add') { const value = this.yBlock.get(key); const keyName = key.replace('prop:', ''); @@ -57,7 +64,7 @@ export class SyncController { } }); }); - this.onChange?.(keyName); + this.onChange?.(keyName, isLocal); return; } if (type.action === 'delete') { @@ -70,7 +77,7 @@ export class SyncController { this.model[`${keyName}$`].value = undefined; } }); - this.onChange?.(keyName); + this.onChange?.(keyName, isLocal); return; } }); @@ -105,7 +112,7 @@ export class SyncController { readonly schema: Schema, readonly yBlock: YBlock, readonly doc?: Store, - readonly onChange?: (key: string) => void + readonly onChange?: (key: string, isLocal: boolean) => void ) { const { id, flavour, version, yChildren, props } = this._parseYBlock(); @@ -178,7 +185,7 @@ export class SyncController { if (this._stashed.has(p)) { setValue(target, p, value); const result = Reflect.set(target, p, value, receiver); - this.onChange?.(p); + this.onChange?.(p, true); return result; } @@ -222,8 +229,8 @@ export class SyncController { private _getPropsProxy(name: string, value: unknown) { return createYProxy(value, { - onChange: () => { - this.onChange?.(name); + onChange: (_, isLocal) => { + this.onChange?.(name, isLocal); const signalKey = `${name}$`; if (signalKey in this.model) { this._mutex(() => { @@ -344,12 +351,12 @@ export class SyncController { }, set: (target, p, value, receiver) => { const result = Reflect.set(target, p, value, receiver); - this.onChange?.(prop); + this.onChange?.(prop, true); return result; }, deleteProperty: (target, p) => { const result = Reflect.deleteProperty(target, p); - this.onChange?.(prop); + this.onChange?.(prop, true); return result; }, }); @@ -365,12 +372,12 @@ export class SyncController { return Reflect.set(target, p, value, receiver); } const result = Reflect.set(target, p, value, receiver); - this.onChange?.(prop); + this.onChange?.(prop, true); return result; }, deleteProperty: (target, p) => { const result = Reflect.deleteProperty(target, p); - this.onChange?.(p as string); + this.onChange?.(p as string, true); return result; }, }); diff --git a/blocksuite/framework/store/src/model/block/types.ts b/blocksuite/framework/store/src/model/block/types.ts index 35a52b5e65..79566dd981 100644 --- a/blocksuite/framework/store/src/model/block/types.ts +++ b/blocksuite/framework/store/src/model/block/types.ts @@ -10,7 +10,7 @@ export type YBlock = Y.Map & { }; export type BlockOptions = { - onChange?: (block: Block, key: string) => void; + onChange?: (block: Block, key: string, isLocal: boolean) => void; }; export type BlockSysProps = { diff --git a/blocksuite/framework/store/src/model/doc.ts b/blocksuite/framework/store/src/model/doc.ts index ee0be5ea92..194a866bae 100644 --- a/blocksuite/framework/store/src/model/doc.ts +++ b/blocksuite/framework/store/src/model/doc.ts @@ -29,10 +29,12 @@ export interface Doc { | { type: 'add'; id: string; + isLocal: boolean; } | { type: 'delete'; id: string; + isLocal: boolean; } >; }; diff --git a/blocksuite/framework/store/src/model/store/store.ts b/blocksuite/framework/store/src/model/store/store.ts index 103d55f31a..f9019611ca 100644 --- a/blocksuite/framework/store/src/model/store/store.ts +++ b/blocksuite/framework/store/src/model/store/store.ts @@ -34,6 +34,31 @@ export type StoreOptions = { extensions?: ExtensionType[]; }; +export type BlockUpdatedPayload = + | { + type: 'add'; + id: string; + isLocal: boolean; + init: boolean; + flavour: string; + model: BlockModel; + } + | { + type: 'delete'; + id: string; + isLocal: boolean; + flavour: string; + parent: string; + model: BlockModel; + } + | { + type: 'update'; + id: string; + isLocal: boolean; + flavour: string; + props: { key: string }; + }; + const internalExtensions = [StoreSelectionExtension]; export class Store { @@ -76,28 +101,7 @@ export class Store { */ rootAdded: Subject; rootDeleted: Subject; - blockUpdated: Subject< - | { - type: 'add'; - id: string; - init: boolean; - flavour: string; - model: BlockModel; - } - | { - type: 'delete'; - id: string; - flavour: string; - parent: string; - model: BlockModel; - } - | { - type: 'update'; - id: string; - flavour: string; - props: { key: string }; - } - >; + blockUpdated: Subject; }; updateBlock: { @@ -357,7 +361,7 @@ export class Store { if (id in this._blocks.peek()) { return; } - this._onBlockAdded(id, true); + this._onBlockAdded(id, false, true); }); this._subscribeToSlots(); @@ -365,31 +369,18 @@ export class Store { private readonly _subscribeToSlots = () => { this.disposableGroup.add( - this._doc.slots.yBlockUpdated.subscribe( - ({ type, id }: { type: string; id: string }) => { - switch (type) { - case 'add': { - this._onBlockAdded(id); - return; - } - case 'delete': { - this._onBlockRemoved(id); - return; - } - case 'update': { - const block = this.getBlock(id); - if (!block) return; - this.slots.blockUpdated.next({ - type: 'update', - id, - flavour: block.flavour, - props: { key: 'content' }, - }); - return; - } + this._doc.slots.yBlockUpdated.subscribe(({ type, id, isLocal }) => { + switch (type) { + case 'add': { + this._onBlockAdded(id, isLocal, false); + return; + } + case 'delete': { + this._onBlockRemoved(id, isLocal); + return; } } - ) + }) ); this.disposableGroup.add(this.slots.ready); this.disposableGroup.add(this.slots.blockUpdated); @@ -414,7 +405,7 @@ export class Store { return fn(parent, index); } - private _onBlockAdded(id: string, init = false) { + private _onBlockAdded(id: string, isLocal: boolean, init: boolean) { try { if (id in this._blocks.peek()) { return; @@ -426,7 +417,7 @@ export class Store { } const options: BlockOptions = { - onChange: (block, key) => { + onChange: (block, key, isLocal) => { if (key) { block.model.propsUpdated.next({ key }); } @@ -436,6 +427,7 @@ export class Store { id, flavour: block.flavour, props: { key }, + isLocal, }); }, }; @@ -459,6 +451,7 @@ export class Store { init, flavour: block.model.flavour, model: block.model, + isLocal, }); } catch (e) { console.error('An error occurred while adding block:'); @@ -466,7 +459,7 @@ export class Store { } } - private _onBlockRemoved(id: string) { + private _onBlockRemoved(id: string, isLocal: boolean) { try { const block = this.getBlock(id); if (!block) return; @@ -481,6 +474,7 @@ export class Store { flavour: block.flavour, parent: this.getParent(block.model)?.id ?? '', model: block.model, + isLocal, }); const { [id]: _, ...blocks } = this._blocks.peek(); diff --git a/blocksuite/framework/store/src/reactive/base-reactive-data.ts b/blocksuite/framework/store/src/reactive/base-reactive-data.ts index fb829ccbd7..d5833c45cb 100644 --- a/blocksuite/framework/store/src/reactive/base-reactive-data.ts +++ b/blocksuite/framework/store/src/reactive/base-reactive-data.ts @@ -1,13 +1,15 @@ -import type { Doc as YDoc, YEvent } from 'yjs'; -import { UndoManager } from 'yjs'; +import * as Y from 'yjs'; import type { ProxyOptions } from './types'; -export abstract class BaseReactiveYData { +export abstract class BaseReactiveYData< + T, + YSource extends Y.AbstractType, +> { protected _getOrigin = ( - doc: YDoc + doc: Y.Doc ): { - doc: YDoc; + doc: Y.Doc; proxy: true; target: BaseReactiveYData; @@ -19,16 +21,24 @@ export abstract class BaseReactiveYData { }; }; - protected _onObserve = (event: YEvent, handler: () => void) => { + protected _onObserve = (event: Y.YEvent, handler: () => void) => { if ( event.transaction.origin?.proxy !== true && (!event.transaction.local || - event.transaction.origin instanceof UndoManager) + event.transaction.origin instanceof Y.UndoManager) ) { handler(); } - this._options?.onChange?.(this._proxy); + const isLocal = + !event.transaction.origin || + !this._ySource.doc || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this._ySource.doc.clientID; + + this._options?.onChange?.(this._proxy, isLocal); }; protected abstract readonly _options?: ProxyOptions; @@ -41,7 +51,7 @@ export abstract class BaseReactiveYData { protected readonly _stashed = new Set(); - protected _transact = (doc: YDoc, fn: () => void) => { + protected _transact = (doc: Y.Doc, fn: () => void) => { doc.transact(fn, this._getOrigin(doc)); }; @@ -54,7 +64,7 @@ export abstract class BaseReactiveYData { this._skipNext = false; }; - protected abstract readonly _ySource: Y; + protected abstract readonly _ySource: YSource; get proxy() { return this._proxy; diff --git a/blocksuite/framework/store/src/reactive/boxed.ts b/blocksuite/framework/store/src/reactive/boxed.ts index 6289dd2537..11f56a8733 100644 --- a/blocksuite/framework/store/src/reactive/boxed.ts +++ b/blocksuite/framework/store/src/reactive/boxed.ts @@ -2,7 +2,7 @@ import * as Y from 'yjs'; import { NATIVE_UNIQ_IDENTIFIER } from '../consts.js'; -export type OnBoxedChange = (data: unknown) => void; +export type OnBoxedChange = (data: unknown, isLocal: boolean) => void; export class Boxed { static from = (map: Y.Map, onChange?: OnBoxedChange): Boxed => { @@ -44,8 +44,17 @@ export class Boxed { this._map.set('type', NATIVE_UNIQ_IDENTIFIER as T); this._map.set('value', value); } - this._map.observeDeep(() => { - this._onChange?.(this.getValue()); + this._map.observeDeep(events => { + events.forEach(event => { + const isLocal = + !event.transaction.origin || + !this._map.doc || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this._map.doc.clientID; + this._onChange?.(this.getValue(), isLocal); + }); }); } diff --git a/blocksuite/framework/store/src/reactive/flat-native-y.ts b/blocksuite/framework/store/src/reactive/flat-native-y.ts index 1a74a160e4..1d517cf58e 100644 --- a/blocksuite/framework/store/src/reactive/flat-native-y.ts +++ b/blocksuite/framework/store/src/reactive/flat-native-y.ts @@ -22,7 +22,7 @@ const keyWithoutPrefix = (key: string) => key.replace(/(prop|sys):/, ''); const keyWithPrefix = (key: string) => SYS_KEYS.has(key) ? `sys:${key}` : `prop:${key}`; -type OnChange = (key: string) => void; +type OnChange = (key: string, isLocal: boolean) => void; type Transform = (key: string, value: unknown, origin: unknown) => unknown; type CreateProxyOptions = { @@ -119,7 +119,7 @@ function createProxy( } byPassSignalUpdate(() => { proxy[p] = next; - onChange?.(firstKey); + onChange?.(firstKey, true); }); }); const subscription = onDispose.subscribe(() => { @@ -139,7 +139,7 @@ function createProxy( : prev; // @ts-expect-error allow magic props root[signalKey].value = next; - onChange?.(firstKey); + onChange?.(firstKey, true); }); }; @@ -162,7 +162,7 @@ function createProxy( list.push(() => { if (value instanceof Text || Boxed.is(value)) { value.bind(() => { - onChange?.(firstKey); + onChange?.(firstKey, true); }); } yMap.set(keyWithPrefix(fullPath), native2Y(value)); @@ -197,7 +197,7 @@ function createProxy( if (value instanceof Text || Boxed.is(value)) { value.bind(() => { - onChange?.(firstKey); + onChange?.(firstKey, true); }); } const yValue = native2Y(value); @@ -251,7 +251,7 @@ function createProxy( : prev; // @ts-expect-error allow magic props root[signalKey].value = next; - onChange?.(firstKey); + onChange?.(firstKey, true); }); }; @@ -324,6 +324,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< return acc[key] as UnRecord; }, proxy as UnRecord); }); + this._onChange?.(firstKey, false); return; } if (type.action === 'delete') { @@ -390,8 +391,8 @@ export class ReactiveFlatYMap extends BaseReactiveYData< }; private readonly _getPropOnChange = (key: string) => { - return () => { - this._onChange?.(key); + return (_: unknown, isLocal: boolean) => { + this._onChange?.(key, isLocal); }; }; @@ -485,7 +486,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData< } this._updateWithSkip(() => { proxy[key] = next; - this._onChange?.(key); + this._onChange?.(key, true); }); }); const subscription = _onDispose.subscribe(() => { diff --git a/blocksuite/framework/store/src/reactive/proxy.ts b/blocksuite/framework/store/src/reactive/proxy.ts index 53ddcebd9d..358d51b035 100644 --- a/blocksuite/framework/store/src/reactive/proxy.ts +++ b/blocksuite/framework/store/src/reactive/proxy.ts @@ -62,7 +62,7 @@ export class ReactiveYArray extends BaseReactiveYData< if (this._stashed.has(index)) { const result = Reflect.set(target, p, value, receiver); - this._options.onChange?.(this._proxy); + this._options.onChange?.(this._proxy, true); return result; } @@ -196,7 +196,7 @@ export class ReactiveYMap extends BaseReactiveYData> { if (this._stashed.has(p)) { const result = Reflect.set(target, p, value, receiver); - this._options.onChange?.(this._proxy); + this._options.onChange?.(this._proxy, true); return result; } diff --git a/blocksuite/framework/store/src/reactive/text.ts b/blocksuite/framework/store/src/reactive/text.ts index 2fe8e45dac..2bf3300452 100644 --- a/blocksuite/framework/store/src/reactive/text.ts +++ b/blocksuite/framework/store/src/reactive/text.ts @@ -13,7 +13,7 @@ export type DeltaOperation = { retain?: number; } & OptionalAttributes; -export type OnTextChange = (data: Y.Text) => void; +export type OnTextChange = (data: Y.Text, isLocal: boolean) => void; export class Text { private readonly _deltas$: Signal; @@ -67,10 +67,17 @@ export class Text { this._length$ = signal(length); this._deltas$ = signal(this._yText.doc ? this._yText.toDelta() : []); - this._yText.observe(() => { + this._yText.observe(event => { + const isLocal = + !event.transaction.origin || + !this._yText.doc || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this._yText.doc.clientID; this._length$.value = this._yText.length; this._deltas$.value = this._yText.toDelta(); - this._onChange?.(this._yText); + this._onChange?.(this._yText, isLocal); }); } diff --git a/blocksuite/framework/store/src/reactive/types.ts b/blocksuite/framework/store/src/reactive/types.ts index 8aa819b74a..5277f8007b 100644 --- a/blocksuite/framework/store/src/reactive/types.ts +++ b/blocksuite/framework/store/src/reactive/types.ts @@ -15,5 +15,5 @@ export type TransformOptions = { }; export type ProxyOptions = { - onChange?: (data: T) => void; + onChange?: (data: T, isLocal: boolean) => void; }; diff --git a/blocksuite/framework/store/src/test/test-doc.ts b/blocksuite/framework/store/src/test/test-doc.ts index b5debc647d..b1d7339c21 100644 --- a/blocksuite/framework/store/src/test/test-doc.ts +++ b/blocksuite/framework/store/src/test/test-doc.ts @@ -110,10 +110,12 @@ export class TestDoc implements Doc { | { type: 'add'; id: string; + isLocal: boolean; } | { type: 'delete'; id: string; + isLocal: boolean; } >(), }; @@ -185,12 +187,12 @@ export class TestDoc implements Doc { return (readonly?.toString() as 'true' | 'false') ?? 'false'; } - private _handleYBlockAdd(id: string) { - this.slots.yBlockUpdated.next({ type: 'add', id }); + private _handleYBlockAdd(id: string, isLocal: boolean) { + this.slots.yBlockUpdated.next({ type: 'add', id, isLocal }); } - private _handleYBlockDelete(id: string) { - this.slots.yBlockUpdated.next({ type: 'delete', id }); + private _handleYBlockDelete(id: string, isLocal: boolean) { + this.slots.yBlockUpdated.next({ type: 'delete', id, isLocal }); } private _handleYEvent(event: Y.YEvent>) { @@ -198,14 +200,21 @@ export class TestDoc implements Doc { if (event.target !== this._yBlocks) { return; } + const isLocal = + !event.transaction.origin || + !this._yBlocks.doc || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this._yBlocks.doc.clientID; event.keys.forEach((value, id) => { try { if (value.action === 'add') { - this._handleYBlockAdd(id); + this._handleYBlockAdd(id, isLocal); return; } if (value.action === 'delete') { - this._handleYBlockDelete(id); + this._handleYBlockDelete(id, isLocal); return; } } catch (e) { @@ -318,7 +327,7 @@ export class TestDoc implements Doc { this._initYBlocks(); this._yBlocks.forEach((_, id) => { - this._handleYBlockAdd(id); + this._handleYBlockAdd(id, false); }); initFn?.(); diff --git a/packages/frontend/core/src/modules/workspace/impls/doc.ts b/packages/frontend/core/src/modules/workspace/impls/doc.ts index a8c8ab81c1..9cc5f060eb 100644 --- a/packages/frontend/core/src/modules/workspace/impls/doc.ts +++ b/packages/frontend/core/src/modules/workspace/impls/doc.ts @@ -109,10 +109,12 @@ export class DocImpl implements Doc { | { type: 'add'; id: string; + isLocal: boolean; } | { type: 'delete'; id: string; + isLocal: boolean; } >(), }; @@ -175,12 +177,12 @@ export class DocImpl implements Doc { return (readonly?.toString() as 'true' | 'false') ?? 'false'; } - private _handleYBlockAdd(id: string) { - this.slots.yBlockUpdated.next({ type: 'add', id }); + private _handleYBlockAdd(id: string, isLocal: boolean) { + this.slots.yBlockUpdated.next({ type: 'add', id, isLocal }); } - private _handleYBlockDelete(id: string) { - this.slots.yBlockUpdated.next({ type: 'delete', id }); + private _handleYBlockDelete(id: string, isLocal: boolean) { + this.slots.yBlockUpdated.next({ type: 'delete', id, isLocal }); } private _handleYEvent(event: Y.YEvent>) { @@ -188,14 +190,21 @@ export class DocImpl implements Doc { if (event.target !== this._yBlocks) { return; } + const isLocal = + !event.transaction.origin || + !this._yBlocks.doc || + event.transaction.origin instanceof Y.UndoManager || + event.transaction.origin.proxy + ? true + : event.transaction.origin === this._yBlocks.doc.clientID; event.keys.forEach((value, id) => { try { if (value.action === 'add') { - this._handleYBlockAdd(id); + this._handleYBlockAdd(id, isLocal); return; } if (value.action === 'delete') { - this._handleYBlockDelete(id); + this._handleYBlockDelete(id, isLocal); return; } } catch (e) { @@ -317,7 +326,7 @@ export class DocImpl implements Doc { this._initYBlocks(); this._yBlocks.forEach((_, id) => { - this._handleYBlockAdd(id); + this._handleYBlockAdd(id, false); }); initFn?.();