From 73d872b2380e21a5584404321d1277a5bf5d90cb Mon Sep 17 00:00:00 2001 From: undefined Date: Tue, 28 Feb 2023 08:54:58 +0800 Subject: [PATCH] ui: add cordis --- packages/ui-default/api.ts | 4 +- packages/ui-default/bus.ts | 94 ------------------- .../scratchpad/ScratchpadEditorContainer.tsx | 3 +- .../scratchpad/{index.jsx => index.tsx} | 50 ++++++++-- packages/ui-default/context.ts | 49 ++++++++++ packages/ui-default/package.json | 1 + ...ssages.page.jsx => home_messages.page.tsx} | 16 +++- 7 files changed, 110 insertions(+), 107 deletions(-) delete mode 100644 packages/ui-default/bus.ts rename packages/ui-default/components/scratchpad/{index.jsx => index.tsx} (76%) create mode 100644 packages/ui-default/context.ts rename packages/ui-default/pages/{home_messages.page.jsx => home_messages.page.tsx} (83%) diff --git a/packages/ui-default/api.ts b/packages/ui-default/api.ts index 8b8c7add3..e18b4d24d 100644 --- a/packages/ui-default/api.ts +++ b/packages/ui-default/api.ts @@ -3,7 +3,7 @@ export * from './utils'; export { default as Notification } from './components/notification'; export * from './components/dialog'; -export * as bus from './bus'; +export { Context, Service, ctx } from './context'; export { default as AnsiUp } from 'ansi_up'; export { default as loadMonaco } from './components/monaco/loader'; export { default as uploadFiles } from './components/upload'; @@ -36,6 +36,8 @@ export default async function load(name: string) { return lazyModules[name]; } +export interface EventMap { } + import AutoComplete from './components/autocomplete'; import CustomSelectAutoComplete from './components/autocomplete/CustomSelectAutoComplete'; import DomainSelectAutoComplete from './components/autocomplete/DomainSelectAutoComplete'; diff --git a/packages/ui-default/bus.ts b/packages/ui-default/bus.ts deleted file mode 100644 index 66879daa0..000000000 --- a/packages/ui-default/bus.ts +++ /dev/null @@ -1,94 +0,0 @@ -/* eslint-disable no-await-in-loop */ -import type * as monaco from 'monaco-editor'; - -// eslint-disable-next-line @typescript-eslint/naming-convention -const _hooks: Record any>> = {}; - -function isBailed(value: any) { - return value !== null && value !== false && value !== undefined; -} - -export type Disposable = () => void; -export type VoidReturn = Promise | any; - -export interface EventMap extends Record { - 'scratchpadEditorCreate': (editor: monaco.editor.IStandaloneCodeEditor, monaco: typeof import('monaco-editor')) => any; -} - -function getHooks(name: K) { - const hooks = (_hooks[name] ||= []); - if (hooks.length >= 2048) { - console.warn( - 'max listener count (2048) for event "%s" exceeded, which may be caused by a memory leak', - name, - ); - } - return hooks; -} - -export function removeListener(name: K, listener: EventMap[K]) { - const index = (_hooks[name] || []).findIndex((callback) => callback === listener); - if (index >= 0) { - _hooks[name].splice(index, 1); - return true; - } - return false; -} - -export function addListener(name: K, listener: EventMap[K]) { - getHooks(name).push(listener); - return () => removeListener(name, listener); -} - -export function prependListener(name: K, listener: EventMap[K]) { - getHooks(name).unshift(listener); - return () => removeListener(name, listener); -} - -export function once(name: K, listener: EventMap[K]) { - let dispose; - function l(...args: any[]) { - dispose(); - return listener.apply(this, args); - } - l.toString = () => `// Once \n${listener.toString()}`; - dispose = addListener(name, l); - return dispose; -} - -export function on(name: K, listener: EventMap[K]) { - return addListener(name, listener); -} - -export function off(name: K, listener: EventMap[K]) { - return removeListener(name, listener); -} - -export async function parallel(name: K, ...args: Parameters): Promise { - const tasks: Promise[] = []; - for (const callback of _hooks[name] || []) { - tasks.push(callback.apply(this, args)); - } - await Promise.all(tasks); -} - -export function emit(name: K, ...args: Parameters) { - return parallel(name, ...args); -} - -export async function serial(name: K, ...args: Parameters): Promise { - const hooks = Array.from(_hooks[name] || []); - for (const callback of hooks) { - await callback.apply(this, args); - } -} - -export async function bail(name: K, ...args: Parameters): Promise> { - const hooks = Array.from(_hooks[name] || []); - for (const callback of hooks) { - let result = callback.apply(this, args); - if (result instanceof Promise) result = await result; - if (isBailed(result)) return result; - } - return null; -} diff --git a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx index 0b6bf7382..a368b58e6 100644 --- a/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx +++ b/packages/ui-default/components/scratchpad/ScratchpadEditorContainer.tsx @@ -2,6 +2,7 @@ import type * as monaco from 'monaco-editor'; import React from 'react'; import { connect } from 'react-redux'; import { load } from 'vj/components/monaco/loader'; +import { ctx } from 'vj/context'; interface ScratchpadOptions { value?: string; @@ -59,7 +60,7 @@ export default connect((state: any) => ({ ); (window as any).editor = this.editor; (window as any).monaco = monaco; - window.Hydro.bus.emit('scratchpadEditorCreate', this.editor, monaco); + ctx.scratchpad.init(this.editor, monaco); } } diff --git a/packages/ui-default/components/scratchpad/index.jsx b/packages/ui-default/components/scratchpad/index.tsx similarity index 76% rename from packages/ui-default/components/scratchpad/index.jsx rename to packages/ui-default/components/scratchpad/index.tsx index 14ced035e..33f0460c9 100644 --- a/packages/ui-default/components/scratchpad/index.jsx +++ b/packages/ui-default/components/scratchpad/index.tsx @@ -1,11 +1,13 @@ import ProblemIcon from '@vscode/codicons/src/icons/file.svg?react'; import $ from 'jquery'; import _ from 'lodash'; +import type * as monaco from 'monaco-editor'; import React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector, useStore } from 'react-redux'; import SplitPane from 'react-split-pane'; import Dom from 'vj/components/react/DomComponent'; import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent'; +import { Context, ctx, Service } from 'vj/context'; import ScratchpadEditor from './ScratchpadEditorContainer'; import ScratchpadPretest from './ScratchpadPretestContainer'; import ScratchpadRecords from './ScratchpadRecordsContainer'; @@ -40,23 +42,51 @@ const pages = { }; let rerenderCallback = null; -export function addPage(key, icon, component) { - pages[key] = { - icon, - component, - }; - rerenderCallback?.(); +class ScratchpadService extends Service { + constructor(public store) { + super(ctx, 'scratchpad', true); + this.load = new Promise((resolve) => { + this.loadCallback = resolve; + }); + } + + pages = pages; + load: Promise; + loadCallback: () => void; + editor: monaco.editor.IStandaloneCodeEditor; + monaco: typeof import('monaco-editor'); + + init(editor: monaco.editor.IStandaloneCodeEditor, monaco: typeof import('monaco-editor')) { + this.editor = editor; + this.monaco = monaco; + this.loadCallback(); + } + + addPage(key, icon, component) { + pages[key] = { + icon, + component, + }; + rerenderCallback?.(); + } +} +Context.service('scratchpad', ScratchpadService); +declare module '../../context' { + interface Context { + scratchpad: ScratchpadService; + } } -window.Hydro.scratchpad = { addPage, pages }; export default function ScratchpadContainer() { - const [, updateState] = React.useState(); + const store = useStore(); + ctx.scratchpad ||= new ScratchpadService(store); + const [, updateState] = React.useState(); const forceUpdate = React.useCallback(() => updateState({}), []); React.useEffect(() => { rerenderCallback = forceUpdate; }, []); const dispatch = useDispatch(); - const ui = useSelector((state) => state.ui, _.isEqual); + const ui = useSelector((state) => state.ui, _.isEqual); const handleChangeSize = _.debounce((uiElement, size) => { dispatch({ diff --git a/packages/ui-default/context.ts b/packages/ui-default/context.ts new file mode 100644 index 000000000..14b22cbf1 --- /dev/null +++ b/packages/ui-default/context.ts @@ -0,0 +1,49 @@ +import * as cordis from 'cordis'; +import type { EventMap } from './api'; + +export interface Events extends cordis.Events, EventMap { +} + +export interface Context { + [Context.events]: Events; + setTimeout: typeof setTimeout; + setInterval: typeof setInterval; + setImmediate: typeof setImmediate; + api: ApiMixin; +} + +export class Context extends cordis.Context { + static readonly session = Symbol('session'); +} + +export namespace Context { + export interface Config extends cordis.Context.Config { } +} + +export type MainScope = cordis.MainScope; +export type EffectScope = cordis.EffectScope; +export type ForkScope = cordis.ForkScope; +export type Plugin = cordis.Plugin; +export const Service = cordis.Service; +export namespace Plugin { + export type Function = cordis.Plugin.Function; + export type Constructor = cordis.Plugin.Constructor; + export type Object = cordis.Plugin.Object; +} + +const T = any>(origFunc: F, disposeFunc?) => + function method(this: cordis.Service, ...args: Parameters) { + const res = origFunc(...args); + this.caller?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res())); + }; +export class ApiMixin extends Service { + static readonly methods = ['addScript', 'setInterval', 'setTimeout', 'setImmediate', 'provideModule', 'inject', 'broadcast']; + setInterval = T(setInterval, clearInterval); + setTimeout = T(setTimeout, clearTimeout); + setImmediate = T(setImmediate, clearImmediate); + constructor(ctx) { + super(ctx, 'api', true); + } +} +Context.service('api', ApiMixin); +export const ctx = new Context(); diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 32f31ef2b..c851c4da1 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -45,6 +45,7 @@ "chalk": "^5.2.0", "classnames": "^2.3.2", "clipboard": "^2.0.11", + "cordis": "^2.7.3", "diff": "^5.1.0", "diff-dom": "^5.0.4", "echarts": "^5.4.1", diff --git a/packages/ui-default/pages/home_messages.page.jsx b/packages/ui-default/pages/home_messages.page.tsx similarity index 83% rename from packages/ui-default/pages/home_messages.page.jsx rename to packages/ui-default/pages/home_messages.page.tsx index 7ad8f05ae..7555bd17a 100644 --- a/packages/ui-default/pages/home_messages.page.jsx +++ b/packages/ui-default/pages/home_messages.page.tsx @@ -3,9 +3,22 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import VjNotification from 'vj/components/notification'; import selectUser from 'vj/components/selectUser'; +import { Context, ctx, Service } from 'vj/context'; import { NamedPage } from 'vj/misc/Page'; import { api, gql, loadReactRedux } from 'vj/utils'; +class MessagePadService extends Service { + constructor(public store, public WebSocket: typeof import('../components/socket').default) { + super(ctx, 'messagepad', true); + } +} +Context.service('messagepad', MessagePadService); +declare module '../context' { + interface Context { + messagepad: MessagePadService; + } +} + const page = new NamedPage('home_messages', () => { let reduxStore; @@ -29,7 +42,8 @@ const page = new NamedPage('home_messages', () => { const { Provider, store } = await loadReactRedux(MessagePadReducer); reduxStore = store; - window.store = reduxStore; + (window as any).store = reduxStore; + ctx.messagepad = new MessagePadService(store, WebSocket); const sock = new WebSocket(`${UiContext.ws_prefix}home/messages-conn`); sock.onmessage = (message) => {