重新设计了subscriber和socket

This commit is contained in:
Xu Chang 2024-11-08 14:50:31 +08:00
parent a0a25d8445
commit 4a94777066
45 changed files with 1266 additions and 123 deletions

View File

@ -2,12 +2,12 @@ import { ED, OakAbsAttrDef } from '../../types/AbstractComponent';
import { ReactComponentProps } from '../../types/Page';
import { Breakpoint } from 'antd';
declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
column?: number | Record<Breakpoint, number> | undefined;
column?: number | Record<Breakpoint, number>;
entity: T2;
attributes: OakAbsAttrDef[];
data: Partial<ED2[T2]["Schema"]>;
title?: string | undefined;
bordered?: boolean | undefined;
layout?: "horizontal" | "vertical" | undefined;
data: Partial<ED2[T2]['Schema']>;
title?: string;
bordered?: boolean;
layout?: 'horizontal' | 'vertical';
}>) => React.ReactElement;
export default _default;

View File

@ -3,8 +3,8 @@ import { ReactComponentProps } from '../../types/Page';
import { ECode } from '../../types/ErrorPage';
declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
code: ECode;
title?: string | undefined;
desc?: string | undefined;
title?: string;
desc?: string;
children?: React.ReactNode;
icon?: React.ReactNode;
}>) => React.ReactElement;

View File

@ -10,18 +10,13 @@ declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactCompo
data: RowWithActions<ED2, T2>[];
loading: boolean;
tablePagination?: React.ReactNode | false;
rowSelection?: import("antd/es/table/interface").TableRowSelection<RowWithActions<ED2, T2>> | undefined;
hideHeader?: boolean | undefined;
disableSerialNumber?: boolean | undefined;
size?: "small" | "large" | "middle" | undefined;
scroll?: ({
x?: string | number | true | undefined;
y?: string | number | undefined;
} & {
scrollToFirstRowOnChange?: boolean | undefined;
}) | undefined;
rowSelection?: TableProps<RowWithActions<ED2, T2>>['rowSelection'];
hideHeader?: boolean;
disableSerialNumber?: boolean;
size?: 'large' | 'middle' | 'small';
scroll?: TableProps<RowWithActions<ED2, T2>>['scroll'];
empty?: React.ReactNode;
opWidth?: number | undefined;
ellipsis?: boolean | undefined;
opWidth?: number;
ellipsis?: boolean;
}>) => React.ReactElement;
export default _default;

View File

@ -1,21 +1,20 @@
/// <reference types="react" />
import { ReactComponentProps } from '../../types/Page';
import { ED } from '../../types/AbstractComponent';
declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
style?: import("react").CSSProperties | undefined;
className?: string | undefined;
style?: React.CSSProperties;
className?: string;
title?: React.ReactNode;
showBack?: boolean | undefined;
onBack?: (() => void) | undefined;
showBack?: boolean;
onBack?: () => void;
backIcon?: React.ReactNode;
delta?: number | undefined;
delta?: number;
extra?: React.ReactNode;
subTitle?: React.ReactNode;
contentMargin?: boolean | undefined;
contentStyle?: import("react").CSSProperties | undefined;
contentClassName?: string | undefined;
contentMargin?: boolean;
contentStyle?: React.CSSProperties;
contentClassName?: string;
tags?: React.ReactNode;
children?: React.ReactNode;
showHeader?: boolean | undefined;
showHeader?: boolean;
}>) => React.ReactElement;
export default _default;

View File

@ -1,23 +1,22 @@
/// <reference types="react" />
import { ReactComponentProps } from '../../types/Page';
import { ED } from '../../types/AbstractComponent';
declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
style?: import("react").CSSProperties | undefined;
className?: string | undefined;
showHeader?: boolean | undefined;
showBack?: boolean | undefined;
onBack?: (() => void) | undefined;
style?: React.CSSProperties;
className?: string;
showHeader?: boolean;
showBack?: boolean;
onBack?: () => void;
backIcon?: React.ReactNode;
delta?: number | undefined;
delta?: number;
title?: React.ReactNode;
subTitle?: React.ReactNode;
tags?: React.ReactNode;
extra?: React.ReactNode;
children?: React.ReactNode;
content: React.ReactNode;
contentStyle?: import("react").CSSProperties | undefined;
contentClassName?: string | undefined;
bodyStyle?: import("react").CSSProperties | undefined;
bodyClassName?: string | undefined;
contentStyle?: React.CSSProperties;
contentClassName?: string;
bodyStyle?: React.CSSProperties;
bodyClassName?: string;
}>) => React.ReactElement;
export default _default;

View File

@ -1,13 +1,12 @@
/// <reference types="react" />
import { ReactComponentProps } from '../../types/Page';
import { ED } from '../../types/AbstractComponent';
declare const _default: <ED2 extends ED, T2 extends keyof ED2>(props: ReactComponentProps<ED2, T2, false, {
entity: T2;
style?: import("react").CSSProperties | undefined;
className?: string | undefined;
showQuickJumper?: boolean | undefined;
size?: "small" | "default" | undefined;
showSizeChanger?: boolean | undefined;
showTotal?: ((total: number, range: [number, number]) => React.ReactNode) | undefined;
style?: React.CSSProperties;
className?: string;
showQuickJumper?: boolean;
size?: 'default' | 'small';
showSizeChanger?: boolean;
showTotal?: (total: number, range: [number, number]) => React.ReactNode;
}>) => React.ReactElement;
export default _default;

View File

@ -4,7 +4,7 @@ declare const _default: <ED2 extends ED, T2 extends keyof ED2, T3 extends string
helps: Record<string, string>;
entity: T2;
attributes: OakAbsAttrUpsertDef<ED2, T2, T3>[];
data: ED2[T2]["Schema"];
data: ED2[T2]['Schema'];
layout: 'horizontal' | 'vertical';
mode: 'default' | 'card';
}>) => React.ReactElement;

View File

@ -1,7 +1,7 @@
import { EntityDict } from 'oak-domain/lib/base-app-domain';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
import { SyncContext, SyncRowStore } from 'oak-domain/lib/store/SyncRowStore';
import { SubScriber } from '../features/subscriber';
import { SubScriber } from '../features/socket/subscriber';
import { Environment } from '../features/environment';
import { Navigator } from '../features/navigator';
import { BriefEnv } from 'oak-domain/lib/types/Environment';

View File

@ -13,13 +13,15 @@ import { CacheStore } from '../cacheStore/CacheStore';
import { Navigator } from './navigator';
import { Port } from './port';
import { Style } from './style';
import { SubScriber } from './subscriber';
import { SubScriber } from './socket/subscriber';
import { Socket } from './socket/socket';
import { ContextMenuFactory } from './contextMenuFactory';
import { Geo } from './geo';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CommonConfiguration, RenderConfiguration } from 'oak-domain/lib/types/Configuration';
export declare function initializeStep2<ED extends EntityDict & BaseEntityDict, FrontCxt extends SyncContext<ED>>(features: Pick<BasicFeatures<ED>, 'localStorage' | 'environment' | 'message'>, connector: Connector<ED, FrontCxt>, storageSchema: StorageSchema<ED>, frontendContextBuilder: (store: CacheStore<ED>) => SyncContext<ED>, checkers: Array<Checker<ED, keyof ED, FrontCxt>>, common: CommonConfiguration<ED>, render: RenderConfiguration<ED>): {
cache: Cache<ED>;
socket: Socket;
runningTree: RunningTree<ED>;
locales: Locales<ED>;
port: Port<ED>;

View File

@ -10,7 +10,9 @@ import { Navigator } from './navigator';
import { Port } from './port';
// import { RelationAuth } from './relationAuth';
import { Style } from './style';
import { SubScriber } from './subscriber';
import { SubScriber } from './socket/subscriber';
import SocketPoint from './socket/socketPoint';
import { Socket } from './socket/socket';
import { ContextMenuFactory } from './contextMenuFactory';
import { Geo } from './geo';
export function initializeStep2(features, connector, storageSchema, frontendContextBuilder, checkers, common, render) {
@ -22,10 +24,12 @@ export function initializeStep2(features, connector, storageSchema, frontendCont
const style = new Style(render.styleDict);
const locales = new Locales(cache, localStorage, environment, 'zh-CN'); // 临时性代码,应由上层传入
const contextMenuFactory = new ContextMenuFactory(cache);
const subscriber = new SubScriber(cache, message, () => connector.getSubscribePoint());
const socketPoint = new SocketPoint(() => connector.getSocketPoint());
const subscriber = new SubScriber(cache, message, socketPoint);
const socket = new Socket(message, socketPoint);
return {
cache,
// relationAuth,
socket,
runningTree,
locales,
port,

29
es/features/socket/socket.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
import { Message } from '../message';
import { Feature } from '../../types/Feature';
import SocketPoint from './socketPoint';
export type SocketEvent = {
event: 'connect';
} | {
event: 'disconnect';
} | {
event: 'connect_error';
reason: any;
} | {
event: 'data';
data: any;
};
export declare class Socket extends Feature {
private socketPoint;
private url?;
private path?;
private io?;
private message;
constructor(message: Message, socketPoint: SocketPoint);
private initSocketPoint;
private connectQueue;
private ioState;
private onConnected;
connect(): Promise<unknown>;
send(data: any, callback?: Function): Promise<void>;
disconnect(): void;
}

View File

@ -0,0 +1,100 @@
import { OakSocketConnectException } from 'oak-domain/lib/types/Exception';
import { Feature } from '../../types/Feature';
import io from '../../utils/socket.io/socket.io';
import assert from 'assert';
export class Socket extends Feature {
socketPoint;
url;
path;
io;
message;
constructor(message, socketPoint) {
super();
this.message = message;
this.socketPoint = socketPoint;
}
async initSocketPoint() {
try {
const { socketUrl, path } = await this.socketPoint.get();
this.url = socketUrl;
this.path = path;
}
catch (err) {
this.url = undefined;
this.path = undefined;
throw new OakSocketConnectException();
}
}
connectQueue = [];
ioState = 'unconnected';
onConnected() {
this.io.off('disconnect');
this.io.off('data');
this.io.on('disconnect', () => {
this.publish({
event: 'disconnect',
});
this.ioState = 'unconnected';
this.io = undefined;
});
this.io.on('data', (data) => {
this.publish({
event: 'data',
data,
});
});
}
async connect() {
if (this.ioState === 'connecting') {
const p = new Promise((resolve, reject) => {
this.connectQueue.push([resolve, reject]);
});
return p;
}
else {
assert(this.ioState === 'unconnected');
this.ioState = 'connecting';
await this.initSocketPoint();
this.io = io(this.url, {
path: this.path,
});
return new Promise((resolve, reject) => {
this.io.off('connect');
this.io.on('connect', () => {
this.publish({
event: 'connect',
});
this.ioState = 'connected';
this.connectQueue.forEach(([resolve]) => resolve());
this.onConnected();
resolve(undefined);
});
this.io.on('connect_error', (err) => {
this.message.setMessage({
type: 'error',
title: 'connect error',
content: err.message,
});
this.publish({
event: 'connect_error',
reason: err,
});
this.ioState = 'unconnected';
this.connectQueue.forEach(([resolve, reject]) => reject(err));
reject(err);
});
});
}
}
async send(data, callback) {
if (!this.io) {
await this.connect();
}
this.io.emit('data', data, callback);
}
disconnect() {
assert(this.ioState === 'connected');
this.io.disconnect();
}
}
;

13
es/features/socket/socketPoint.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
type SocketPointInfo = {
path: string;
subscribeUrl: string;
socketUrl: string;
};
export default class SocketPoint {
getSocketPointFn: () => Promise<SocketPointInfo>;
private socketPointInfo?;
constructor(getSocketPointFn: () => Promise<SocketPointInfo>);
init(): Promise<void>;
get(): Promise<SocketPointInfo>;
}
export {};

View File

@ -0,0 +1,22 @@
import { OakSocketConnectException } from "oak-domain/lib/types/Exception";
export default class SocketPoint {
getSocketPointFn;
socketPointInfo;
constructor(getSocketPointFn) {
this.getSocketPointFn = getSocketPointFn;
}
async init() {
try {
this.socketPointInfo = await this.getSocketPointFn();
}
catch (err) {
throw new OakSocketConnectException();
}
}
async get() {
if (!this.socketPointInfo) {
await this.init();
}
return this.socketPointInfo;
}
}

34
es/features/socket/subscriber.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import { EntityDict, OpRecord } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Cache } from '../cache';
import { Message } from '../message';
import { Feature } from '../../types/Feature';
import SocketPoint from './socketPoint';
type SubscribeEvent = 'connect' | 'disconnect';
type Callback<ED extends EntityDict & BaseEntityDict> = (event: string, records: OpRecord<ED>[]) => void;
export declare class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature {
private cache;
private message;
private socketPoint;
private eventMap;
private url?;
private path?;
private socket?;
private halted;
private reconnectInterval;
private reconnectTimer;
private count;
private socketState;
private eventCallbackMap;
constructor(cache: Cache<ED>, message: Message, socketPoint: SocketPoint);
on(event: SubscribeEvent, callback: (...data: any) => void): void;
off(event: SubscribeEvent, callback: () => void): void;
private emit;
private initSocketPoint;
private connect;
sub(events: string[], callback?: Callback<ED>): Promise<() => void>;
private unsub;
getSubscriberId(): string | undefined;
setHalted(halted: boolean): void;
}
export {};

View File

@ -0,0 +1,14 @@
import { EntityDict, OpRecord, SubDataDef } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Feature } from '../../types/Feature';
type SubscribeEvent = 'connect' | 'disconnect';
export declare class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature {
private eventCallbackMap;
constructor();
on(event: SubscribeEvent, callback: () => void): void;
off(event: SubscribeEvent, callback: () => void): void;
sub(data: SubDataDef<ED, keyof ED>[], callback: (records: OpRecord<ED>[], ids: string[]) => void): Promise<void>;
unsub(ids: string[]): Promise<void>;
getSubscriberId(): undefined;
}
export {};

View File

@ -0,0 +1,24 @@
import { pull } from 'oak-domain/lib/utils/lodash';
import { Feature } from '../../types/Feature';
export class SubScriber extends Feature {
eventCallbackMap = {
connect: [],
disconnect: [],
};
constructor() {
super();
}
on(event, callback) {
this.eventCallbackMap[event].push(callback);
}
off(event, callback) {
pull(this.eventCallbackMap[event], callback);
}
async sub(data, callback) {
console.log('data subscribe 在dev模式下不起作用');
}
async unsub(ids) { }
getSubscriberId() {
return undefined;
}
}

View File

@ -0,0 +1,244 @@
import { assert } from 'oak-domain/lib/utils/assert';
import { pull, unset } from 'oak-domain/lib/utils/lodash';
import { OakSocketConnectException } from 'oak-domain/lib/types/Exception';
import io from '../../utils/socket.io/socket.io';
import { Feature } from '../../types/Feature';
export class SubScriber extends Feature {
cache;
message;
socketPoint;
eventMap = {};
url;
path;
socket;
halted;
reconnectInterval;
reconnectTimer;
count;
socketState = 'unconnected';
eventCallbackMap = {
connect: [],
disconnect: [],
};
constructor(cache, message, socketPoint) {
super();
this.cache = cache;
this.message = message;
this.socketPoint = socketPoint;
this.halted = false;
this.reconnectInterval = 10 * 1000; // 每隔10秒重连
this.reconnectTimer = undefined;
this.count = 0;
}
on(event, callback) {
this.eventCallbackMap[event].push(callback);
}
off(event, callback) {
pull(this.eventCallbackMap[event], callback);
}
emit(event, ...data) {
this.eventCallbackMap[event].forEach((ele) => ele(data));
}
async initSocketPoint() {
try {
const { subscribeUrl, path } = await this.socketPoint.get();
this.url = subscribeUrl;
this.path = path;
}
catch (err) {
this.url = undefined;
this.path = undefined;
throw new OakSocketConnectException();
}
}
async connect() {
if (this.halted) {
return;
}
this.socketState = 'connecting';
let optionInited = false;
if (!this.url) {
await this.initSocketPoint();
optionInited = true;
this.count = 0;
}
const url = this.url;
const path = this.path;
// import { io } from "socket.io-client";
// const socket = io('https://example.com/order', {
// path: '/my-custom-path/',
// });
// the Socket instance is attached to the "order" Namespace
// the HTTP requests will look like: GET https://example.com/my-custom-path/?EIO=4&transport=polling&t=ML4jUwU
// 文档 https://socket.io/docs/v4/client-options/
if (!this.socket) {
// 理论上有可能出现this.socket初始化时url和path和这次connect时发生变化暂时不处理。by Xc
this.socket = io(url, {
path,
});
}
const socket = this.socket;
return new Promise((resolve, reject) => {
/**
* https://socket.io/zh-CN/docs/v4/client-socket-instance/
*/
socket.on('connect', async () => {
this.socketState = 'connected';
this.emit('connect');
socket.off('connect');
socket.on('disconnect', () => {
this.socketState = 'unconnected';
this.emit('disconnect');
socket.removeAllListeners();
if (Object.keys(this.eventMap).length > 0) {
this.connect();
}
});
socket.on('data', (opRecords, event) => {
const registered = this.eventMap[event];
if (registered) {
registered.callbacks.forEach((ele) => ele(event, opRecords));
}
this.cache.sync(opRecords);
});
socket.on('error', (errString) => {
console.error(errString);
this.message.setMessage({
type: 'error',
title: '服务器subscriber抛出异常',
content: errString,
});
});
if (Object.keys(this.eventMap).length > 0) {
socket.emit('sub', Object.keys(this.eventMap));
resolve(undefined);
}
else {
resolve(undefined);
socket.disconnect();
}
});
if (!optionInited) {
socket.on('connect_error', async (err) => {
this.count++;
if (this.count > 50) {
// 可能socket地址改变了刷新重连
this.url = undefined;
}
socket.removeAllListeners();
socket.disconnect();
this.socket = undefined;
// 清除之前的重连定时器
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer);
}
// 根据重连次数设置不同的重连间隔
if (this.count <= 10) {
this.reconnectInterval = 10 * 1000; // 10秒
}
else if (this.count <= 20) {
this.reconnectInterval = 30 * 1000; // 30秒
}
else if (this.count <= 30) {
this.reconnectInterval = 60 * 1000; // 60秒
}
else if (this.count <= 40) {
this.reconnectInterval = 90 * 1000; // 90秒
}
else {
this.reconnectInterval = 120 * 1000; // 2分钟
}
// 设置新的定时器
this.reconnectTimer = setInterval(async () => {
await this.connect();
// resolve(undefined);
}, this.reconnectInterval);
resolve(undefined);
});
}
socket.connect();
});
}
async sub(events, callback) {
const newEvents = [];
events.forEach((event) => {
const registered = this.eventMap[event];
if (registered) {
if (callback) {
registered.callbacks.push(callback);
}
registered.count++;
}
else {
this.eventMap[event] = {
callbacks: callback ? [callback] : [],
count: 1,
};
newEvents.push(event);
}
;
});
if (this.socketState === 'unconnected') {
await this.connect();
}
else if (this.socketState === 'connected' && newEvents.length > 0) {
await new Promise((resolve, reject) => {
// this.socket!.emit('sub', newEvents, (result: string) => {
// if (result) {
// this.message.setMessage({
// type: 'error',
// title: 'sub data error',
// content: result,
// });
// reject();
// }
// else {
// resolve(undefined);
// }
// });
// TODO 临时代码 后续结合文档再解决
this.socket.emit('sub', newEvents);
resolve(undefined);
});
}
return () => {
this.unsub(events, callback);
};
}
async unsub(events, callback) {
const invalidEvents = [];
events.forEach((event) => {
const registered = this.eventMap[event];
assert(registered);
registered.count--;
if (registered.count > 0) {
if (callback) {
pull(registered.callbacks, callback);
}
}
else {
invalidEvents.push(event);
}
});
invalidEvents.forEach(event => unset(this.eventMap, event));
if (this.socketState === 'connected') {
this.socket.emit('unsub', events);
if (Object.keys(this.eventMap).length === 0) {
this.socket.disconnect();
this.socket.removeAllListeners();
this.socketState = 'unconnected';
}
}
}
getSubscriberId() {
if (this.socket) {
return this.socket.id;
}
}
setHalted(halted) {
this.halted = halted;
if (halted === false && Object.keys(this.eventMap).length > 0) {
this.connect();
}
}
}

3
es/initialize.d.ts vendored
View File

@ -28,12 +28,13 @@ export declare function initialize<ED extends EntityDict & BaseEntityDict, Cxt e
navigator: import("./features/navigator.web").Navigator;
} & {
cache: import("./features/cache").Cache<ED>;
socket: import("./features/socket/socket").Socket;
runningTree: import("./features/runningTree").RunningTree<ED>;
locales: import("./features/locales").Locales<ED>;
port: import("./features/port").Port<ED>;
style: import("./features/style").Style<ED>;
geo: import("./features/geo").Geo<ED>;
contextMenuFactory: import("./features/contextMenuFactory").ContextMenuFactory<ED>;
subscriber: import("./features/subscriber").SubScriber<ED>;
subscriber: import("./features/socket/subscriber").SubScriber<ED>;
};
};

19
es/page.react.d.ts vendored
View File

@ -49,21 +49,21 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
redirectTo<T2_1 extends keyof ED>(options: {
url: string;
} & OakNavigateToParameters<ED, T2_1>, state?: Record<string, any> | undefined, disableNamespace?: boolean | undefined): Promise<void>;
addItem<T extends keyof ED>(data: Omit<ED[T]["CreateSingle"]["data"], "id"> & {
addItem<T_1 extends keyof ED>(data: Omit<ED[T_1]["CreateSingle"]["data"], "id"> & {
id?: string | undefined;
}, path?: string | undefined): string;
addItems<T_1 extends keyof ED>(data: (Omit<ED[T_1]["CreateSingle"]["data"], "id"> & {
addItems<T_2 extends keyof ED>(data: (Omit<ED[T_2]["CreateSingle"]["data"], "id"> & {
id?: string | undefined;
})[], path?: string | undefined): string[];
removeItem(id: string, path?: string | undefined): void;
removeItems(ids: string[], path?: string | undefined): void;
updateItem<T_2 extends keyof ED>(data: ED[T_2]["Update"]["data"], id: string, action?: ED[T_2]["Action"] | undefined, path?: string | undefined): void;
updateItems<T_3 extends keyof ED>(data: ED[T_3]["Update"]["data"], ids: string[], action?: ED[T_3]["Action"] | undefined, path?: string | undefined): void;
updateItem<T_3 extends keyof ED>(data: ED[T_3]["Update"]["data"], id: string, action?: ED[T_3]["Action"] | undefined, path?: string | undefined): void;
updateItems<T_4 extends keyof ED>(data: ED[T_4]["Update"]["data"], ids: string[], action?: ED[T_4]["Action"] | undefined, path?: string | undefined): void;
recoverItem(id: string, path?: string | undefined): void;
recoverItems(ids: string[], path?: string | undefined): void;
resetItem(id: string, path?: string | undefined): void;
update<T_4 extends keyof ED>(data: ED[T_4]["Update"]["data"], action?: ED[T_4]["Action"] | undefined, path?: string | undefined): void;
create<T_5 extends keyof ED>(data: Omit<ED[T_5]["CreateSingle"]["data"], "id">, path?: string | undefined): void;
update<T_5 extends keyof ED>(data: ED[T_5]["Update"]["data"], action?: ED[T_5]["Action"] | undefined, path?: string | undefined): void;
create<T_6 extends keyof ED>(data: Omit<ED[T_6]["CreateSingle"]["data"], "id">, path?: string | undefined): void;
remove(path?: string | undefined): void;
isCreation(path?: string | undefined): boolean;
clean(lsn?: number | undefined, dontPublish?: true | undefined, path?: string | undefined): void;
@ -77,7 +77,7 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
getFreshValue(path?: string | undefined): Partial<import("oak-domain/lib/types").GeneralEntityShape> | Partial<import("oak-domain/lib/types").GeneralEntityShape>[] | undefined;
checkOperation<T2_2 extends keyof ED>(entity: T2_2, operation: Omit<ED[T2_2]["Operation"], "id">, checkerTypes?: CheckerType[] | undefined): boolean | { [A in ED[T2_2]["Action"]]: boolean | import("oak-domain/lib/types").OakUserException<ED>; }[ED[T2_2]["Action"]];
tryExecute(path?: string | undefined, action?: string | undefined): boolean | { [A_1 in ED[keyof ED]["Action"]]: boolean | import("oak-domain/lib/types").OakUserException<ED>; }[ED[keyof ED]["Action"]];
getOperations<T_6 extends keyof ED>(path?: string | undefined): {
getOperations<T_7 extends keyof ED>(path?: string | undefined): {
entity: keyof ED;
operation: ED[keyof ED]["Operation"];
}[] | undefined;
@ -119,10 +119,7 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
componentWillReceiveProps?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextContext: any): void;
UNSAFE_componentWillReceiveProps?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextContext: any): void;
componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void;
UNSAFE_componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void; /**
* setState后先调willUnmount析构callback不被调用的情况willUnmount需要将runningTree上的node正确析构
* by Xc 20240806
*/
UNSAFE_componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void;
};
contextType?: React.Context<any> | undefined;
};

View File

@ -22,9 +22,9 @@ export default class DebugConnector<ED extends EntityDict & BaseEntityDict, Cxt
parseRequest(): any;
serializeResult(): any;
serializeException(): any;
getSubscribeRouter(): any;
getSubscribePointRouter(): any;
getSubscribePoint(): any;
getSocketPath(): any;
getSocketPointRouter(): any;
getSocketPoint(): any;
getBridgeRouter(): string;
makeBridgeUrl(url: string): string;
getEndpointRouter(): string;

View File

@ -56,15 +56,15 @@ export default class DebugConnector {
throw new Error('not available in debugConnector');
return {};
}
getSubscribeRouter() {
getSocketPath() {
throw new Error('not available in debugConnector');
return {};
}
getSubscribePointRouter() {
getSocketPointRouter() {
throw new Error('not available in debugConnector');
return {};
}
getSubscribePoint() {
getSocketPoint() {
throw new Error('not available in debugConnector');
return {};
}

View File

@ -1,7 +1,7 @@
import { EntityDict } from 'oak-domain/lib/base-app-domain';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
import { SyncContext, SyncRowStore } from 'oak-domain/lib/store/SyncRowStore';
import { SubScriber } from '../features/subscriber';
import { SubScriber } from '../features/socket/subscriber';
import { Environment } from '../features/environment';
import { Navigator } from '../features/navigator';
import { BriefEnv } from 'oak-domain/lib/types/Environment';

View File

@ -13,13 +13,15 @@ import { CacheStore } from '../cacheStore/CacheStore';
import { Navigator } from './navigator';
import { Port } from './port';
import { Style } from './style';
import { SubScriber } from './subscriber';
import { SubScriber } from './socket/subscriber';
import { Socket } from './socket/socket';
import { ContextMenuFactory } from './contextMenuFactory';
import { Geo } from './geo';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
import { CommonConfiguration, RenderConfiguration } from 'oak-domain/lib/types/Configuration';
export declare function initializeStep2<ED extends EntityDict & BaseEntityDict, FrontCxt extends SyncContext<ED>>(features: Pick<BasicFeatures<ED>, 'localStorage' | 'environment' | 'message'>, connector: Connector<ED, FrontCxt>, storageSchema: StorageSchema<ED>, frontendContextBuilder: (store: CacheStore<ED>) => SyncContext<ED>, checkers: Array<Checker<ED, keyof ED, FrontCxt>>, common: CommonConfiguration<ED>, render: RenderConfiguration<ED>): {
cache: Cache<ED>;
socket: Socket;
runningTree: RunningTree<ED>;
locales: Locales<ED>;
port: Port<ED>;

View File

@ -1,6 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initializeStep1 = exports.initializeStep2 = void 0;
const tslib_1 = require("tslib");
const cache_1 = require("./cache");
const location_1 = require("./location");
const runningTree_1 = require("./runningTree");
@ -13,7 +14,9 @@ const navigator_1 = require("./navigator");
const port_1 = require("./port");
// import { RelationAuth } from './relationAuth';
const style_1 = require("./style");
const subscriber_1 = require("./subscriber");
const subscriber_1 = require("./socket/subscriber");
const socketPoint_1 = tslib_1.__importDefault(require("./socket/socketPoint"));
const socket_1 = require("./socket/socket");
const contextMenuFactory_1 = require("./contextMenuFactory");
const geo_1 = require("./geo");
function initializeStep2(features, connector, storageSchema, frontendContextBuilder, checkers, common, render) {
@ -25,10 +28,12 @@ function initializeStep2(features, connector, storageSchema, frontendContextBuil
const style = new style_1.Style(render.styleDict);
const locales = new locales_1.Locales(cache, localStorage, environment, 'zh-CN'); // 临时性代码,应由上层传入
const contextMenuFactory = new contextMenuFactory_1.ContextMenuFactory(cache);
const subscriber = new subscriber_1.SubScriber(cache, message, () => connector.getSubscribePoint());
const socketPoint = new socketPoint_1.default(() => connector.getSocketPoint());
const subscriber = new subscriber_1.SubScriber(cache, message, socketPoint);
const socket = new socket_1.Socket(message, socketPoint);
return {
cache,
// relationAuth,
socket,
runningTree,
locales,
port,

29
lib/features/socket/socket.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
import { Message } from '../message';
import { Feature } from '../../types/Feature';
import SocketPoint from './socketPoint';
export type SocketEvent = {
event: 'connect';
} | {
event: 'disconnect';
} | {
event: 'connect_error';
reason: any;
} | {
event: 'data';
data: any;
};
export declare class Socket extends Feature {
private socketPoint;
private url?;
private path?;
private io?;
private message;
constructor(message: Message, socketPoint: SocketPoint);
private initSocketPoint;
private connectQueue;
private ioState;
private onConnected;
connect(): Promise<unknown>;
send(data: any, callback?: Function): Promise<void>;
disconnect(): void;
}

View File

@ -0,0 +1,105 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Socket = void 0;
const tslib_1 = require("tslib");
const Exception_1 = require("oak-domain/lib/types/Exception");
const Feature_1 = require("../../types/Feature");
const socket_io_1 = tslib_1.__importDefault(require("../../utils/socket.io/socket.io"));
const assert_1 = tslib_1.__importDefault(require("assert"));
class Socket extends Feature_1.Feature {
socketPoint;
url;
path;
io;
message;
constructor(message, socketPoint) {
super();
this.message = message;
this.socketPoint = socketPoint;
}
async initSocketPoint() {
try {
const { socketUrl, path } = await this.socketPoint.get();
this.url = socketUrl;
this.path = path;
}
catch (err) {
this.url = undefined;
this.path = undefined;
throw new Exception_1.OakSocketConnectException();
}
}
connectQueue = [];
ioState = 'unconnected';
onConnected() {
this.io.off('disconnect');
this.io.off('data');
this.io.on('disconnect', () => {
this.publish({
event: 'disconnect',
});
this.ioState = 'unconnected';
this.io = undefined;
});
this.io.on('data', (data) => {
this.publish({
event: 'data',
data,
});
});
}
async connect() {
if (this.ioState === 'connecting') {
const p = new Promise((resolve, reject) => {
this.connectQueue.push([resolve, reject]);
});
return p;
}
else {
(0, assert_1.default)(this.ioState === 'unconnected');
this.ioState = 'connecting';
await this.initSocketPoint();
this.io = (0, socket_io_1.default)(this.url, {
path: this.path,
});
return new Promise((resolve, reject) => {
this.io.off('connect');
this.io.on('connect', () => {
this.publish({
event: 'connect',
});
this.ioState = 'connected';
this.connectQueue.forEach(([resolve]) => resolve());
this.onConnected();
resolve(undefined);
});
this.io.on('connect_error', (err) => {
this.message.setMessage({
type: 'error',
title: 'connect error',
content: err.message,
});
this.publish({
event: 'connect_error',
reason: err,
});
this.ioState = 'unconnected';
this.connectQueue.forEach(([resolve, reject]) => reject(err));
reject(err);
});
});
}
}
async send(data, callback) {
if (!this.io) {
await this.connect();
}
this.io.emit('data', data, callback);
}
disconnect() {
(0, assert_1.default)(this.ioState === 'connected');
this.io.disconnect();
}
}
exports.Socket = Socket;
;

13
lib/features/socket/socketPoint.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
type SocketPointInfo = {
path: string;
subscribeUrl: string;
socketUrl: string;
};
export default class SocketPoint {
getSocketPointFn: () => Promise<SocketPointInfo>;
private socketPointInfo?;
constructor(getSocketPointFn: () => Promise<SocketPointInfo>);
init(): Promise<void>;
get(): Promise<SocketPointInfo>;
}
export {};

View File

@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const Exception_1 = require("oak-domain/lib/types/Exception");
class SocketPoint {
getSocketPointFn;
socketPointInfo;
constructor(getSocketPointFn) {
this.getSocketPointFn = getSocketPointFn;
}
async init() {
try {
this.socketPointInfo = await this.getSocketPointFn();
}
catch (err) {
throw new Exception_1.OakSocketConnectException();
}
}
async get() {
if (!this.socketPointInfo) {
await this.init();
}
return this.socketPointInfo;
}
}
exports.default = SocketPoint;

34
lib/features/socket/subscriber.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
import { EntityDict, OpRecord } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Cache } from '../cache';
import { Message } from '../message';
import { Feature } from '../../types/Feature';
import SocketPoint from './socketPoint';
type SubscribeEvent = 'connect' | 'disconnect';
type Callback<ED extends EntityDict & BaseEntityDict> = (event: string, records: OpRecord<ED>[]) => void;
export declare class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature {
private cache;
private message;
private socketPoint;
private eventMap;
private url?;
private path?;
private socket?;
private halted;
private reconnectInterval;
private reconnectTimer;
private count;
private socketState;
private eventCallbackMap;
constructor(cache: Cache<ED>, message: Message, socketPoint: SocketPoint);
on(event: SubscribeEvent, callback: (...data: any) => void): void;
off(event: SubscribeEvent, callback: () => void): void;
private emit;
private initSocketPoint;
private connect;
sub(events: string[], callback?: Callback<ED>): Promise<() => void>;
private unsub;
getSubscriberId(): string | undefined;
setHalted(halted: boolean): void;
}
export {};

View File

@ -0,0 +1,14 @@
import { EntityDict, OpRecord, SubDataDef } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Feature } from '../../types/Feature';
type SubscribeEvent = 'connect' | 'disconnect';
export declare class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature {
private eventCallbackMap;
constructor();
on(event: SubscribeEvent, callback: () => void): void;
off(event: SubscribeEvent, callback: () => void): void;
sub(data: SubDataDef<ED, keyof ED>[], callback: (records: OpRecord<ED>[], ids: string[]) => void): Promise<void>;
unsub(ids: string[]): Promise<void>;
getSubscriberId(): undefined;
}
export {};

View File

@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubScriber = void 0;
const lodash_1 = require("oak-domain/lib/utils/lodash");
const Feature_1 = require("../../types/Feature");
class SubScriber extends Feature_1.Feature {
eventCallbackMap = {
connect: [],
disconnect: [],
};
constructor() {
super();
}
on(event, callback) {
this.eventCallbackMap[event].push(callback);
}
off(event, callback) {
(0, lodash_1.pull)(this.eventCallbackMap[event], callback);
}
async sub(data, callback) {
console.log('data subscribe 在dev模式下不起作用');
}
async unsub(ids) { }
getSubscriberId() {
return undefined;
}
}
exports.SubScriber = SubScriber;

View File

@ -0,0 +1,249 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SubScriber = void 0;
const tslib_1 = require("tslib");
const assert_1 = require("oak-domain/lib/utils/assert");
const lodash_1 = require("oak-domain/lib/utils/lodash");
const Exception_1 = require("oak-domain/lib/types/Exception");
const socket_io_1 = tslib_1.__importDefault(require("../../utils/socket.io/socket.io"));
const Feature_1 = require("../../types/Feature");
class SubScriber extends Feature_1.Feature {
cache;
message;
socketPoint;
eventMap = {};
url;
path;
socket;
halted;
reconnectInterval;
reconnectTimer;
count;
socketState = 'unconnected';
eventCallbackMap = {
connect: [],
disconnect: [],
};
constructor(cache, message, socketPoint) {
super();
this.cache = cache;
this.message = message;
this.socketPoint = socketPoint;
this.halted = false;
this.reconnectInterval = 10 * 1000; // 每隔10秒重连
this.reconnectTimer = undefined;
this.count = 0;
}
on(event, callback) {
this.eventCallbackMap[event].push(callback);
}
off(event, callback) {
(0, lodash_1.pull)(this.eventCallbackMap[event], callback);
}
emit(event, ...data) {
this.eventCallbackMap[event].forEach((ele) => ele(data));
}
async initSocketPoint() {
try {
const { subscribeUrl, path } = await this.socketPoint.get();
this.url = subscribeUrl;
this.path = path;
}
catch (err) {
this.url = undefined;
this.path = undefined;
throw new Exception_1.OakSocketConnectException();
}
}
async connect() {
if (this.halted) {
return;
}
this.socketState = 'connecting';
let optionInited = false;
if (!this.url) {
await this.initSocketPoint();
optionInited = true;
this.count = 0;
}
const url = this.url;
const path = this.path;
// import { io } from "socket.io-client";
// const socket = io('https://example.com/order', {
// path: '/my-custom-path/',
// });
// the Socket instance is attached to the "order" Namespace
// the HTTP requests will look like: GET https://example.com/my-custom-path/?EIO=4&transport=polling&t=ML4jUwU
// 文档 https://socket.io/docs/v4/client-options/
if (!this.socket) {
// 理论上有可能出现this.socket初始化时url和path和这次connect时发生变化暂时不处理。by Xc
this.socket = (0, socket_io_1.default)(url, {
path,
});
}
const socket = this.socket;
return new Promise((resolve, reject) => {
/**
* https://socket.io/zh-CN/docs/v4/client-socket-instance/
*/
socket.on('connect', async () => {
this.socketState = 'connected';
this.emit('connect');
socket.off('connect');
socket.on('disconnect', () => {
this.socketState = 'unconnected';
this.emit('disconnect');
socket.removeAllListeners();
if (Object.keys(this.eventMap).length > 0) {
this.connect();
}
});
socket.on('data', (opRecords, event) => {
const registered = this.eventMap[event];
if (registered) {
registered.callbacks.forEach((ele) => ele(event, opRecords));
}
this.cache.sync(opRecords);
});
socket.on('error', (errString) => {
console.error(errString);
this.message.setMessage({
type: 'error',
title: '服务器subscriber抛出异常',
content: errString,
});
});
if (Object.keys(this.eventMap).length > 0) {
socket.emit('sub', Object.keys(this.eventMap));
resolve(undefined);
}
else {
resolve(undefined);
socket.disconnect();
}
});
if (!optionInited) {
socket.on('connect_error', async (err) => {
this.count++;
if (this.count > 50) {
// 可能socket地址改变了刷新重连
this.url = undefined;
}
socket.removeAllListeners();
socket.disconnect();
this.socket = undefined;
// 清除之前的重连定时器
if (this.reconnectTimer) {
clearInterval(this.reconnectTimer);
}
// 根据重连次数设置不同的重连间隔
if (this.count <= 10) {
this.reconnectInterval = 10 * 1000; // 10秒
}
else if (this.count <= 20) {
this.reconnectInterval = 30 * 1000; // 30秒
}
else if (this.count <= 30) {
this.reconnectInterval = 60 * 1000; // 60秒
}
else if (this.count <= 40) {
this.reconnectInterval = 90 * 1000; // 90秒
}
else {
this.reconnectInterval = 120 * 1000; // 2分钟
}
// 设置新的定时器
this.reconnectTimer = setInterval(async () => {
await this.connect();
// resolve(undefined);
}, this.reconnectInterval);
resolve(undefined);
});
}
socket.connect();
});
}
async sub(events, callback) {
const newEvents = [];
events.forEach((event) => {
const registered = this.eventMap[event];
if (registered) {
if (callback) {
registered.callbacks.push(callback);
}
registered.count++;
}
else {
this.eventMap[event] = {
callbacks: callback ? [callback] : [],
count: 1,
};
newEvents.push(event);
}
;
});
if (this.socketState === 'unconnected') {
await this.connect();
}
else if (this.socketState === 'connected' && newEvents.length > 0) {
await new Promise((resolve, reject) => {
// this.socket!.emit('sub', newEvents, (result: string) => {
// if (result) {
// this.message.setMessage({
// type: 'error',
// title: 'sub data error',
// content: result,
// });
// reject();
// }
// else {
// resolve(undefined);
// }
// });
// TODO 临时代码 后续结合文档再解决
this.socket.emit('sub', newEvents);
resolve(undefined);
});
}
return () => {
this.unsub(events, callback);
};
}
async unsub(events, callback) {
const invalidEvents = [];
events.forEach((event) => {
const registered = this.eventMap[event];
(0, assert_1.assert)(registered);
registered.count--;
if (registered.count > 0) {
if (callback) {
(0, lodash_1.pull)(registered.callbacks, callback);
}
}
else {
invalidEvents.push(event);
}
});
invalidEvents.forEach(event => (0, lodash_1.unset)(this.eventMap, event));
if (this.socketState === 'connected') {
this.socket.emit('unsub', events);
if (Object.keys(this.eventMap).length === 0) {
this.socket.disconnect();
this.socket.removeAllListeners();
this.socketState = 'unconnected';
}
}
}
getSubscriberId() {
if (this.socket) {
return this.socket.id;
}
}
setHalted(halted) {
this.halted = halted;
if (halted === false && Object.keys(this.eventMap).length > 0) {
this.connect();
}
}
}
exports.SubScriber = SubScriber;

3
lib/initialize.d.ts vendored
View File

@ -28,12 +28,13 @@ export declare function initialize<ED extends EntityDict & BaseEntityDict, Cxt e
navigator: import("./features/navigator.web").Navigator;
} & {
cache: import("./features/cache").Cache<ED>;
socket: import("./features/socket/socket").Socket;
runningTree: import("./features/runningTree").RunningTree<ED>;
locales: import("./features/locales").Locales<ED>;
port: import("./features/port").Port<ED>;
style: import("./features/style").Style<ED>;
geo: import("./features/geo").Geo<ED>;
contextMenuFactory: import("./features/contextMenuFactory").ContextMenuFactory<ED>;
subscriber: import("./features/subscriber").SubScriber<ED>;
subscriber: import("./features/socket/subscriber").SubScriber<ED>;
};
};

19
lib/page.react.d.ts vendored
View File

@ -49,21 +49,21 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
redirectTo<T2_1 extends keyof ED>(options: {
url: string;
} & OakNavigateToParameters<ED, T2_1>, state?: Record<string, any> | undefined, disableNamespace?: boolean | undefined): Promise<void>;
addItem<T extends keyof ED>(data: Omit<ED[T]["CreateSingle"]["data"], "id"> & {
addItem<T_1 extends keyof ED>(data: Omit<ED[T_1]["CreateSingle"]["data"], "id"> & {
id?: string | undefined;
}, path?: string | undefined): string;
addItems<T_1 extends keyof ED>(data: (Omit<ED[T_1]["CreateSingle"]["data"], "id"> & {
addItems<T_2 extends keyof ED>(data: (Omit<ED[T_2]["CreateSingle"]["data"], "id"> & {
id?: string | undefined;
})[], path?: string | undefined): string[];
removeItem(id: string, path?: string | undefined): void;
removeItems(ids: string[], path?: string | undefined): void;
updateItem<T_2 extends keyof ED>(data: ED[T_2]["Update"]["data"], id: string, action?: ED[T_2]["Action"] | undefined, path?: string | undefined): void;
updateItems<T_3 extends keyof ED>(data: ED[T_3]["Update"]["data"], ids: string[], action?: ED[T_3]["Action"] | undefined, path?: string | undefined): void;
updateItem<T_3 extends keyof ED>(data: ED[T_3]["Update"]["data"], id: string, action?: ED[T_3]["Action"] | undefined, path?: string | undefined): void;
updateItems<T_4 extends keyof ED>(data: ED[T_4]["Update"]["data"], ids: string[], action?: ED[T_4]["Action"] | undefined, path?: string | undefined): void;
recoverItem(id: string, path?: string | undefined): void;
recoverItems(ids: string[], path?: string | undefined): void;
resetItem(id: string, path?: string | undefined): void;
update<T_4 extends keyof ED>(data: ED[T_4]["Update"]["data"], action?: ED[T_4]["Action"] | undefined, path?: string | undefined): void;
create<T_5 extends keyof ED>(data: Omit<ED[T_5]["CreateSingle"]["data"], "id">, path?: string | undefined): void;
update<T_5 extends keyof ED>(data: ED[T_5]["Update"]["data"], action?: ED[T_5]["Action"] | undefined, path?: string | undefined): void;
create<T_6 extends keyof ED>(data: Omit<ED[T_6]["CreateSingle"]["data"], "id">, path?: string | undefined): void;
remove(path?: string | undefined): void;
isCreation(path?: string | undefined): boolean;
clean(lsn?: number | undefined, dontPublish?: true | undefined, path?: string | undefined): void;
@ -77,7 +77,7 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
getFreshValue(path?: string | undefined): Partial<import("oak-domain/lib/types").GeneralEntityShape> | Partial<import("oak-domain/lib/types").GeneralEntityShape>[] | undefined;
checkOperation<T2_2 extends keyof ED>(entity: T2_2, operation: Omit<ED[T2_2]["Operation"], "id">, checkerTypes?: CheckerType[] | undefined): boolean | { [A in ED[T2_2]["Action"]]: boolean | import("oak-domain/lib/types").OakUserException<ED>; }[ED[T2_2]["Action"]];
tryExecute(path?: string | undefined, action?: string | undefined): boolean | { [A_1 in ED[keyof ED]["Action"]]: boolean | import("oak-domain/lib/types").OakUserException<ED>; }[ED[keyof ED]["Action"]];
getOperations<T_6 extends keyof ED>(path?: string | undefined): {
getOperations<T_7 extends keyof ED>(path?: string | undefined): {
entity: keyof ED;
operation: ED[keyof ED]["Operation"];
}[] | undefined;
@ -119,10 +119,7 @@ export declare function createComponent<IsList extends boolean, ED extends Entit
componentWillReceiveProps?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextContext: any): void;
UNSAFE_componentWillReceiveProps?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextContext: any): void;
componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void;
UNSAFE_componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void; /**
* setState后先调willUnmount析构callback不被调用的情况willUnmount需要将runningTree上的node正确析构
* by Xc 20240806
*/
UNSAFE_componentWillUpdate?(nextProps: Readonly<ComponentProps<ED, T, TProperty>>, nextState: Readonly<ComponentData<ED, T, FormedData, TData>>, nextContext: any): void;
};
contextType?: React.Context<any> | undefined;
};

View File

@ -22,9 +22,9 @@ export default class DebugConnector<ED extends EntityDict & BaseEntityDict, Cxt
parseRequest(): any;
serializeResult(): any;
serializeException(): any;
getSubscribeRouter(): any;
getSubscribePointRouter(): any;
getSubscribePoint(): any;
getSocketPath(): any;
getSocketPointRouter(): any;
getSocketPoint(): any;
getBridgeRouter(): string;
makeBridgeUrl(url: string): string;
getEndpointRouter(): string;

View File

@ -59,15 +59,15 @@ class DebugConnector {
throw new Error('not available in debugConnector');
return {};
}
getSubscribeRouter() {
getSocketPath() {
throw new Error('not available in debugConnector');
return {};
}
getSubscribePointRouter() {
getSocketPointRouter() {
throw new Error('not available in debugConnector');
return {};
}
getSubscribePoint() {
getSocketPoint() {
throw new Error('not available in debugConnector');
return {};
}

View File

@ -26,6 +26,7 @@
"oak-domain": "file:../oak-domain",
"oak-memory-tree-store": "file:../oak-memory-tree-store",
"ol": "^7.3.0",
"rc-pagination": "^4.3.0",
"react-activation": "^0.12.4",
"react-native-device-info": "^10.12.0",
"react-native-localize": "^3.0.4",

View File

@ -1,7 +1,7 @@
import { EntityDict } from 'oak-domain/lib/base-app-domain';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/types/Entity';
import { SyncContext, SyncRowStore } from 'oak-domain/lib/store/SyncRowStore';
import { SubScriber } from '../features/subscriber';
import { SubScriber } from '../features/socket/subscriber';
import { Environment } from '../features/environment';
import { Navigator } from '../features/navigator';
import { BriefEnv } from 'oak-domain/lib/types/Environment';

View File

@ -14,7 +14,9 @@ import { Navigator } from './navigator';
import { Port } from './port';
// import { RelationAuth } from './relationAuth';
import { Style } from './style';
import { SubScriber } from './subscriber';
import { SubScriber } from './socket/subscriber';
import SocketPoint from './socket/socketPoint';
import { Socket } from './socket/socket';
import { ContextMenuFactory } from './contextMenuFactory';
import { Geo } from './geo';
import { SyncContext } from 'oak-domain/lib/store/SyncRowStore';
@ -38,10 +40,13 @@ export function initializeStep2<ED extends EntityDict & BaseEntityDict, FrontCxt
const style = new Style<ED>(render.styleDict);
const locales = new Locales(cache, localStorage, environment, 'zh-CN'); // 临时性代码,应由上层传入
const contextMenuFactory = new ContextMenuFactory<ED>(cache);
const subscriber = new SubScriber(cache, message, () => connector.getSubscribePoint());
const socketPoint = new SocketPoint(() => connector.getSocketPoint());
const subscriber = new SubScriber(cache, message, socketPoint);
const socket = new Socket(message, socketPoint);
return {
cache,
// relationAuth,
socket,
runningTree,
locales,
port,

View File

@ -0,0 +1,137 @@
import { EntityDict, OpRecord, SubDataDef } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { OakSocketConnectException } from 'oak-domain/lib/types/Exception';
import { Message } from '../message';
import { Feature } from '../../types/Feature';
import io, { Socket as SocketIo } from '../../utils/socket.io/socket.io';
import SocketPoint from './socketPoint';
import assert from 'assert';
export type SocketEvent = {
event: 'connect',
} | {
event: 'disconnect',
} | {
event: 'connect_error',
reason: any,
} | {
event: 'data',
data: any,
};
export class Socket extends Feature {
private socketPoint: SocketPoint;
private url?: string;
private path?: string;
private io?: SocketIo;
private message: Message;
constructor(message: Message, socketPoint: SocketPoint) {
super();
this.message = message;
this.socketPoint = socketPoint;
}
private async initSocketPoint() {
try {
const { socketUrl, path } = await this.socketPoint.get();
this.url = socketUrl;
this.path = path;
}
catch (err) {
this.url = undefined;
this.path = undefined;
throw new OakSocketConnectException();
}
}
private connectQueue: Array<[(value?: any) => void, (reason?: any) => void]> = [];
private ioState: 'unconnected' | 'connecting' | 'connected' = 'unconnected';
private onConnected() {
this.io!.off('disconnect');
this.io!.off('data');
this.io!.on('disconnect', () => {
this.publish({
event: 'disconnect',
});
this.ioState = 'unconnected';
this.io = undefined;
});
this.io!.on('data', (data: any) => {
this.publish({
event: 'data',
data,
});
});
}
async connect() {
if (this.ioState === 'connecting') {
const p = new Promise(
(resolve, reject) => {
this.connectQueue.push([resolve, reject]);
}
);
return p;
}
else {
assert(this.ioState === 'unconnected');
this.ioState = 'connecting';
await this.initSocketPoint();
this.io = io(this.url!, {
path: this.path!,
});
return new Promise(
(resolve, reject) => {
this.io!.off('connect');
this.io!.on('connect', () => {
this.publish({
event: 'connect',
});
this.ioState = 'connected';
this.connectQueue.forEach(
([resolve]) => resolve()
);
this.onConnected();
resolve(undefined);
});
this.io!.on('connect_error', (err) => {
this.message.setMessage({
type: 'error',
title: 'connect error',
content: err.message,
});
this.publish({
event: 'connect_error',
reason: err,
});
this.ioState = 'unconnected';
this.connectQueue.forEach(
([resolve, reject]) => reject(err)
);
reject(err);
});
}
);
}
}
async send(data: any, callback?: Function) {
if (!this.io) {
await this.connect();
}
this.io!.emit('data', data, callback);
}
disconnect() {
assert(this.ioState === 'connected');
this.io!.disconnect();
}
};

View File

@ -0,0 +1,32 @@
import { OakSocketConnectException } from "oak-domain/lib/types/Exception";
type SocketPointInfo = {
path: string;
subscribeUrl: string;
socketUrl: string;
}
export default class SocketPoint {
getSocketPointFn: () => Promise<SocketPointInfo>;
private socketPointInfo?: SocketPointInfo;
constructor(getSocketPointFn: () => Promise<SocketPointInfo>) {
this.getSocketPointFn = getSocketPointFn;
}
async init() {
try {
this.socketPointInfo = await this.getSocketPointFn();
}
catch(err) {
throw new OakSocketConnectException();
}
}
async get() {
if (!this.socketPointInfo) {
await this.init();
}
return this.socketPointInfo!;
}
}

View File

@ -1,7 +1,7 @@
import { EntityDict, OpRecord, SubDataDef } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { pull } from 'oak-domain/lib/utils/lodash';
import { Feature } from '../types/Feature';
import { Feature } from '../../types/Feature';
type SubscribeEvent = 'connect' | 'disconnect';
@ -11,12 +11,7 @@ export class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature
disconnect: [],
};
constructor(
getSubscribePointFn: () => Promise<{
url: string;
path: string;
}>
) {
constructor() {
super();
}

View File

@ -3,10 +3,11 @@ import { EntityDict, OpRecord } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { pull, unset } from 'oak-domain/lib/utils/lodash';
import { OakSocketConnectException } from 'oak-domain/lib/types/Exception';
import { Cache } from './cache';
import { Message } from './message';
import io, { Socket } from '../utils/socket.io/socket.io';
import { Feature } from '../types/Feature';
import { Cache } from '../cache';
import { Message } from '../message';
import io, { Socket } from '../../utils/socket.io/socket.io';
import { Feature } from '../../types/Feature';
import SocketPoint from './socketPoint';
type SubscribeEvent = 'connect' | 'disconnect';
@ -15,10 +16,7 @@ type Callback<ED extends EntityDict & BaseEntityDict> = (event: string, records:
export class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature {
private cache: Cache<ED>;
private message: Message;
private getSubscribePointFn: () => Promise<{
url: string;
path: string;
}>;
private socketPoint: SocketPoint;
private eventMap: Record<string, {
callbacks: Callback<ED>[];
count: number;
@ -42,15 +40,12 @@ export class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature
constructor(
cache: Cache<ED>,
message: Message,
getSubscribePointFn: () => Promise<{
url: string;
path: string;
}>
socketPoint: SocketPoint
) {
super();
this.cache = cache;
this.message = message;
this.getSubscribePointFn = getSubscribePointFn;
this.socketPoint = socketPoint;
this.halted = false;
this.reconnectInterval = 10 * 1000; // 每隔10秒重连
this.reconnectTimer = undefined
@ -71,8 +66,8 @@ export class SubScriber<ED extends EntityDict & BaseEntityDict> extends Feature
private async initSocketPoint() {
try {
const { url, path } = await this.getSubscribePointFn();
this.url = url;
const { subscribeUrl, path } = await this.socketPoint.get();
this.url = subscribeUrl;
this.path = path;
}
catch(err) {

View File

@ -87,15 +87,15 @@ export default class DebugConnector<ED extends EntityDict & BaseEntityDict, Cxt
throw new Error('not available in debugConnector');
return {} as any;
}
getSubscribeRouter() {
getSocketPath() {
throw new Error('not available in debugConnector');
return {} as any;
}
getSubscribePointRouter() {
getSocketPointRouter() {
throw new Error('not available in debugConnector');
return {} as any;
}
getSubscribePoint(){
getSocketPoint(){
throw new Error('not available in debugConnector');
return {} as any;
};