diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9b1e27be..00000000 --- a/.prettierignore +++ /dev/null @@ -1,35 +0,0 @@ -.DS_Store -node_modules -dist -coverage -docs -storybook-static -docker/**/data -types -.turbo -*.vsix -build -.docusaurus -.cache-loader - -packages-external -packages/erd-editor-vscode/public -packages/erd-editor-vscode/public-legacy - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Editor directories and files -.idea - -.vscode/* -!.vscode/launch.json -!.vscode/tasks.json - -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/erd-editor-app/package.json b/packages/erd-editor-app/package.json index fc7558c3..7e00efdd 100644 --- a/packages/erd-editor-app/package.json +++ b/packages/erd-editor-app/package.json @@ -12,6 +12,7 @@ "dependencies": { "@dineug/erd-editor": "workspace:*", "@dineug/erd-editor-shiki-worker": "workspace:*", + "@dineug/shared": "workspace:*", "@emotion/react": "^11.11.3", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/themes": "^2.0.3", @@ -25,6 +26,7 @@ "nanoid": "^5.0.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "socket.io-client": "^4.7.2", "workbox-core": "^7.0.0", "workbox-precaching": "^7.0.0", "workbox-window": "^7.0.0" diff --git a/packages/erd-editor-app/public/index.html b/packages/erd-editor-app/public/index.html index 656edf12..ebace460 100644 --- a/packages/erd-editor-app/public/index.html +++ b/packages/erd-editor-app/public/index.html @@ -16,19 +16,6 @@
- - - + <%= gtag %> diff --git a/packages/erd-editor-app/src/atoms/modules/sidebar/index.ts b/packages/erd-editor-app/src/atoms/modules/sidebar/index.ts index f2d35e7c..20fec93c 100644 --- a/packages/erd-editor-app/src/atoms/modules/sidebar/index.ts +++ b/packages/erd-editor-app/src/atoms/modules/sidebar/index.ts @@ -2,6 +2,7 @@ import { atom, useAtomValue, useSetAtom } from 'jotai'; import { loadable } from 'jotai/utils'; import { getAppDatabaseService } from '@/services/indexeddb'; +import { dispatch, replicationValueAction } from '@/utils/broadcastChannel'; export const selectedSchemaIdAtom = atom(null); @@ -40,6 +41,29 @@ const updateSchemaEntityValueAtom = atom( } ); +const replicationSchemaEntityAtom = atom( + null, + ( + get, + set, + { + id, + actions, + }: { + id: string; + actions: any; + } + ) => { + const service = getAppDatabaseService(); + if (!service) throw new Error('Database service is not initialized'); + + service.replicationSchemaEntity(id, actions); + dispatch(replicationValueAction({ id, actions })); + } +); + export const useSchemaEntity = () => useAtomValue(schemaEntityAtom); export const useUpdateSchemaEntityValue = () => useSetAtom(updateSchemaEntityValueAtom); +export const useReplicationSchemaEntity = () => + useSetAtom(replicationSchemaEntityAtom); diff --git a/packages/erd-editor-app/src/components/viewer/editor/Editor.tsx b/packages/erd-editor-app/src/components/viewer/editor/Editor.tsx index a62b042f..f94b036a 100644 --- a/packages/erd-editor-app/src/components/viewer/editor/Editor.tsx +++ b/packages/erd-editor-app/src/components/viewer/editor/Editor.tsx @@ -5,9 +5,10 @@ import { import { useAtom } from 'jotai'; import { useLayoutEffect, useRef } from 'react'; -import { useUpdateSchemaEntityValue } from '@/atoms/modules/sidebar'; +import { useReplicationSchemaEntity } from '@/atoms/modules/sidebar'; import { themeAtom } from '@/atoms/modules/theme'; import { SchemaEntity } from '@/services/indexeddb/modules/schema'; +import { bridge } from '@/utils/broadcastChannel'; import * as styles from './Editor.styles'; @@ -23,23 +24,38 @@ const Editor: React.FC = props => { const viewerRef = useRef(null); const editorRef = useRef(null); const [theme, setTheme] = useAtom(themeAtom); - const updateSchemaEntityValue = useUpdateSchemaEntityValue(); + const replicationSchemaEntity = useReplicationSchemaEntity(); useLayoutEffect(() => { const $viewer = viewerRef.current; if (!$viewer) return; + const unsubscribeSet = new Set<() => void>(); const editor = document.createElement('erd-editor'); editorRef.current = editor; editor.enableThemeBuilder = true; editor.setInitialValue(props.entity.value); - const handleChange = () => { - updateSchemaEntityValue({ - id: props.entity.id, - value: editor.value, - }); - }; + const sharedStore = editor.getSharedStore({ mouseTracker: false }); + + unsubscribeSet + .add( + sharedStore.subscribe(actions => { + replicationSchemaEntity({ + id: props.entity.id, + actions, + }); + }) + ) + .add( + bridge.on({ + replicationValue: ({ payload: { id, actions } }) => { + if (id === props.entity.id) { + sharedStore.dispatch(actions); + } + }, + }) + ); const handleChangePresetTheme = (event: Event) => { const e = event as CustomEvent; @@ -51,18 +67,18 @@ const Editor: React.FC = props => { }); }; - editor.addEventListener('change', handleChange); editor.addEventListener('changePresetTheme', handleChangePresetTheme); $viewer.appendChild(editor); return () => { $viewer.removeChild(editor); - editor.removeEventListener('change', handleChange); editor.removeEventListener('changePresetTheme', handleChangePresetTheme); + Array.from(unsubscribeSet).forEach(unsubscribe => unsubscribe()); + unsubscribeSet.clear(); editor.destroy(); editorRef.current = null; }; - }, [setTheme, updateSchemaEntityValue, props.entity.id, props.entity.value]); + }, [setTheme, replicationSchemaEntity, props.entity.id, props.entity.value]); useLayoutEffect(() => { const editor = editorRef.current; diff --git a/packages/erd-editor-app/src/internal-types/index.ts b/packages/erd-editor-app/src/internal-types/index.ts index 2decfc83..04fe9428 100644 --- a/packages/erd-editor-app/src/internal-types/index.ts +++ b/packages/erd-editor-app/src/internal-types/index.ts @@ -3,3 +3,21 @@ export type EntityType = T & { updateAt: number; createAt: number; }; + +export type ValuesType> = T[keyof T]; + +type Action = { + type: K; + payload: M[K]; +}; + +export type AnyAction

= { + type: string; + payload: P; +}; + +type Reducer = (action: Action) => void; + +export type ReducerRecord = { + [P in K]: Reducer; +}; diff --git a/packages/erd-editor-app/src/services/indexeddb/appDatabaseService.ts b/packages/erd-editor-app/src/services/indexeddb/appDatabaseService.ts index 642128c1..0876b18a 100644 --- a/packages/erd-editor-app/src/services/indexeddb/appDatabaseService.ts +++ b/packages/erd-editor-app/src/services/indexeddb/appDatabaseService.ts @@ -1,13 +1,7 @@ import Dexie, { Table } from 'dexie'; -import { - addSchemaEntity, - deleteSchemaEntity, - getSchemaEntities, - getSchemaEntity, - SchemaEntity, - updateSchemaEntity, -} from '@/services/indexeddb/modules/schema'; +import { SchemaEntity } from '@/services/indexeddb/modules/schema'; +import { SchemaService } from '@/services/indexeddb/modules/schema/service'; export class AppDatabase extends Dexie { schemas!: Table; @@ -31,30 +25,33 @@ function getDB() { } export class AppDatabaseService { - private get db() { - return getDB(); - } + private db = getDB(); + private schemaService = new SchemaService(this.db); async addSchemaEntity(entityValue: Pick) { - return await addSchemaEntity(this.db, entityValue); + return await this.schemaService.add(entityValue); } async updateSchemaEntity( id: string, entityValue: Partial> ) { - return await updateSchemaEntity(this.db, id, entityValue); + return await this.schemaService.update(id, entityValue); } async deleteSchemaEntity(id: string) { - return await deleteSchemaEntity(this.db, id); + return await this.schemaService.delete(id); } async getSchemaEntity(id: string) { - return await getSchemaEntity(this.db, id); + return await this.schemaService.get(id); } async getSchemaEntities() { - return await getSchemaEntities(this.db); + return await this.schemaService.getEntities(); + } + + async replicationSchemaEntity(id: string, actions: any) { + await this.schemaService.replication(id, actions); } } diff --git a/packages/erd-editor-app/src/services/indexeddb/modules/schema/service.ts b/packages/erd-editor-app/src/services/indexeddb/modules/schema/service.ts new file mode 100644 index 00000000..4e338e88 --- /dev/null +++ b/packages/erd-editor-app/src/services/indexeddb/modules/schema/service.ts @@ -0,0 +1,94 @@ +import { + createReplicationStore, + ReplicationStore, +} from '@dineug/erd-editor/engine.js'; +import { omit } from 'lodash-es'; + +import { type AppDatabase } from '@/services/indexeddb/appDatabaseService'; +import { + addSchemaEntity, + deleteSchemaEntity, + getSchemaEntities, + getSchemaEntity, + SchemaEntity, + updateSchemaEntity, +} from '@/services/indexeddb/modules/schema'; + +export class SchemaService { + private cache = new Map(); + constructor(private db: AppDatabase) {} + + async add(entityValue: Pick) { + const result = await addSchemaEntity(this.db, entityValue); + + const store = createReplicationStore({ toWidth }); + store.on({ + change: () => { + this.update(result.id, { value: store.value }); + }, + }); + this.cache.set(result.id, { ...result, store }); + + return result; + } + + async update( + id: string, + entityValue: Partial> + ) { + const result = await updateSchemaEntity(this.db, id, entityValue); + + const prev = this.cache.get(id); + if (prev && result) { + this.cache.set(id, { ...prev, ...entityValue }); + } + + return result; + } + + async delete(id: string) { + const result = await deleteSchemaEntity(this.db, id); + this.cache.delete(id); + return result; + } + + async get(id: string) { + const prev = this.cache.get(id); + if (prev) return omit(prev, 'store'); + + const result = await getSchemaEntity(this.db, id); + if (result) { + const store = createReplicationStore({ toWidth }); + store.setInitialValue(result.value); + store.on({ + change: () => { + this.update(result.id, { value: store.value }); + }, + }); + this.cache.set(id, { ...result, store }); + } + + return result; + } + + async getEntities() { + return await getSchemaEntities(this.db); + } + + async replication(id: string, actions: any) { + let prev = this.cache.get(id); + + if (!prev) { + await this.get(id); + prev = this.cache.get(id); + } + + if (prev) { + prev.store.dispatch(actions as any); + } + } +} + +function toWidth(value: string) { + return value.length * 11; +} diff --git a/packages/erd-editor-app/src/utils/broadcastChannel.ts b/packages/erd-editor-app/src/utils/broadcastChannel.ts new file mode 100644 index 00000000..3c41bf15 --- /dev/null +++ b/packages/erd-editor-app/src/utils/broadcastChannel.ts @@ -0,0 +1,61 @@ +import { isObject, safeCallback } from '@dineug/shared'; + +import { AnyAction, ReducerRecord, ValuesType } from '@/internal-types'; + +const BridgeActionType = { + replicationValue: 'replicationValue', +} as const; +type BridgeActionType = ValuesType; + +type BridgeActionMap = { + [BridgeActionType.replicationValue]: { + id: string; + actions: any; + }; +}; + +function createAction

(type: string) { + function actionCreator(payload: P): AnyAction

{ + return { type, payload }; + } + + actionCreator.toString = () => `${type}`; + actionCreator.type = type; + return actionCreator; +} + +export class Emitter { + #observers = new Set>>(); + + on(reducers: Partial>) { + this.#observers.has(reducers) || this.#observers.add(reducers); + + return () => { + this.#observers.delete(reducers); + }; + } + + emit(action: AnyAction) { + if (!isObject(action)) return; + this.#observers.forEach(reducers => { + const reducer = Reflect.get(reducers, action.type); + safeCallback(reducer, action); + }); + } +} + +const channel = new BroadcastChannel('@@bridge'); +export const bridge = new Emitter(); + +export function dispatch(action: AnyAction) { + channel.postMessage(action); +} + +channel.addEventListener('message', event => { + const action = event.data; + bridge.emit(action); +}); + +export const replicationValueAction = createAction< + BridgeActionMap[typeof BridgeActionType.replicationValue] +>(BridgeActionType.replicationValue); diff --git a/packages/erd-editor-app/webpack.config.js b/packages/erd-editor-app/webpack.config.js index a172214c..46895474 100644 --- a/packages/erd-editor-app/webpack.config.js +++ b/packages/erd-editor-app/webpack.config.js @@ -153,6 +153,9 @@ module.exports = (env, argv) => { new HtmlWebpackPlugin({ inject: true, template: resolvePath('public/index.html'), + templateParameters: { + gtag: isProduction ? toGtag() : '', + }, }), isDevelopment && new ReactRefreshWebpackPlugin(), // new BundleAnalyzerPlugin(), @@ -162,3 +165,16 @@ module.exports = (env, argv) => { return config; }; + +function toGtag() { + return /*html*/ ` + +`; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a14710b..025e653a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -419,6 +419,9 @@ importers: '@dineug/erd-editor-shiki-worker': specifier: workspace:* version: link:../erd-editor-shiki-worker + '@dineug/shared': + specifier: workspace:* + version: link:../shared '@emotion/react': specifier: ^11.11.3 version: 11.11.3(@types/react@18.2.45)(react@18.2.0) @@ -458,6 +461,9 @@ importers: react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) + socket.io-client: + specifier: ^4.7.2 + version: 4.7.2 workbox-core: specifier: ^7.0.0 version: 7.0.0 @@ -5430,6 +5436,10 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@storybook/addon-actions@7.6.5: resolution: {integrity: sha512-lW/m9YcaNfBZk+TZLxyzHdd563mBWpsUIveOKYjcPdl/q0FblWWZrRsFHqwLK1ldZ4AZXs8J/47G8CBr6Ew2uQ==} dependencies: @@ -9002,6 +9012,25 @@ packages: once: 1.4.0 dev: true + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4(supports-color@5.5.0) + engine.io-parser: 5.2.1 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -13255,6 +13284,30 @@ packages: is-fullwidth-code-point: 5.0.0 dev: true + /socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4(supports-color@5.5.0) + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + dev: false + /sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} dependencies: @@ -15218,6 +15271,19 @@ packages: optional: true dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.13.0: resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} engines: {node: '>=10.0.0'} @@ -15271,6 +15337,11 @@ packages: engines: {node: '>=4.0'} dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'}