feat(TypedSocket): Add SmartServe integration to TypedSocket, support SmartServe connections and tagging; update dependencies and docs; remove GitLab CI config
This commit is contained in:
@@ -5,6 +5,32 @@ const publicRolePass = 'publicRolePass';
|
||||
|
||||
export type TTypedSocketSide = 'server' | 'client';
|
||||
|
||||
/**
|
||||
* Wrapper for SmartServe's IWebSocketPeer to provide tag compatibility
|
||||
* SmartServe uses Set<string> for tags, while TypedSocket uses {id, payload} format
|
||||
*/
|
||||
export interface ISmartServeConnectionWrapper {
|
||||
peer: plugins.IWebSocketPeer;
|
||||
getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a wrapper around IWebSocketPeer for tag compatibility
|
||||
*/
|
||||
function wrapSmartServePeer(peer: plugins.IWebSocketPeer): ISmartServeConnectionWrapper {
|
||||
const TAG_PREFIX = '__typedsocket_tag__';
|
||||
return {
|
||||
peer,
|
||||
async getTagById(tagId: string): Promise<{ id: string; payload: any } | undefined> {
|
||||
if (!peer.tags.has(tagId)) {
|
||||
return undefined;
|
||||
}
|
||||
const payload = peer.data.get(`${TAG_PREFIX}${tagId}`);
|
||||
return { id: tagId, payload };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class TypedSocket {
|
||||
// STATIC
|
||||
/**
|
||||
@@ -110,16 +136,108 @@ export class TypedSocket {
|
||||
return windowLocationResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TypedSocket server from an existing SmartServe instance.
|
||||
* Use this when you want TypedSocket to work with SmartServe's WebSocket handling.
|
||||
*
|
||||
* @param smartServeArg - SmartServe instance with typedRouter configured in websocket options
|
||||
* @param typedRouterArg - TypedRouter for handling requests (must match SmartServe's typedRouter)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const typedRouter = new TypedRouter();
|
||||
* const smartServe = new SmartServe({
|
||||
* port: 3000,
|
||||
* websocket: {
|
||||
* typedRouter,
|
||||
* onConnectionOpen: (peer) => peer.tags.add('client')
|
||||
* }
|
||||
* });
|
||||
* await smartServe.start();
|
||||
* const typedSocket = TypedSocket.fromSmartServe(smartServe, typedRouter);
|
||||
* ```
|
||||
*/
|
||||
public static fromSmartServe(
|
||||
smartServeArg: plugins.SmartServe,
|
||||
typedRouterArg: plugins.typedrequest.TypedRouter
|
||||
): TypedSocket {
|
||||
const connectionWrappers = new Map<string, ISmartServeConnectionWrapper>();
|
||||
|
||||
// Create the postMethod for server-initiated requests
|
||||
const postMethod = async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
dataArg: T,
|
||||
targetConnectionArg?: ISmartServeConnectionWrapper
|
||||
): Promise<T> => {
|
||||
if (!targetConnectionArg) {
|
||||
const allConnections = smartServeArg.getWebSocketConnections();
|
||||
if (allConnections.length === 1) {
|
||||
console.log(
|
||||
'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
|
||||
);
|
||||
const peer = allConnections[0];
|
||||
let wrapper = connectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
connectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
targetConnectionArg = wrapper;
|
||||
} else if (allConnections.length === 0) {
|
||||
throw new Error('No WebSocket connections available');
|
||||
} else {
|
||||
throw new Error(
|
||||
'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Register interest for the response using correlation ID
|
||||
const interest = await typedRouterArg.fireEventInterestMap.addInterest(
|
||||
dataArg.correlation.id,
|
||||
dataArg
|
||||
);
|
||||
|
||||
// Send the request to the client
|
||||
targetConnectionArg.peer.send(plugins.smartjson.stringify(dataArg));
|
||||
|
||||
// Wait for the response (TypedRouter will fulfill via routeAndAddResponse when response arrives)
|
||||
const response = await interest.interestFullfilled as T;
|
||||
return response;
|
||||
};
|
||||
|
||||
const typedSocket = new TypedSocket(
|
||||
'server',
|
||||
typedRouterArg,
|
||||
postMethod as any,
|
||||
null as any // No smartsocket server/client when using SmartServe
|
||||
);
|
||||
|
||||
typedSocket.smartServeRef = smartServeArg;
|
||||
typedSocket.smartServeConnectionWrappers = connectionWrappers;
|
||||
|
||||
return typedSocket;
|
||||
}
|
||||
|
||||
// INSTANCE
|
||||
public side: TTypedSocketSide;
|
||||
public typedrouter: plugins.typedrequest.TypedRouter;
|
||||
|
||||
// SmartServe mode properties
|
||||
private smartServeRef?: plugins.SmartServe;
|
||||
private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
|
||||
|
||||
public get eventSubject(): plugins.smartrx.rxjs.Subject<plugins.smartsocket.TConnectionStatus> {
|
||||
if (this.smartServeRef) {
|
||||
// SmartServe doesn't provide an eventSubject, return a new Subject
|
||||
// In SmartServe mode, connection events are handled via onConnectionOpen/onConnectionClose hooks
|
||||
console.warn('eventSubject is not fully supported in SmartServe mode. Use SmartServe hooks instead.');
|
||||
return new plugins.smartrx.rxjs.Subject();
|
||||
}
|
||||
return this.socketServerOrClient.eventSubject;
|
||||
}
|
||||
private postMethod: plugins.typedrequest.IPostMethod &
|
||||
((
|
||||
typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest,
|
||||
socketConnectionArg?: plugins.smartsocket.SocketConnection
|
||||
socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
|
||||
) => Promise<plugins.typedrequestInterfaces.ITypedRequest>);
|
||||
private socketServerOrClient:
|
||||
| plugins.smartsocket.Smartsocket
|
||||
@@ -155,12 +273,12 @@ export class TypedSocket {
|
||||
|
||||
public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
||||
methodName: T['method'],
|
||||
targetConnection?: plugins.smartsocket.SocketConnection
|
||||
targetConnection?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
|
||||
): plugins.typedrequest.TypedRequest<T> {
|
||||
const typedrequest = new plugins.typedrequest.TypedRequest<T>(
|
||||
new plugins.typedrequest.TypedTarget({
|
||||
postMethod: async (requestDataArg) => {
|
||||
const result = await this.postMethod(requestDataArg, targetConnection);
|
||||
const result = await this.postMethod(requestDataArg, targetConnection as any);
|
||||
return result;
|
||||
},
|
||||
}),
|
||||
@@ -170,13 +288,31 @@ export class TypedSocket {
|
||||
}
|
||||
|
||||
/**
|
||||
* returns all matching target connection
|
||||
* @param asyncFindFuncArg
|
||||
* @returns
|
||||
* returns all matching target connections
|
||||
* Works with both Smartsocket and SmartServe backends
|
||||
* @param asyncFindFuncArg - async filter function
|
||||
* @returns array of matching connections
|
||||
*/
|
||||
public async findAllTargetConnections(
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection) => Promise<boolean>
|
||||
) {
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
|
||||
// SmartServe mode
|
||||
if (this.smartServeRef) {
|
||||
const matchingConnections: ISmartServeConnectionWrapper[] = [];
|
||||
for (const peer of this.smartServeRef.getWebSocketConnections()) {
|
||||
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
if (await asyncFindFuncArg(wrapper)) {
|
||||
matchingConnections.push(wrapper);
|
||||
}
|
||||
}
|
||||
return matchingConnections;
|
||||
}
|
||||
|
||||
// Smartsocket mode
|
||||
if (this.socketServerOrClient instanceof plugins.smartsocket.Smartsocket) {
|
||||
const matchingSockets: plugins.smartsocket.SocketConnection[] = [];
|
||||
for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) {
|
||||
@@ -185,9 +321,9 @@ export class TypedSocket {
|
||||
}
|
||||
}
|
||||
return matchingSockets;
|
||||
} else {
|
||||
throw new Error('this method >>findTargetConnection<< is only available from the server');
|
||||
}
|
||||
|
||||
throw new Error('this method >>findTargetConnection<< is only available from the server');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,22 +332,51 @@ export class TypedSocket {
|
||||
* @returns
|
||||
*/
|
||||
public async findTargetConnection(
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection) => Promise<boolean>
|
||||
) {
|
||||
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
|
||||
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
|
||||
const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
|
||||
return allMatching[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all connections that have a specific tag
|
||||
* Works with both Smartsocket and SmartServe backends
|
||||
*/
|
||||
public async findAllTargetConnectionsByTag<
|
||||
TTag extends plugins.typedrequestInterfaces.ITag = any
|
||||
>(keyArg: TTag['name'], payloadArg?: TTag['payload']) {
|
||||
>(keyArg: TTag['name'], payloadArg?: TTag['payload']): Promise<(plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper)[]> {
|
||||
// SmartServe mode - use native filtering for better performance
|
||||
if (this.smartServeRef) {
|
||||
const peers = this.smartServeRef.getWebSocketConnectionsByTag(keyArg);
|
||||
const results: ISmartServeConnectionWrapper[] = [];
|
||||
|
||||
for (const peer of peers) {
|
||||
let wrapper = this.smartServeConnectionWrappers.get(peer.id);
|
||||
if (!wrapper) {
|
||||
wrapper = wrapSmartServePeer(peer);
|
||||
this.smartServeConnectionWrappers.set(peer.id, wrapper);
|
||||
}
|
||||
|
||||
// If payload specified, also filter by payload stored in peer.data
|
||||
if (payloadArg !== undefined) {
|
||||
const tag = await wrapper.getTagById(keyArg);
|
||||
if (plugins.smartjson.stringify(tag?.payload) !== plugins.smartjson.stringify(payloadArg)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
results.push(wrapper);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Smartsocket mode - use existing logic
|
||||
return this.findAllTargetConnections(async (socketConnectionArg) => {
|
||||
let result: boolean;
|
||||
if (!payloadArg) {
|
||||
result = !!(await socketConnectionArg.getTagById(keyArg));
|
||||
result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg));
|
||||
} else {
|
||||
result = !!(
|
||||
plugins.smartjson.stringify((await socketConnectionArg.getTagById(keyArg))?.payload) ===
|
||||
plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) ===
|
||||
plugins.smartjson.stringify(payloadArg)
|
||||
);
|
||||
}
|
||||
@@ -219,15 +384,28 @@ export class TypedSocket {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a single connection by tag
|
||||
*/
|
||||
public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
|
||||
keyArg: TTag['name'],
|
||||
payloadArg?: TTag['payload']
|
||||
) {
|
||||
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
|
||||
const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
|
||||
return allResults[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the TypedSocket server/client
|
||||
* Note: In SmartServe mode, SmartServe manages its own lifecycle
|
||||
*/
|
||||
public async stop() {
|
||||
if (this.smartServeRef) {
|
||||
// SmartServe manages its own lifecycle
|
||||
// Clear our connection wrappers
|
||||
this.smartServeConnectionWrappers.clear();
|
||||
return;
|
||||
}
|
||||
await this.socketServerOrClient.stop();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user