diff --git a/.github/workflows/devtools-publish.yml b/.github/workflows/devtools-publish.yml index d7ede4057..4b2752fee 100644 --- a/.github/workflows/devtools-publish.yml +++ b/.github/workflows/devtools-publish.yml @@ -5,7 +5,7 @@ on: branches: - 'main' paths: - - packages/devtools/** + - packages/devtools/package.json jobs: build-and-deploy: runs-on: ubuntu-latest diff --git a/packages/devtools/src/devtools/components/Detail.tsx b/packages/devtools/src/devtools/components/Detail.tsx index f4f49d98a..6eefe0fd8 100644 --- a/packages/devtools/src/devtools/components/Detail.tsx +++ b/packages/devtools/src/devtools/components/Detail.tsx @@ -110,9 +110,13 @@ function TreeGraph({ tree }: { tree: Devtools.TreeNodeInfo }) { [], ); - return flattenTreeWithDepth(tree).map((node) => ( - - )); + return ( + <> + {flattenTreeWithDepth(tree).map((node) => ( + + ))} + + ); } export function TreeDetail({ diff --git a/packages/devtools/src/devtools/components/Tree.tsx b/packages/devtools/src/devtools/components/Tree.tsx index 002bfbfd3..974535643 100644 --- a/packages/devtools/src/devtools/components/Tree.tsx +++ b/packages/devtools/src/devtools/components/Tree.tsx @@ -22,7 +22,7 @@ import useResizeObserver from 'use-resize-observer'; import { useSelectedNode } from '../contexts/SelectedNode'; import { useSelectedPresence } from '../contexts/SelectedPresence'; -import type { Devtools } from 'yorkie-js-sdk'; +import type { Devtools, Json } from 'yorkie-js-sdk'; import { ArrayIcon, @@ -47,7 +47,7 @@ export type UserNode = Devtools.Client & { export type PresenceJsonNode = { id: string; key: string; - value: Devtools.Json; + value: Json; isLastChild: boolean; type: 'JSON'; }; diff --git a/packages/devtools/src/devtools/contexts/YorkieSource.tsx b/packages/devtools/src/devtools/contexts/YorkieSource.tsx index 986a11d8a..0a280b2d1 100644 --- a/packages/devtools/src/devtools/contexts/YorkieSource.tsx +++ b/packages/devtools/src/devtools/contexts/YorkieSource.tsx @@ -24,18 +24,14 @@ import { useState, } from 'react'; -import { - DocEventType, - type SDKToPanelMessage, - type TransactionEvent, -} from 'yorkie-js-sdk'; +import { DocEventType, Devtools, type SDKToPanelMessage } from 'yorkie-js-sdk'; import { connectPort, sendToSDK } from '../../port'; import { Code, YorkieError } from '@yorkie-js-sdk/src/util/error'; const DocKeyContext = createContext(null); const YorkieDocContext = createContext(null); -const TransactionEventsContext = createContext<{ - events: Array; +const DocEventsForReplayContext = createContext<{ + events: Array; hidePresenceEvents: boolean; setHidePresenceEvents: Dispatch>; }>(null); @@ -47,17 +43,16 @@ type Props = { export function YorkieSourceProvider({ children }: Props) { const [currentDocKey, setCurrentDocKey] = useState(''); const [doc, setDoc] = useState(null); - const [transactionEvents, setTransactionEvents] = useState< - Array + const [docEventsForReplay, setDocEventsForReplay] = useState< + Array >([]); // filter out presence events - const [hideTransactionPresenceEvents, setHideTransactionPresenceEvents] = - useState(false); + const [hidePresenceEvents, setHidePresenceEvents] = useState(false); const resetDocument = () => { setCurrentDocKey(''); - setTransactionEvents([]); + setDocEventsForReplay([]); setDoc(null); }; @@ -77,11 +72,11 @@ export function YorkieSourceProvider({ children }: Props) { case 'doc::sync::full': // TODO(chacha912): Notify the user that they need to use the latest version of Yorkie-JS-SDK. if (message.events === undefined) break; - setTransactionEvents(message.events); + setDocEventsForReplay(message.events); break; case 'doc::sync::partial': if (message.event === undefined) break; - setTransactionEvents((events) => [...events, message.event]); + setDocEventsForReplay((events) => [...events, message.event]); break; } }, []); @@ -108,17 +103,17 @@ export function YorkieSourceProvider({ children }: Props) { return ( - {children} - + ); } @@ -145,51 +140,54 @@ export function useYorkieDoc() { return value; } -export enum TransactionEventType { - Document = 'document', +/** + * `DocEventScope` represents the scope of the document event. + */ +export enum DocEventScope { + Root = 'root', Presence = 'presence', + Document = 'document', } -export const getTransactionEventType = ( - event: TransactionEvent, -): TransactionEventType => { - for (const docEvent of event) { +export const getDocEventsScope = ( + events: Devtools.DocEventsForReplay, +): DocEventScope => { + for (const e of events) { if ( - docEvent.type === DocEventType.StatusChanged || - docEvent.type === DocEventType.Snapshot || - docEvent.type === DocEventType.LocalChange || - docEvent.type === DocEventType.RemoteChange + e.type === DocEventType.Snapshot || + e.type === DocEventType.LocalChange || + e.type === DocEventType.RemoteChange ) { - return TransactionEventType.Document; + return DocEventScope.Root; + } else if (e.type === DocEventType.StatusChanged) { + return DocEventScope.Document; } } - return TransactionEventType.Presence; + return DocEventScope.Presence; }; -export function useTransactionEvents() { +export function useDocEventsForReplay() { const { events, hidePresenceEvents, setHidePresenceEvents } = useContext( - TransactionEventsContext, + DocEventsForReplayContext, ); if (events === undefined) { throw new YorkieError( Code.ErrContextNotProvided, - 'useTransactionEvents should be used within YorkieSourceProvider', + 'useDocEventsForReplay should be used within YorkieSourceProvider', ); } // create an enhanced events with metadata const enhancedEvents = useMemo(() => { return events.map((event) => { - const transactionEventType = getTransactionEventType(event); + const scope = getDocEventsScope(event); return { event, - transactionEventType, - isFiltered: - hidePresenceEvents && - transactionEventType === TransactionEventType.Presence, + scope, + isFiltered: hidePresenceEvents && scope === DocEventScope.Presence, }; }); }, [hidePresenceEvents, events]); diff --git a/packages/devtools/src/devtools/panel/index.tsx b/packages/devtools/src/devtools/panel/index.tsx index 4ced813a3..c3059be00 100644 --- a/packages/devtools/src/devtools/panel/index.tsx +++ b/packages/devtools/src/devtools/panel/index.tsx @@ -23,7 +23,7 @@ import { SelectedPresenceProvider } from '../contexts/SelectedPresence'; import { YorkieSourceProvider, useCurrentDocKey, - useTransactionEvents, + useDocEventsForReplay, useYorkieDoc, } from '../contexts/YorkieSource'; import { Document } from '../tabs/Document'; @@ -34,7 +34,7 @@ import { Separator } from '../components/ResizableSeparator'; const Panel = () => { const currentDocKey = useCurrentDocKey(); const { originalEvents, presenceFilteredEvents, hidePresenceEvents } = - useTransactionEvents(); + useDocEventsForReplay(); const [, setDoc] = useYorkieDoc(); const [selectedEventIndexInfo, setSelectedEventIndexInfo] = useState({ index: null, @@ -91,7 +91,7 @@ const Panel = () => { filteredEventIndex++; } - doc.applyTransactionEvent(originalEvents[eventIndex].event); + doc.applyDocEventsForReplay(originalEvents[eventIndex].event); eventIndex++; } diff --git a/packages/devtools/src/devtools/tabs/History.tsx b/packages/devtools/src/devtools/tabs/History.tsx index d01af06c9..1a7518570 100644 --- a/packages/devtools/src/devtools/tabs/History.tsx +++ b/packages/devtools/src/devtools/tabs/History.tsx @@ -15,18 +15,15 @@ */ import { useEffect, useState, useRef } from 'react'; -import { DocEventType, Change, type TransactionEvent } from 'yorkie-js-sdk'; +import { DocEventType, Change, Devtools } from 'yorkie-js-sdk'; import Slider from 'rc-slider'; import { JSONView } from '../components/JsonView'; import { CursorIcon, DocumentIcon } from '../icons'; -import { - TransactionEventType, - useTransactionEvents, -} from '../contexts/YorkieSource'; +import { DocEventScope, useDocEventsForReplay } from '../contexts/YorkieSource'; const SLIDER_MARK_WIDTH = 24; -const getEventInfo = (event: TransactionEvent) => { +const getEventInfo = (event: Devtools.DocEventsForReplay) => { const info = []; for (const docEvent of event) { if ( @@ -75,7 +72,7 @@ export function History({ presenceFilteredEvents, hidePresenceEvents, setHidePresenceEvents, - } = useTransactionEvents(); + } = useDocEventsForReplay(); const events = hidePresenceEvents ? presenceFilteredEvents : originalEvents; @@ -109,13 +106,10 @@ export function History({ const marks = {}; for (const [index, event] of events.entries()) { const source = event.event[0].source; - const transactionEventType = event.transactionEventType; marks[index] = ( - - {transactionEventType === TransactionEventType.Presence ? ( + + {event.scope === DocEventScope.Presence ? ( ) : ( diff --git a/packages/sdk/src/devtools/index.ts b/packages/sdk/src/devtools/index.ts index 5a30162fc..e103340cf 100644 --- a/packages/sdk/src/devtools/index.ts +++ b/packages/sdk/src/devtools/index.ts @@ -14,33 +14,29 @@ * limitations under the License. */ -import { - Document, - Indexable, - TransactionEvent, -} from '@yorkie-js-sdk/src/yorkie'; -import { DocEventType } from '@yorkie-js-sdk/src/document/document'; +import { Document, Indexable } from '@yorkie-js-sdk/src/yorkie'; import { logger } from '@yorkie-js-sdk/src/util/logger'; import type * as DevTools from './protocol'; import { EventSourceDevPanel, EventSourceSDK } from './protocol'; +import { DocEventsForReplay, isDocEventsForReplay } from './types'; type DevtoolsStatus = 'connected' | 'disconnected' | 'synced'; let devtoolsStatus: DevtoolsStatus = 'disconnected'; const unsubsByDocKey = new Map void>>(); /** - * `transactionEventsByDocKey` stores all events in the document for replaying + * `docEventsForReplayByDocKey` stores all events in the document for replaying * (time-traveling feature) in Devtools. Later, external storage such as * IndexedDB will be used. */ -const transactionEventsByDocKey = new Map>(); +const docEventsForReplayByDocKey = new Map>(); declare global { interface Window { - transactionEventsByDocKey: Map>; + docEventsForReplayByDocKey: Map>; } } if (typeof window !== 'undefined') { - window.transactionEventsByDocKey = transactionEventsByDocKey; + window.docEventsForReplayByDocKey = docEventsForReplayByDocKey; } /** @@ -79,25 +75,13 @@ export function setupDevtools( return; } - transactionEventsByDocKey.set(doc.getKey(), []); + docEventsForReplayByDocKey.set(doc.getKey(), []); const unsub = doc.subscribe('all', (event) => { - if ( - event.some( - (docEvent) => - docEvent.type !== DocEventType.StatusChanged && - docEvent.type !== DocEventType.Snapshot && - docEvent.type !== DocEventType.LocalChange && - docEvent.type !== DocEventType.RemoteChange && - docEvent.type !== DocEventType.Initialized && - docEvent.type !== DocEventType.Watched && - docEvent.type !== DocEventType.Unwatched && - docEvent.type !== DocEventType.PresenceChanged, - ) - ) { + if (!isDocEventsForReplay(event)) { return; } - transactionEventsByDocKey.get(doc.getKey())!.push(event); + docEventsForReplayByDocKey.get(doc.getKey())!.push(event); if (devtoolsStatus === 'synced') { sendToPanel({ msg: 'doc::sync::partial', @@ -148,7 +132,7 @@ export function setupDevtools( sendToPanel({ msg: 'doc::sync::full', docKey: doc.getKey(), - events: transactionEventsByDocKey.get(doc.getKey())!, + events: docEventsForReplayByDocKey.get(doc.getKey())!, }); logger.info(`[YD] Devtools subscribed. Doc: ${doc.getKey()}`); break; diff --git a/packages/sdk/src/devtools/protocol.ts b/packages/sdk/src/devtools/protocol.ts index 2efc161be..9858f4a2d 100644 --- a/packages/sdk/src/devtools/protocol.ts +++ b/packages/sdk/src/devtools/protocol.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { TransactionEvent } from '@yorkie-js-sdk/src/document/document'; +import { DocEventsForReplay } from './types'; /** * `EventSourceDevPanel` is the name of the source representing messages @@ -76,7 +76,7 @@ export type SDKToPanelMessage = | { msg: 'doc::sync::full'; docKey: string; - events: Array; + events: Array; } /** * Sent whenever the document is changed. @@ -84,7 +84,7 @@ export type SDKToPanelMessage = | { msg: 'doc::sync::partial'; docKey: string; - event: TransactionEvent; + event: DocEventsForReplay; }; export type FullPanelToSDKMessage = PanelToSDKMessage & { diff --git a/packages/sdk/src/devtools/types.ts b/packages/sdk/src/devtools/types.ts index 7cf2efd89..47c9a082b 100644 --- a/packages/sdk/src/devtools/types.ts +++ b/packages/sdk/src/devtools/types.ts @@ -17,7 +17,21 @@ import type { PrimitiveValue } from '@yorkie-js-sdk/src/document/crdt/primitive'; import type { CRDTTreePosStruct } from '@yorkie-js-sdk/src/document/crdt/tree'; import { CounterValue } from '@yorkie-js-sdk/src/document/crdt/counter'; -import { Json } from '@yorkie-js-sdk/src/document/document'; +import { + Json, + DocEvent, + DocEventType, + type Indexable, + type StatusChangedEvent, + type SnapshotEvent, + type LocalChangeEvent, + type RemoteChangeEvent, + type InitializedEvent, + type WatchedEvent, + type UnwatchedEvent, + type PresenceChangedEvent, +} from '@yorkie-js-sdk/src/document/document'; +import type { OperationInfo } from '@yorkie-js-sdk/src/document/operation/operation'; /** * `Client` represents a client value in devtools. @@ -85,3 +99,53 @@ export type TreeNodeInfo = { path?: Array; pos?: CRDTTreePosStruct; }; + +/** + * `DocEventForReplay` is an event used to replay a document. + */ +export type DocEventForReplay< + P extends Indexable = Indexable, + T = OperationInfo, +> = + | StatusChangedEvent + | SnapshotEvent + | LocalChangeEvent + | RemoteChangeEvent + | InitializedEvent

+ | WatchedEvent

+ | UnwatchedEvent

+ | PresenceChangedEvent

; + +/** + * `DocEventsForReplay` is a list of events used to replay a document. + */ +export type DocEventsForReplay = Array; + +/** + * `isDocEventForReplay` checks if an event can be used to replay a document. + */ +export function isDocEventForReplay( + event: DocEvent, +): event is DocEventForReplay { + const types = [ + DocEventType.StatusChanged, + DocEventType.Snapshot, + DocEventType.LocalChange, + DocEventType.RemoteChange, + DocEventType.Initialized, + DocEventType.Watched, + DocEventType.Unwatched, + DocEventType.PresenceChanged, + ]; + + return types.includes(event.type); +} + +/** + * `isDocEventsForReplay` checks if a list of events can be used to replay a document. + */ +export function isDocEventsForReplay( + events: Array, +): events is DocEventsForReplay { + return events.every(isDocEventForReplay); +} diff --git a/packages/sdk/src/document/document.ts b/packages/sdk/src/document/document.ts index 92aa1a39f..025b9572a 100644 --- a/packages/sdk/src/document/document.ts +++ b/packages/sdk/src/document/document.ts @@ -228,12 +228,10 @@ export type DocEvent

= | AuthErrorEvent; /** - * `TransactionEvent` represents document events that occur within + * `DocEvents` represents document events that occur within * a single transaction (e.g., doc.update). */ -export type TransactionEvent

= Array< - DocEvent

->; +export type DocEvents

= Array>; /** * @internal @@ -449,7 +447,7 @@ type DocEventCallbackMap

= { broadcast: NextFn; 'local-broadcast': NextFn; 'auth-error': NextFn; - all: NextFn>; + all: NextFn>; }; export type DocEventTopic = keyof DocEventCallbackMap; export type DocEventCallback

= @@ -633,8 +631,8 @@ export class Document { presences: Map; }; - private eventStream: Observable>; - private eventStreamObserver!: Observer>; + private eventStream: Observable>; + private eventStreamObserver!: Observer>; /** * `onlineClients` is a set of client IDs that are currently online. @@ -673,7 +671,7 @@ export class Document { this.checkpoint = InitialCheckpoint; this.localChanges = []; - this.eventStream = createObservable>((observer) => { + this.eventStream = createObservable>((observer) => { this.eventStreamObserver = observer; }); @@ -771,7 +769,7 @@ export class Document { // 03. Publish the document change event. // NOTE(chacha912): Check opInfos, which represent the actually executed operations. - const event: TransactionEvent

= []; + const event: DocEvents

= []; if (opInfos.length > 0) { event.push({ type: DocEventType.LocalChange, @@ -1168,7 +1166,7 @@ export class Document { * `publish` triggers an event in this document, which can be received by * callback functions from document.subscribe(). */ - public publish(event: TransactionEvent

) { + public publish(event: DocEvents

) { if (this.eventStreamObserver) { this.eventStreamObserver.next(event); } @@ -1522,7 +1520,7 @@ export class Document { this.ensureClone(); change.execute(this.clone!.root, this.clone!.presences, source); - const event: TransactionEvent

= []; + const event: DocEvents

= []; const actorID = change.getID().getActorID(); if (change.hasPresenceChange() && this.onlineClients.has(actorID)) { const presenceChange = change.getPresenceChange()!; @@ -1718,9 +1716,9 @@ export class Document { } /** - * `applyDocEvent` applies the docEvent into this document. + * `applyDocEventForReplay` applies the given event into this document. */ - public applyDocEvent(event: DocEvent

) { + public applyDocEventForReplay(event: Devtools.DocEventForReplay

) { if (event.type === DocEventType.StatusChanged) { this.applyStatus(event.value.status); if (event.value.status === DocStatus.Attached) { @@ -1782,11 +1780,11 @@ export class Document { } /** - * `applyTransactionEvent` applies the given TransactionEvent into this document. + * `applyDocEventsForReplay` applies the given events into this document. */ - public applyTransactionEvent(event: TransactionEvent

) { + public applyDocEventsForReplay(event: Array>) { for (const docEvent of event) { - this.applyDocEvent(docEvent); + this.applyDocEventForReplay(docEvent); } } @@ -2034,7 +2032,7 @@ export class Document { this.localChanges.push(change); this.changeID = change.getID(); const actorID = this.changeID.getActorID(); - const event: TransactionEvent

= []; + const event: DocEvents

= []; if (opInfos.length > 0) { event.push({ type: DocEventType.LocalChange, @@ -2133,7 +2131,7 @@ export class Document { this.localChanges.push(change); this.changeID = change.getID(); const actorID = this.changeID.getActorID(); - const event: TransactionEvent

= []; + const event: DocEvents

= []; if (opInfos.length > 0) { event.push({ type: DocEventType.LocalChange, diff --git a/packages/sdk/src/yorkie.ts b/packages/sdk/src/yorkie.ts index ae1aff558..8abcb9ccc 100644 --- a/packages/sdk/src/yorkie.ts +++ b/packages/sdk/src/yorkie.ts @@ -44,8 +44,9 @@ export { DocSyncStatus, DocStatus, type Indexable, + type Json, type DocEvent, - type TransactionEvent, + type DocEvents, Document, type ChangeInfo, } from '@yorkie-js-sdk/src/document/document';