Files
typedsocket/ts/typedsocket.classes.typedsocket.ts

408 lines
15 KiB
TypeScript
Raw Normal View History

2022-03-24 22:46:08 +01:00
import * as plugins from './typedsocket.plugins.js';
2020-12-21 21:01:37 +00:00
const publicRoleName = 'publicRoleName';
const publicRolePass = 'publicRolePass';
2022-03-08 00:14:40 +01:00
export type TTypedSocketSide = 'server' | 'client';
2021-01-28 02:12:22 +00:00
/**
* 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 };
},
};
}
2020-12-21 21:01:37 +00:00
export class TypedSocket {
// STATIC
/**
* creates a typedsocket server
* note: this will fail in browser environments as server libs are not bundled.
*/
2022-10-26 10:54:36 +02:00
public static async createServer(
typedrouterArg: plugins.typedrequest.TypedRouter
): Promise<TypedSocket> {
const smartsocketServer = new plugins.smartsocket.Smartsocket({
2022-01-19 19:17:22 +01:00
alias: 'typedsocketServer',
port: 3000,
});
2022-03-08 00:14:40 +01:00
smartsocketServer.socketFunctions.add(
new plugins.smartsocket.SocketFunction({
funcName: 'processMessage',
funcDef: async (dataArg, socketConnectionArg) => {
return typedrouterArg.routeAndAddResponse(dataArg);
},
})
);
const typedsocket = new TypedSocket(
2021-01-28 02:12:22 +00:00
'server',
typedrouterArg,
2022-10-26 10:54:36 +02:00
async <T extends plugins.typedrequestInterfaces.ITypedRequest>(
dataArg: T,
targetConnectionArg?: plugins.smartsocket.SocketConnection
): Promise<T> => {
if (!targetConnectionArg) {
if ((smartsocketServer.socketConnections.getArray().length = 1)) {
console.log(
'Since no targetConnection was supplied and there is only one active one present, choosing that one automatically'
);
2022-10-26 10:54:36 +02:00
targetConnectionArg = smartsocketServer.socketConnections.getArray()[0];
} else {
2022-10-26 10:54:36 +02:00
throw new Error(
'you need to specify the wanted targetConnection. Currently no target is selectable automatically.'
);
}
}
2022-10-26 10:54:36 +02:00
const response: T = (await smartsocketServer.clientCall(
'processMessage',
dataArg,
targetConnectionArg
2022-10-26 10:54:36 +02:00
)) as any;
return response;
},
smartsocketServer
);
await smartsocketServer.start();
return typedsocket;
}
2022-10-26 10:54:36 +02:00
public static async createClient(
typedrouterArg: plugins.typedrequest.TypedRouter,
2021-01-21 14:26:48 +00:00
serverUrlArg: string,
aliasArg = 'clientArg'
): Promise<TypedSocket> {
2021-01-22 15:13:05 +00:00
const domain = new plugins.smartstring.Domain(serverUrlArg);
2022-01-19 19:17:22 +01:00
const socketOptions: plugins.smartsocket.ISmartsocketClientOptions = {
2021-01-21 14:26:48 +00:00
alias: aliasArg,
2021-01-22 15:13:05 +00:00
port: domain.port || 3000,
2021-01-23 05:14:29 +00:00
url: `${domain.nodeParsedUrl.protocol}//${domain.nodeParsedUrl.hostname}`,
autoReconnect: true,
2022-10-26 10:54:36 +02:00
};
console.log(`starting typedsocket with the following settings:`);
2021-01-23 05:14:29 +00:00
console.log(socketOptions);
const smartsocketClient = new plugins.smartsocket.SmartsocketClient(socketOptions);
2020-12-26 18:45:17 +00:00
smartsocketClient.addSocketFunction(
new plugins.smartsocket.SocketFunction({
funcName: 'processMessage',
funcDef: async (dataArg, socketConnectionArg) => {
return typedrouterArg.routeAndAddResponse(dataArg);
},
})
);
const typedsocket = new TypedSocket(
2021-01-28 02:12:22 +00:00
'client',
typedrouterArg,
2022-10-26 10:54:36 +02:00
async <T extends plugins.typedrequestInterfaces.ITypedRequest>(dataArg: T): Promise<T> => {
const response: T = smartsocketClient.serverCall('processMessage', dataArg) as any as T;
return response;
},
smartsocketClient
);
2023-03-20 17:06:33 +01:00
console.log(`typedsocket triggering smartsocket to connect...`);
const before = Date.now();
await smartsocketClient.connect();
2023-03-20 17:06:33 +01:00
console.log(`typedsocket triggered smartsocket connected in ${Date.now() - before}ms!!!`)
return typedsocket;
}
2023-03-29 13:47:03 +02:00
public static useWindowLocationOriginUrl = () => {
const windowLocationResult = plugins.smarturl.Smarturl.createFromUrl(globalThis.location.origin).toString();
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
2021-01-28 02:12:22 +00:00
public side: TTypedSocketSide;
public typedrouter: plugins.typedrequest.TypedRouter;
// SmartServe mode properties
private smartServeRef?: plugins.SmartServe;
private smartServeConnectionWrappers: Map<string, ISmartServeConnectionWrapper> = new Map();
2023-08-06 16:37:39 +02:00
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();
}
2023-03-29 16:29:58 +02:00
return this.socketServerOrClient.eventSubject;
}
private postMethod: plugins.typedrequest.IPostMethod &
((
typedRequestPostObject: plugins.typedrequestInterfaces.ITypedRequest,
socketConnectionArg?: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper
) => Promise<plugins.typedrequestInterfaces.ITypedRequest>);
2022-10-26 10:54:36 +02:00
private socketServerOrClient:
| plugins.smartsocket.Smartsocket
| plugins.smartsocket.SmartsocketClient;
constructor(
2021-01-28 02:12:22 +00:00
sideArg: TTypedSocketSide,
typedrouterArg: plugins.typedrequest.TypedRouter,
postMethodArg: plugins.typedrequest.IPostMethod,
socketServerOrClientArg: plugins.smartsocket.Smartsocket | plugins.smartsocket.SmartsocketClient
) {
2021-01-28 02:12:22 +00:00
this.side = sideArg;
this.typedrouter = typedrouterArg;
this.postMethod = postMethodArg;
this.socketServerOrClient = socketServerOrClientArg;
}
2022-12-28 15:15:19 +01:00
public addTag<T extends plugins.typedrequestInterfaces.ITag = any>(
nameArg: T['name'],
payloadArg: T['payload']
) {
2022-10-26 10:54:36 +02:00
if (
this.side === 'client' &&
this.socketServerOrClient instanceof plugins.smartsocket.SmartsocketClient
) {
2021-01-28 02:12:22 +00:00
this.socketServerOrClient.socketConnection.addTag({
2022-10-26 22:39:39 +02:00
id: nameArg,
2022-10-26 10:54:36 +02:00
payload: payloadArg,
});
2021-01-28 02:12:22 +00:00
} else {
throw new Error('tagging is only supported on clients');
}
}
2022-10-26 10:54:36 +02:00
public createTypedRequest<T extends plugins.typedrequestInterfaces.ITypedRequest>(
methodName: T['method'],
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 as any);
return result;
},
}),
methodName
);
return typedrequest;
}
2020-12-21 21:01:37 +00:00
2021-10-22 21:09:42 +02:00
/**
* returns all matching target connections
* Works with both Smartsocket and SmartServe backends
* @param asyncFindFuncArg - async filter function
* @returns array of matching connections
2021-10-22 21:09:42 +02:00
*/
2022-10-26 10:54:36 +02:00
public async findAllTargetConnections(
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) {
2021-01-28 12:58:42 +00:00
const matchingSockets: plugins.smartsocket.SocketConnection[] = [];
for (const socketConnection of this.socketServerOrClient.socketConnections.getArray()) {
2021-01-28 12:58:42 +00:00
if (await asyncFindFuncArg(socketConnection)) {
matchingSockets.push(socketConnection);
}
}
2021-01-28 12:58:42 +00:00
return matchingSockets;
}
throw new Error('this method >>findTargetConnection<< is only available from the server');
}
2020-12-21 21:01:37 +00:00
2021-10-22 21:09:42 +02:00
/**
* returns a single target connection by returning the first one of all matching ones
* @param asyncFindFuncArg
2022-10-26 10:54:36 +02:00
* @returns
2021-10-22 21:09:42 +02:00
*/
2022-10-26 10:54:36 +02:00
public async findTargetConnection(
asyncFindFuncArg: (connectionArg: plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper) => Promise<boolean>
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
2021-10-22 21:09:42 +02:00
const allMatching = await this.findAllTargetConnections(asyncFindFuncArg);
2022-10-26 10:54:36 +02:00
return allMatching[0];
2021-01-28 12:58:42 +00:00
}
/**
* Find all connections that have a specific tag
* Works with both Smartsocket and SmartServe backends
*/
2022-12-28 15:15:19 +01:00
public async findAllTargetConnectionsByTag<
TTag extends plugins.typedrequestInterfaces.ITag = any
>(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
2022-10-26 10:54:36 +02:00
return this.findAllTargetConnections(async (socketConnectionArg) => {
2021-01-28 02:12:22 +00:00
let result: boolean;
2021-01-28 12:58:42 +00:00
if (!payloadArg) {
result = !!(await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg));
2021-01-28 02:12:22 +00:00
} else {
2022-10-26 10:54:18 +02:00
result = !!(
plugins.smartjson.stringify((await (socketConnectionArg as plugins.smartsocket.SocketConnection).getTagById(keyArg))?.payload) ===
2022-10-26 10:54:36 +02:00
plugins.smartjson.stringify(payloadArg)
2022-10-26 10:54:18 +02:00
);
2021-01-28 02:12:22 +00:00
}
return result;
2022-10-26 10:54:36 +02:00
});
2021-01-28 02:12:22 +00:00
}
/**
* Find a single connection by tag
*/
2022-12-28 15:15:19 +01:00
public async findTargetConnectionByTag<TTag extends plugins.typedrequestInterfaces.ITag = any>(
keyArg: TTag['name'],
payloadArg?: TTag['payload']
): Promise<plugins.smartsocket.SocketConnection | ISmartServeConnectionWrapper | undefined> {
2022-10-26 10:54:36 +02:00
const allResults = await this.findAllTargetConnectionsByTag(keyArg, payloadArg);
return allResults[0];
2021-01-28 12:58:42 +00:00
}
/**
* Stop the TypedSocket server/client
* Note: In SmartServe mode, SmartServe manages its own lifecycle
*/
2022-10-26 10:54:36 +02:00
public async stop() {
if (this.smartServeRef) {
// SmartServe manages its own lifecycle
// Clear our connection wrappers
this.smartServeConnectionWrappers.clear();
return;
}
2022-10-26 10:54:36 +02:00
await this.socketServerOrClient.stop();
}
}