@motiadev/core 0.15.6-beta.174 → 0.16.0-beta.175

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"tracer.mjs","names":["traceStream: MotiaStream<Trace>","traceGroupStream: MotiaStream<TraceGroup>","traceGroup: TraceGroup","traceStreamAdapter: MotiaStream<Trace>","traceGroupStreamAdapter: MotiaStream<TraceGroup>"],"sources":["../../../src/observability/tracer.ts"],"sourcesContent":["import { FileStreamAdapterManager } from '../adapters/defaults/stream/file-stream-adapter-manager'\nimport type { LockedData } from '../locked-data'\nimport type { Logger } from '../logger'\nimport type { Step } from '../types'\nimport type { MotiaStream } from '../types-stream'\nimport { createTrace } from './create-trace'\nimport type { TracerFactory } from './index'\nimport { RedisTraceStreamAdapter } from './redis-trace-stream-adapter'\nimport { StreamTracer } from './stream-tracer'\nimport { TraceManager } from './trace-manager'\nimport type { Trace, TraceGroup } from './types'\n\nexport class BaseTracerFactory implements TracerFactory {\n constructor(\n private readonly traceStream: MotiaStream<Trace>,\n private readonly traceGroupStream: MotiaStream<TraceGroup>,\n ) {}\n\n private async getAllGroups() {\n return await this.traceGroupStream.getGroup('default')\n }\n\n private async deleteGroup(group: TraceGroup) {\n const traces = await this.traceStream.getGroup(group.id)\n\n for (const trace of traces) {\n await this.traceStream.delete(group.id, trace.id)\n }\n await this.traceGroupStream.delete('default', group.id)\n }\n\n async clear() {\n const groups = await this.getAllGroups()\n\n for (const group of groups) {\n await this.deleteGroup(group)\n }\n }\n\n async createTracer(traceId: string, step: Step, logger: Logger) {\n const traceGroup: TraceGroup = {\n id: traceId,\n name: step.config.name,\n lastActivity: Date.now(),\n metadata: {\n completedSteps: 0,\n activeSteps: 0,\n totalSteps: 0,\n },\n correlationId: undefined,\n status: 'running',\n startTime: Date.now(),\n }\n\n const trace = createTrace(traceGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, traceGroup, trace)\n\n return new StreamTracer(manager, traceGroup, trace, logger)\n }\n\n async attachToTrace(traceId: string, step: Step, logger: Logger) {\n const existingGroup = await this.traceGroupStream.get('default', traceId)\n\n if (!existingGroup) {\n return this.createTracer(traceId, step, logger)\n }\n\n const trace = createTrace(existingGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, existingGroup, trace)\n\n return new StreamTracer(manager, existingGroup, trace, logger)\n }\n}\n\nexport const createTracerFactory = (lockedData: LockedData): TracerFactory => {\n if (!lockedData.redisClient) {\n throw new Error(\n 'Redis client is required for tracer factory. Please provide a redisClient when creating LockedData.',\n )\n }\n\n const traceStreamName = 'motia-trace'\n const traceStreamAdapter: MotiaStream<Trace> = new RedisTraceStreamAdapter(traceStreamName, lockedData.redisClient)\n\n const traceStream = lockedData.createStream<Trace>({\n filePath: traceStreamName,\n hidden: true,\n config: {\n name: traceStreamName,\n baseConfig: { storageType: 'custom', factory: () => traceStreamAdapter },\n schema: null as never,\n },\n })()\n\n const traceGroupName = 'motia-trace-group'\n const traceGroupStreamAdapter: MotiaStream<TraceGroup> = new RedisTraceStreamAdapter(\n traceGroupName,\n lockedData.redisClient,\n )\n\n const traceGroupStream = lockedData.createStream<TraceGroup>({\n filePath: traceGroupName,\n hidden: true,\n config: {\n name: traceGroupName,\n baseConfig: { storageType: 'custom', factory: () => traceGroupStreamAdapter },\n schema: null as never,\n },\n })()\n\n return new BaseTracerFactory(traceStream, traceGroupStream)\n}\n"],"mappings":";;;;;;AAYA,IAAa,oBAAb,MAAwD;CACtD,YACE,AAAiBA,aACjB,AAAiBC,kBACjB;EAFiB;EACA;;CAGnB,MAAc,eAAe;AAC3B,SAAO,MAAM,KAAK,iBAAiB,SAAS,UAAU;;CAGxD,MAAc,YAAY,OAAmB;EAC3C,MAAM,SAAS,MAAM,KAAK,YAAY,SAAS,MAAM,GAAG;AAExD,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,OAAO,MAAM,IAAI,MAAM,GAAG;AAEnD,QAAM,KAAK,iBAAiB,OAAO,WAAW,MAAM,GAAG;;CAGzD,MAAM,QAAQ;EACZ,MAAM,SAAS,MAAM,KAAK,cAAc;AAExC,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,MAAM;;CAIjC,MAAM,aAAa,SAAiB,MAAY,QAAgB;EAC9D,MAAMC,aAAyB;GAC7B,IAAI;GACJ,MAAM,KAAK,OAAO;GAClB,cAAc,KAAK,KAAK;GACxB,UAAU;IACR,gBAAgB;IAChB,aAAa;IACb,YAAY;IACb;GACD,eAAe;GACf,QAAQ;GACR,WAAW,KAAK,KAAK;GACtB;EAED,MAAM,QAAQ,YAAY,YAAY,KAAK;AAG3C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,YAAY,MAAM,EAE3D,YAAY,OAAO,OAAO;;CAG7D,MAAM,cAAc,SAAiB,MAAY,QAAgB;EAC/D,MAAM,gBAAgB,MAAM,KAAK,iBAAiB,IAAI,WAAW,QAAQ;AAEzE,MAAI,CAAC,cACH,QAAO,KAAK,aAAa,SAAS,MAAM,OAAO;EAGjD,MAAM,QAAQ,YAAY,eAAe,KAAK;AAG9C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,eAAe,MAAM,EAE9D,eAAe,OAAO,OAAO;;;AAIlE,MAAa,uBAAuB,eAA0C;AAC5E,KAAI,CAAC,WAAW,YACd,OAAM,IAAI,MACR,sGACD;CAGH,MAAM,kBAAkB;CACxB,MAAMC,qBAAyC,IAAI,wBAAwB,iBAAiB,WAAW,YAAY;CAEnH,MAAM,cAAc,WAAW,aAAoB;EACjD,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAoB;GACxE,QAAQ;GACT;EACF,CAAC,EAAE;CAEJ,MAAM,iBAAiB;CACvB,MAAMC,0BAAmD,IAAI,wBAC3D,gBACA,WAAW,YACZ;AAYD,QAAO,IAAI,kBAAkB,aAVJ,WAAW,aAAyB;EAC3D,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAyB;GAC7E,QAAQ;GACT;EACF,CAAC,EAAE,CAEuD"}
1
+ {"version":3,"file":"tracer.mjs","names":["traceStream: MotiaStream<Trace>","traceGroupStream: MotiaStream<TraceGroup>","traceGroup: TraceGroup","traceStreamAdapter: MotiaStream<Trace>","traceGroupStreamAdapter: MotiaStream<TraceGroup>"],"sources":["../../../src/observability/tracer.ts"],"sourcesContent":["import type { LockedData } from '../locked-data'\nimport type { Logger } from '../logger'\nimport type { Step } from '../types'\nimport type { MotiaStream } from '../types-stream'\nimport { createTrace } from './create-trace'\nimport type { TracerFactory } from './index'\nimport { RedisTraceStreamAdapter } from './redis-trace-stream-adapter'\nimport { StreamTracer } from './stream-tracer'\nimport { TraceManager } from './trace-manager'\nimport type { Trace, TraceGroup } from './types'\n\nexport class BaseTracerFactory implements TracerFactory {\n constructor(\n private readonly traceStream: MotiaStream<Trace>,\n private readonly traceGroupStream: MotiaStream<TraceGroup>,\n ) {}\n\n private async getAllGroups() {\n return await this.traceGroupStream.getGroup('default')\n }\n\n private async deleteGroup(group: TraceGroup) {\n const traces = await this.traceStream.getGroup(group.id)\n\n for (const trace of traces) {\n await this.traceStream.delete(group.id, trace.id)\n }\n await this.traceGroupStream.delete('default', group.id)\n }\n\n async clear() {\n const groups = await this.getAllGroups()\n\n for (const group of groups) {\n await this.deleteGroup(group)\n }\n }\n\n async createTracer(traceId: string, step: Step, logger: Logger) {\n const traceGroup: TraceGroup = {\n id: traceId,\n name: step.config.name,\n lastActivity: Date.now(),\n metadata: {\n completedSteps: 0,\n activeSteps: 0,\n totalSteps: 0,\n },\n correlationId: undefined,\n status: 'running',\n startTime: Date.now(),\n }\n\n const trace = createTrace(traceGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, traceGroup, trace)\n\n return new StreamTracer(manager, traceGroup, trace, logger)\n }\n\n async attachToTrace(traceId: string, step: Step, logger: Logger) {\n const existingGroup = await this.traceGroupStream.get('default', traceId)\n\n if (!existingGroup) {\n return this.createTracer(traceId, step, logger)\n }\n\n const trace = createTrace(existingGroup, step)\n const manager = new TraceManager(this.traceStream, this.traceGroupStream, existingGroup, trace)\n\n return new StreamTracer(manager, existingGroup, trace, logger)\n }\n}\n\nexport const createTracerFactory = (lockedData: LockedData): TracerFactory => {\n if (!lockedData.redisClient) {\n throw new Error(\n 'Redis client is required for tracer factory. Please provide a redisClient when creating LockedData.',\n )\n }\n\n const traceStreamName = 'motia-trace'\n const traceStreamAdapter: MotiaStream<Trace> = new RedisTraceStreamAdapter(traceStreamName, lockedData.redisClient)\n\n const traceStream = lockedData.createStream<Trace>({\n filePath: traceStreamName,\n hidden: true,\n config: {\n name: traceStreamName,\n baseConfig: { storageType: 'custom', factory: () => traceStreamAdapter },\n schema: null as never,\n },\n })()\n\n const traceGroupName = 'motia-trace-group'\n const traceGroupStreamAdapter: MotiaStream<TraceGroup> = new RedisTraceStreamAdapter(\n traceGroupName,\n lockedData.redisClient,\n )\n\n const traceGroupStream = lockedData.createStream<TraceGroup>({\n filePath: traceGroupName,\n hidden: true,\n config: {\n name: traceGroupName,\n baseConfig: { storageType: 'custom', factory: () => traceGroupStreamAdapter },\n schema: null as never,\n },\n })()\n\n return new BaseTracerFactory(traceStream, traceGroupStream)\n}\n"],"mappings":";;;;;;AAWA,IAAa,oBAAb,MAAwD;CACtD,YACE,AAAiBA,aACjB,AAAiBC,kBACjB;EAFiB;EACA;;CAGnB,MAAc,eAAe;AAC3B,SAAO,MAAM,KAAK,iBAAiB,SAAS,UAAU;;CAGxD,MAAc,YAAY,OAAmB;EAC3C,MAAM,SAAS,MAAM,KAAK,YAAY,SAAS,MAAM,GAAG;AAExD,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,OAAO,MAAM,IAAI,MAAM,GAAG;AAEnD,QAAM,KAAK,iBAAiB,OAAO,WAAW,MAAM,GAAG;;CAGzD,MAAM,QAAQ;EACZ,MAAM,SAAS,MAAM,KAAK,cAAc;AAExC,OAAK,MAAM,SAAS,OAClB,OAAM,KAAK,YAAY,MAAM;;CAIjC,MAAM,aAAa,SAAiB,MAAY,QAAgB;EAC9D,MAAMC,aAAyB;GAC7B,IAAI;GACJ,MAAM,KAAK,OAAO;GAClB,cAAc,KAAK,KAAK;GACxB,UAAU;IACR,gBAAgB;IAChB,aAAa;IACb,YAAY;IACb;GACD,eAAe;GACf,QAAQ;GACR,WAAW,KAAK,KAAK;GACtB;EAED,MAAM,QAAQ,YAAY,YAAY,KAAK;AAG3C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,YAAY,MAAM,EAE3D,YAAY,OAAO,OAAO;;CAG7D,MAAM,cAAc,SAAiB,MAAY,QAAgB;EAC/D,MAAM,gBAAgB,MAAM,KAAK,iBAAiB,IAAI,WAAW,QAAQ;AAEzE,MAAI,CAAC,cACH,QAAO,KAAK,aAAa,SAAS,MAAM,OAAO;EAGjD,MAAM,QAAQ,YAAY,eAAe,KAAK;AAG9C,SAAO,IAAI,aAFK,IAAI,aAAa,KAAK,aAAa,KAAK,kBAAkB,eAAe,MAAM,EAE9D,eAAe,OAAO,OAAO;;;AAIlE,MAAa,uBAAuB,eAA0C;AAC5E,KAAI,CAAC,WAAW,YACd,OAAM,IAAI,MACR,sGACD;CAGH,MAAM,kBAAkB;CACxB,MAAMC,qBAAyC,IAAI,wBAAwB,iBAAiB,WAAW,YAAY;CAEnH,MAAM,cAAc,WAAW,aAAoB;EACjD,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAoB;GACxE,QAAQ;GACT;EACF,CAAC,EAAE;CAEJ,MAAM,iBAAiB;CACvB,MAAMC,0BAAmD,IAAI,wBAC3D,gBACA,WAAW,YACZ;AAYD,QAAO,IAAI,kBAAkB,aAVJ,WAAW,aAAyB;EAC3D,UAAU;EACV,QAAQ;EACR,QAAQ;GACN,MAAM;GACN,YAAY;IAAE,aAAa;IAAU,eAAe;IAAyB;GAC7E,QAAQ;GACT;EACF,CAAC,EAAE,CAEuD"}
@@ -1,6 +1,6 @@
1
1
  import { globalLogger } from "./logger.mjs";
2
2
  import { getRoom, sendAccessDenied, sendError } from "./socket-server/helpers.mjs";
3
- import { WebSocketServer } from "ws";
3
+ import { WebSocket, WebSocketServer } from "ws";
4
4
 
5
5
  //#region src/socket-server.ts
6
6
  const AUTH_ERROR_CODE = 401;
@@ -37,9 +37,9 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
37
37
  };
38
38
  socketServer.on("connection", async (socket, request) => {
39
39
  authContexts.set(socket, request.authContext);
40
- subscriptions.set(socket, /* @__PURE__ */ new Set());
41
- socket.on("message", async (payload) => {
42
- const message = JSON.parse(payload.toString());
40
+ subscriptions.set(socket, /* @__PURE__ */ new Map());
41
+ let messageQueue = Promise.resolve();
42
+ const processMessage = async (message) => {
43
43
  if (message.type === "join") {
44
44
  if (!await isAuthorized(socket, message.data)) {
45
45
  sendAccessDenied(socket, message.data);
@@ -78,14 +78,28 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
78
78
  }
79
79
  }
80
80
  rooms[room].set(message.data.subscriptionId, socket);
81
- subscriptions.get(socket)?.add([room, message.data.subscriptionId]);
81
+ subscriptions.get(socket)?.set(message.data.subscriptionId, room);
82
82
  } else if (message.type === "leave") {
83
+ if (!message.data.subscriptionId) {
84
+ globalLogger.error("[Socket Server] Subscription ID is required for leave message");
85
+ return;
86
+ }
83
87
  const room = getRoom(message.data);
84
- if (rooms[room]) rooms[room].delete(message.data.subscriptionId);
88
+ if (rooms[room]) {
89
+ rooms[room].delete(message.data.subscriptionId);
90
+ if (rooms[room].size === 0) delete rooms[room];
91
+ }
92
+ subscriptions.get(socket)?.delete(message.data.subscriptionId);
85
93
  }
94
+ };
95
+ socket.on("message", (payload) => {
96
+ const message = JSON.parse(payload.toString());
97
+ messageQueue = messageQueue.then(() => processMessage(message)).catch((error) => {
98
+ globalLogger.error("[Socket Server] Error processing message", error);
99
+ });
86
100
  });
87
101
  socket.on("close", () => {
88
- subscriptions.get(socket)?.forEach(([room, subscriptionId]) => {
102
+ subscriptions.get(socket)?.forEach((room, subscriptionId) => {
89
103
  rooms[room]?.delete(subscriptionId);
90
104
  if (rooms[room]?.size === 0) delete rooms[room];
91
105
  });
@@ -103,18 +117,21 @@ const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authori
103
117
  timestamp: Date.now(),
104
118
  ...message
105
119
  });
106
- if (rooms[groupRoom]) rooms[groupRoom].forEach((socket) => {
107
- socket.send(eventMessage);
108
- });
120
+ const safeSend = (socket) => {
121
+ if (socket.readyState === WebSocket.OPEN) try {
122
+ socket.send(eventMessage);
123
+ } catch (error) {
124
+ globalLogger.debug("[Socket Server] Failed to send message to socket", error);
125
+ }
126
+ };
127
+ if (rooms[groupRoom]) rooms[groupRoom].forEach(safeSend);
109
128
  if (id) {
110
129
  const itemRoom = getRoom({
111
130
  groupId,
112
131
  streamName,
113
132
  id
114
133
  });
115
- if (rooms[itemRoom]) rooms[itemRoom].forEach((socket) => {
116
- socket.send(eventMessage);
117
- });
134
+ if (rooms[itemRoom]) rooms[itemRoom].forEach(safeSend);
118
135
  }
119
136
  };
120
137
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"socket-server.mjs","names":["authRequest: StreamAuthRequest","rooms: Record<string, Map<string, WebSocket>>","subscriptions: Map<WebSocket, Set<[string, string]>>","authContexts: Map<WebSocket, unknown>","message: Message","resultMessage: EventMessage<typeof item>","resultMessage: EventMessage<typeof items>"],"sources":["../../src/socket-server.ts"],"sourcesContent":["import type { Server } from 'http'\nimport { type WebSocket, WebSocketServer } from 'ws'\nimport { globalLogger } from './logger'\nimport {\n type BaseMessage,\n type EventMessage,\n getRoom,\n type JoinMessage,\n sendAccessDenied,\n sendError,\n} from './socket-server/helpers'\nimport type { StreamAuthRequest } from './types/app-config-types'\n\ntype Message = { type: 'join' | 'leave'; data: JoinMessage }\n\ntype Props = {\n server: Server\n onJoin: <TData>(streamName: string, groupId: string, id: string) => Promise<TData>\n onJoinGroup: <TData>(streamName: string, groupId: string) => Promise<TData[] | undefined>\n authenticate?: (request: StreamAuthRequest) => Promise<unknown | null> | unknown | null\n authorize?: (\n subscription: { streamName: string; groupId: string; id?: string },\n authContext?: unknown,\n ) => Promise<boolean> | boolean\n}\n\nconst AUTH_ERROR_CODE = 401\nexport const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authorize }: Props) => {\n const socketServer = new WebSocketServer({\n server,\n verifyClient: async (info, callback) => {\n if (authenticate) {\n try {\n const authRequest: StreamAuthRequest = {\n headers: info.req.headers,\n url: info.req.url,\n }\n info.req.authContext = await authenticate(authRequest)\n callback(true)\n } catch {\n globalLogger.debug('[Socket Server] Authentication failed')\n callback(false, AUTH_ERROR_CODE, 'Authentication failed')\n }\n } else {\n callback(true)\n }\n },\n })\n const rooms: Record<string, Map<string, WebSocket>> = {}\n const subscriptions: Map<WebSocket, Set<[string, string]>> = new Map()\n const authContexts: Map<WebSocket, unknown> = new Map()\n\n const isAuthorized = async (socket: WebSocket, data: BaseMessage): Promise<boolean> => {\n if (!authorize) {\n return true\n }\n\n try {\n const authContext = authContexts.get(socket)\n const result = await authorize(data, authContext)\n return result !== false\n } catch (error) {\n sendError(socket, data, error as Error)\n globalLogger.error('[Socket Server] Failed to authorize stream subscription')\n return false\n }\n }\n\n socketServer.on('connection', async (socket, request) => {\n authContexts.set(socket, request.authContext)\n\n subscriptions.set(socket, new Set())\n\n socket.on('message', async (payload: Buffer) => {\n const message: Message = JSON.parse(payload.toString())\n\n if (message.type === 'join') {\n const authorized = await isAuthorized(socket, message.data)\n\n if (!authorized) {\n sendAccessDenied(socket, message.data)\n return\n }\n\n const room = getRoom(message.data)\n\n if (!rooms[room]) {\n rooms[room] = new Map()\n }\n\n if (message.data.id) {\n const item = await onJoin(message.data.streamName, message.data.groupId, message.data.id)\n\n if (item) {\n const resultMessage: EventMessage<typeof item> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n id: message.data.id,\n event: { type: 'sync', data: item },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n } else {\n const items = await onJoinGroup(message.data.streamName, message.data.groupId)\n\n if (items) {\n const resultMessage: EventMessage<typeof items> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n event: { type: 'sync', data: items },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n }\n\n rooms[room].set(message.data.subscriptionId, socket)\n subscriptions.get(socket)?.add([room, message.data.subscriptionId])\n } else if (message.type === 'leave') {\n const room = getRoom(message.data)\n\n if (rooms[room]) {\n rooms[room].delete(message.data.subscriptionId)\n }\n }\n })\n\n socket.on('close', () => {\n subscriptions.get(socket)?.forEach(([room, subscriptionId]) => {\n rooms[room]?.delete(subscriptionId)\n\n if (rooms[room]?.size === 0) {\n delete rooms[room]\n }\n })\n subscriptions.delete(socket)\n authContexts.delete(socket)\n })\n })\n\n const pushEvent = <TData>(message: Omit<EventMessage<TData>, 'timestamp'>) => {\n const { groupId, streamName, id } = message\n const groupRoom = getRoom({ streamName, groupId })\n const eventMessage = JSON.stringify({ timestamp: Date.now(), ...message })\n\n if (rooms[groupRoom]) {\n rooms[groupRoom].forEach((socket) => {\n socket.send(eventMessage)\n })\n }\n\n if (id) {\n const itemRoom = getRoom({ groupId, streamName, id })\n\n if (rooms[itemRoom]) {\n rooms[itemRoom].forEach((socket) => {\n socket.send(eventMessage)\n })\n }\n }\n }\n\n return { pushEvent, socketServer }\n}\n"],"mappings":";;;;;AA0BA,MAAM,kBAAkB;AACxB,MAAa,sBAAsB,EAAE,QAAQ,QAAQ,aAAa,cAAc,gBAAuB;CACrG,MAAM,eAAe,IAAI,gBAAgB;EACvC;EACA,cAAc,OAAO,MAAM,aAAa;AACtC,OAAI,aACF,KAAI;IACF,MAAMA,cAAiC;KACrC,SAAS,KAAK,IAAI;KAClB,KAAK,KAAK,IAAI;KACf;AACD,SAAK,IAAI,cAAc,MAAM,aAAa,YAAY;AACtD,aAAS,KAAK;WACR;AACN,iBAAa,MAAM,wCAAwC;AAC3D,aAAS,OAAO,iBAAiB,wBAAwB;;OAG3D,UAAS,KAAK;;EAGnB,CAAC;CACF,MAAMC,QAAgD,EAAE;CACxD,MAAMC,gCAAuD,IAAI,KAAK;CACtE,MAAMC,+BAAwC,IAAI,KAAK;CAEvD,MAAM,eAAe,OAAO,QAAmB,SAAwC;AACrF,MAAI,CAAC,UACH,QAAO;AAGT,MAAI;AAGF,UADe,MAAM,UAAU,MADX,aAAa,IAAI,OAAO,CACK,KAC/B;WACX,OAAO;AACd,aAAU,QAAQ,MAAM,MAAe;AACvC,gBAAa,MAAM,0DAA0D;AAC7E,UAAO;;;AAIX,cAAa,GAAG,cAAc,OAAO,QAAQ,YAAY;AACvD,eAAa,IAAI,QAAQ,QAAQ,YAAY;AAE7C,gBAAc,IAAI,wBAAQ,IAAI,KAAK,CAAC;AAEpC,SAAO,GAAG,WAAW,OAAO,YAAoB;GAC9C,MAAMC,UAAmB,KAAK,MAAM,QAAQ,UAAU,CAAC;AAEvD,OAAI,QAAQ,SAAS,QAAQ;AAG3B,QAAI,CAFe,MAAM,aAAa,QAAQ,QAAQ,KAAK,EAE1C;AACf,sBAAiB,QAAQ,QAAQ,KAAK;AACtC;;IAGF,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAElC,QAAI,CAAC,MAAM,MACT,OAAM,wBAAQ,IAAI,KAAK;AAGzB,QAAI,QAAQ,KAAK,IAAI;KACnB,MAAM,OAAO,MAAM,OAAO,QAAQ,KAAK,YAAY,QAAQ,KAAK,SAAS,QAAQ,KAAK,GAAG;AAEzF,SAAI,MAAM;MACR,MAAMC,gBAA2C;OAC/C,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,IAAI,QAAQ,KAAK;OACjB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAM;OACpC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;WAEvC;KACL,MAAM,QAAQ,MAAM,YAAY,QAAQ,KAAK,YAAY,QAAQ,KAAK,QAAQ;AAE9E,SAAI,OAAO;MACT,MAAMC,gBAA4C;OAChD,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAO;OACrC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;;AAI9C,UAAM,MAAM,IAAI,QAAQ,KAAK,gBAAgB,OAAO;AACpD,kBAAc,IAAI,OAAO,EAAE,IAAI,CAAC,MAAM,QAAQ,KAAK,eAAe,CAAC;cAC1D,QAAQ,SAAS,SAAS;IACnC,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAElC,QAAI,MAAM,MACR,OAAM,MAAM,OAAO,QAAQ,KAAK,eAAe;;IAGnD;AAEF,SAAO,GAAG,eAAe;AACvB,iBAAc,IAAI,OAAO,EAAE,SAAS,CAAC,MAAM,oBAAoB;AAC7D,UAAM,OAAO,OAAO,eAAe;AAEnC,QAAI,MAAM,OAAO,SAAS,EACxB,QAAO,MAAM;KAEf;AACF,iBAAc,OAAO,OAAO;AAC5B,gBAAa,OAAO,OAAO;IAC3B;GACF;CAEF,MAAM,aAAoB,YAAoD;EAC5E,MAAM,EAAE,SAAS,YAAY,OAAO;EACpC,MAAM,YAAY,QAAQ;GAAE;GAAY;GAAS,CAAC;EAClD,MAAM,eAAe,KAAK,UAAU;GAAE,WAAW,KAAK,KAAK;GAAE,GAAG;GAAS,CAAC;AAE1E,MAAI,MAAM,WACR,OAAM,WAAW,SAAS,WAAW;AACnC,UAAO,KAAK,aAAa;IACzB;AAGJ,MAAI,IAAI;GACN,MAAM,WAAW,QAAQ;IAAE;IAAS;IAAY;IAAI,CAAC;AAErD,OAAI,MAAM,UACR,OAAM,UAAU,SAAS,WAAW;AAClC,WAAO,KAAK,aAAa;KACzB;;;AAKR,QAAO;EAAE;EAAW;EAAc"}
1
+ {"version":3,"file":"socket-server.mjs","names":["authRequest: StreamAuthRequest","rooms: Record<string, Map<string, WebSocket>>","subscriptions: Map<WebSocket, Map<string, string>>","authContexts: Map<WebSocket, unknown>","messageQueue: Promise<void>","resultMessage: EventMessage<typeof item>","resultMessage: EventMessage<typeof items>","message: Message"],"sources":["../../src/socket-server.ts"],"sourcesContent":["import type { Server } from 'http'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport { globalLogger } from './logger'\nimport {\n type BaseMessage,\n type EventMessage,\n getRoom,\n type JoinMessage,\n sendAccessDenied,\n sendError,\n} from './socket-server/helpers'\nimport type { StreamAuthRequest } from './types/app-config-types'\n\ntype Message = { type: 'join' | 'leave'; data: JoinMessage }\n\ntype Props = {\n server: Server\n onJoin: <TData>(streamName: string, groupId: string, id: string) => Promise<TData>\n onJoinGroup: <TData>(streamName: string, groupId: string) => Promise<TData[] | undefined>\n authenticate?: (request: StreamAuthRequest) => Promise<unknown | null> | unknown | null\n authorize?: (\n subscription: { streamName: string; groupId: string; id?: string },\n authContext?: unknown,\n ) => Promise<boolean> | boolean\n}\n\nconst AUTH_ERROR_CODE = 401\nexport const createSocketServer = ({ server, onJoin, onJoinGroup, authenticate, authorize }: Props) => {\n const socketServer = new WebSocketServer({\n server,\n verifyClient: async (info, callback) => {\n if (authenticate) {\n try {\n const authRequest: StreamAuthRequest = {\n headers: info.req.headers,\n url: info.req.url,\n }\n info.req.authContext = await authenticate(authRequest)\n callback(true)\n } catch {\n globalLogger.debug('[Socket Server] Authentication failed')\n callback(false, AUTH_ERROR_CODE, 'Authentication failed')\n }\n } else {\n callback(true)\n }\n },\n })\n const rooms: Record<string, Map<string, WebSocket>> = {}\n const subscriptions: Map<WebSocket, Map<string, string>> = new Map()\n const authContexts: Map<WebSocket, unknown> = new Map()\n\n const isAuthorized = async (socket: WebSocket, data: BaseMessage): Promise<boolean> => {\n if (!authorize) {\n return true\n }\n\n try {\n const authContext = authContexts.get(socket)\n const result = await authorize(data, authContext)\n return result !== false\n } catch (error) {\n sendError(socket, data, error as Error)\n globalLogger.error('[Socket Server] Failed to authorize stream subscription')\n return false\n }\n }\n\n socketServer.on('connection', async (socket, request) => {\n authContexts.set(socket, request.authContext)\n\n subscriptions.set(socket, new Map())\n\n // Message queue to ensure messages are processed in order\n // This prevents race conditions where async join handlers allow leave to overtake join\n let messageQueue: Promise<void> = Promise.resolve()\n\n const processMessage = async (message: Message) => {\n if (message.type === 'join') {\n const authorized = await isAuthorized(socket, message.data)\n\n if (!authorized) {\n sendAccessDenied(socket, message.data)\n return\n }\n\n const room = getRoom(message.data)\n\n if (!rooms[room]) {\n rooms[room] = new Map()\n }\n\n if (message.data.id) {\n const item = await onJoin(message.data.streamName, message.data.groupId, message.data.id)\n\n if (item) {\n const resultMessage: EventMessage<typeof item> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n id: message.data.id,\n event: { type: 'sync', data: item },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n } else {\n const items = await onJoinGroup(message.data.streamName, message.data.groupId)\n\n if (items) {\n const resultMessage: EventMessage<typeof items> = {\n timestamp: Date.now(),\n streamName: message.data.streamName,\n groupId: message.data.groupId,\n event: { type: 'sync', data: items },\n }\n\n socket.send(JSON.stringify(resultMessage))\n }\n }\n\n rooms[room].set(message.data.subscriptionId, socket)\n subscriptions.get(socket)?.set(message.data.subscriptionId, room)\n } else if (message.type === 'leave') {\n if (!message.data.subscriptionId) {\n globalLogger.error('[Socket Server] Subscription ID is required for leave message')\n return\n }\n\n const room = getRoom(message.data)\n if (rooms[room]) {\n rooms[room].delete(message.data.subscriptionId)\n if (rooms[room].size === 0) {\n delete rooms[room]\n }\n }\n\n subscriptions.get(socket)?.delete(message.data.subscriptionId)\n }\n }\n\n socket.on('message', (payload: Buffer) => {\n const message: Message = JSON.parse(payload.toString())\n // Chain messages to ensure they are processed in order\n messageQueue = messageQueue\n .then(() => processMessage(message))\n .catch((error) => {\n globalLogger.error('[Socket Server] Error processing message', error)\n })\n })\n\n socket.on('close', () => {\n subscriptions.get(socket)?.forEach((room, subscriptionId) => {\n rooms[room]?.delete(subscriptionId)\n\n if (rooms[room]?.size === 0) {\n delete rooms[room]\n }\n })\n subscriptions.delete(socket)\n authContexts.delete(socket)\n })\n })\n\n const pushEvent = <TData>(message: Omit<EventMessage<TData>, 'timestamp'>) => {\n const { groupId, streamName, id } = message\n const groupRoom = getRoom({ streamName, groupId })\n const eventMessage = JSON.stringify({ timestamp: Date.now(), ...message })\n\n const safeSend = (socket: WebSocket) => {\n if (socket.readyState === WebSocket.OPEN) {\n try {\n socket.send(eventMessage)\n } catch (error) {\n globalLogger.debug('[Socket Server] Failed to send message to socket', error)\n }\n }\n }\n\n if (rooms[groupRoom]) {\n rooms[groupRoom].forEach(safeSend)\n }\n\n if (id) {\n const itemRoom = getRoom({ groupId, streamName, id })\n\n if (rooms[itemRoom]) {\n rooms[itemRoom].forEach(safeSend)\n }\n }\n }\n\n return { pushEvent, socketServer }\n}\n"],"mappings":";;;;;AA0BA,MAAM,kBAAkB;AACxB,MAAa,sBAAsB,EAAE,QAAQ,QAAQ,aAAa,cAAc,gBAAuB;CACrG,MAAM,eAAe,IAAI,gBAAgB;EACvC;EACA,cAAc,OAAO,MAAM,aAAa;AACtC,OAAI,aACF,KAAI;IACF,MAAMA,cAAiC;KACrC,SAAS,KAAK,IAAI;KAClB,KAAK,KAAK,IAAI;KACf;AACD,SAAK,IAAI,cAAc,MAAM,aAAa,YAAY;AACtD,aAAS,KAAK;WACR;AACN,iBAAa,MAAM,wCAAwC;AAC3D,aAAS,OAAO,iBAAiB,wBAAwB;;OAG3D,UAAS,KAAK;;EAGnB,CAAC;CACF,MAAMC,QAAgD,EAAE;CACxD,MAAMC,gCAAqD,IAAI,KAAK;CACpE,MAAMC,+BAAwC,IAAI,KAAK;CAEvD,MAAM,eAAe,OAAO,QAAmB,SAAwC;AACrF,MAAI,CAAC,UACH,QAAO;AAGT,MAAI;AAGF,UADe,MAAM,UAAU,MADX,aAAa,IAAI,OAAO,CACK,KAC/B;WACX,OAAO;AACd,aAAU,QAAQ,MAAM,MAAe;AACvC,gBAAa,MAAM,0DAA0D;AAC7E,UAAO;;;AAIX,cAAa,GAAG,cAAc,OAAO,QAAQ,YAAY;AACvD,eAAa,IAAI,QAAQ,QAAQ,YAAY;AAE7C,gBAAc,IAAI,wBAAQ,IAAI,KAAK,CAAC;EAIpC,IAAIC,eAA8B,QAAQ,SAAS;EAEnD,MAAM,iBAAiB,OAAO,YAAqB;AACjD,OAAI,QAAQ,SAAS,QAAQ;AAG3B,QAAI,CAFe,MAAM,aAAa,QAAQ,QAAQ,KAAK,EAE1C;AACf,sBAAiB,QAAQ,QAAQ,KAAK;AACtC;;IAGF,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAElC,QAAI,CAAC,MAAM,MACT,OAAM,wBAAQ,IAAI,KAAK;AAGzB,QAAI,QAAQ,KAAK,IAAI;KACnB,MAAM,OAAO,MAAM,OAAO,QAAQ,KAAK,YAAY,QAAQ,KAAK,SAAS,QAAQ,KAAK,GAAG;AAEzF,SAAI,MAAM;MACR,MAAMC,gBAA2C;OAC/C,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,IAAI,QAAQ,KAAK;OACjB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAM;OACpC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;WAEvC;KACL,MAAM,QAAQ,MAAM,YAAY,QAAQ,KAAK,YAAY,QAAQ,KAAK,QAAQ;AAE9E,SAAI,OAAO;MACT,MAAMC,gBAA4C;OAChD,WAAW,KAAK,KAAK;OACrB,YAAY,QAAQ,KAAK;OACzB,SAAS,QAAQ,KAAK;OACtB,OAAO;QAAE,MAAM;QAAQ,MAAM;QAAO;OACrC;AAED,aAAO,KAAK,KAAK,UAAU,cAAc,CAAC;;;AAI9C,UAAM,MAAM,IAAI,QAAQ,KAAK,gBAAgB,OAAO;AACpD,kBAAc,IAAI,OAAO,EAAE,IAAI,QAAQ,KAAK,gBAAgB,KAAK;cACxD,QAAQ,SAAS,SAAS;AACnC,QAAI,CAAC,QAAQ,KAAK,gBAAgB;AAChC,kBAAa,MAAM,gEAAgE;AACnF;;IAGF,MAAM,OAAO,QAAQ,QAAQ,KAAK;AAClC,QAAI,MAAM,OAAO;AACf,WAAM,MAAM,OAAO,QAAQ,KAAK,eAAe;AAC/C,SAAI,MAAM,MAAM,SAAS,EACvB,QAAO,MAAM;;AAIjB,kBAAc,IAAI,OAAO,EAAE,OAAO,QAAQ,KAAK,eAAe;;;AAIlE,SAAO,GAAG,YAAY,YAAoB;GACxC,MAAMC,UAAmB,KAAK,MAAM,QAAQ,UAAU,CAAC;AAEvD,kBAAe,aACZ,WAAW,eAAe,QAAQ,CAAC,CACnC,OAAO,UAAU;AAChB,iBAAa,MAAM,4CAA4C,MAAM;KACrE;IACJ;AAEF,SAAO,GAAG,eAAe;AACvB,iBAAc,IAAI,OAAO,EAAE,SAAS,MAAM,mBAAmB;AAC3D,UAAM,OAAO,OAAO,eAAe;AAEnC,QAAI,MAAM,OAAO,SAAS,EACxB,QAAO,MAAM;KAEf;AACF,iBAAc,OAAO,OAAO;AAC5B,gBAAa,OAAO,OAAO;IAC3B;GACF;CAEF,MAAM,aAAoB,YAAoD;EAC5E,MAAM,EAAE,SAAS,YAAY,OAAO;EACpC,MAAM,YAAY,QAAQ;GAAE;GAAY;GAAS,CAAC;EAClD,MAAM,eAAe,KAAK,UAAU;GAAE,WAAW,KAAK,KAAK;GAAE,GAAG;GAAS,CAAC;EAE1E,MAAM,YAAY,WAAsB;AACtC,OAAI,OAAO,eAAe,UAAU,KAClC,KAAI;AACF,WAAO,KAAK,aAAa;YAClB,OAAO;AACd,iBAAa,MAAM,oDAAoD,MAAM;;;AAKnF,MAAI,MAAM,WACR,OAAM,WAAW,QAAQ,SAAS;AAGpC,MAAI,IAAI;GACN,MAAM,WAAW,QAAQ;IAAE;IAAS;IAAY;IAAI,CAAC;AAErD,OAAI,MAAM,UACR,OAAM,UAAU,QAAQ,SAAS;;;AAKvC,QAAO;EAAE;EAAW;EAAc"}
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "import": "./dist/index.mjs"
12
12
  }
13
13
  },
14
- "version": "0.15.6-beta.174",
14
+ "version": "0.16.0-beta.175",
15
15
  "dependencies": {
16
16
  "@amplitude/analytics-node": "^1.5.26",
17
17
  "@standard-schema/spec": "^1.0.0",