@mesh-kit/server 2.0.3 → 2.0.4
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.
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1051,7 +1051,7 @@ declare class PersistenceManager extends EventEmitter$1 {
|
|
|
1051
1051
|
/**
|
|
1052
1052
|
* Restores persisted records from storage into Redis on startup
|
|
1053
1053
|
*/
|
|
1054
|
-
|
|
1054
|
+
restorePersistedRecords(): Promise<void>;
|
|
1055
1055
|
/**
|
|
1056
1056
|
* Handle a message received from the internal message stream.
|
|
1057
1057
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -1051,7 +1051,7 @@ declare class PersistenceManager extends EventEmitter$1 {
|
|
|
1051
1051
|
/**
|
|
1052
1052
|
* Restores persisted records from storage into Redis on startup
|
|
1053
1053
|
*/
|
|
1054
|
-
|
|
1054
|
+
restorePersistedRecords(): Promise<void>;
|
|
1055
1055
|
/**
|
|
1056
1056
|
* Handle a message received from the internal message stream.
|
|
1057
1057
|
*/
|
package/dist/index.js
CHANGED
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
DO UPDATE SET version = $2, value = $3, timestamp = $4`,[t.recordId,t.version,t.value,t.timestamp]);await e.query("COMMIT")}catch(t){throw await e.query("ROLLBACK"),d.error("Error in storeRecords:",t),t}finally{e.release()}}async getRecords(r){if(!this.pool)throw new Error("Database not initialized");let e=O(r);d.debug(`PostgreSQL: Getting records matching pattern: ${r} (SQL: ${e})`);let t=await this.pool.query(`SELECT record_id, version, value, timestamp
|
|
54
54
|
FROM records
|
|
55
55
|
WHERE record_id LIKE $1
|
|
56
|
-
ORDER BY timestamp DESC`,[e]);return d.debug(`PostgreSQL: Found ${t.rows.length} records matching pattern ${r}`),t.rows.map(s=>({recordId:s.record_id,version:s.version,value:s.value,timestamp:parseInt(s.timestamp)}))}async close(){this.pool&&(await this.pool.end(),this.pool=null,this.initialized=!1)}};var $=class extends fe.EventEmitter{constructor(e){super();this.channelPatterns=[];this.recordPatterns=[];this.messageBuffer=new Map;this.recordBuffer=new Map;this.flushTimers=new Map;this.recordFlushTimer=null;this.isShuttingDown=!1;this.initialized=!1;this.recordManager=null;this.pendingRecordUpdates=[];let{defaultAdapterOptions:t={},adapterType:s="sqlite"}=e;s==="postgres"?this.defaultAdapter=new x(t):this.defaultAdapter=new N(t),this.messageStream=y.getInstance()}setRecordManager(e){this.recordManager=e}async ready(){return this.initialized?Promise.resolve():new Promise(e=>{this.once("initialized",e)})}async processPendingRecordUpdates(){if(this.pendingRecordUpdates.length===0)return;d.info(`Processing ${this.pendingRecordUpdates.length} pending record updates`);let e=[...this.pendingRecordUpdates];this.pendingRecordUpdates=[];for(let{recordId:t,value:s,version:n}of e)this.handleRecordUpdate(t,s,n)}async initialize(){if(!this.initialized)try{await this.defaultAdapter.initialize(),this.messageStream.subscribeToMessages(this.handleStreamMessage.bind(this)),await this.restorePersistedRecords(),this.initialized=!0,await this.processPendingRecordUpdates(),this.emit("initialized")}catch(e){throw d.error("Failed to initialize persistence manager:",e),e}}async restorePersistedRecords(){if(!this.recordManager){d.warn("Cannot restore persisted records: record manager not available");return}let e=this.recordManager.getRedis();if(!e){d.warn("Cannot restore records: Redis not available");return}try{d.info("Restoring persisted records...");let t=this.recordPatterns.map(s=>typeof s.pattern=="string"?s.pattern:s.pattern.source);if(t.length===0){d.info("No record patterns to restore");return}for(let s of t)try{let n=O(s),i=await this.defaultAdapter.getRecords?.(n)||[];if(i.length>0){d.info(`Restoring ${i.length} records for pattern ${s}`);for(let o of i)try{let{recordId:c,value:l,version:p}=o,h=typeof l=="string"?JSON.parse(l):l,u=this.recordManager.recordKey(c),b=this.recordManager.recordVersionKey(c),A=e.pipeline();A.set(u,JSON.stringify(h)),A.set(b,p.toString()),await A.exec(),d.debug(`Restored record ${c} (version ${p})`)}catch(c){d.error(`Failed to parse record value for recordId: ${o.recordId}: ${c}`)}}else d.debug(`No records found for pattern ${s}`)}catch(n){d.error(`Error restoring records for pattern ${s}: ${n}`)}d.info("Finished restoring persisted records")}catch(t){d.error("Failed to restore persisted records:",t)}}handleStreamMessage(e){let{channel:t,message:s,instanceId:n,timestamp:i}=e;this.handleChannelMessage(t,s,n,i)}enableChannelPersistence(e,t={}){let s={historyLimit:t.historyLimit??50,filter:t.filter??(()=>!0),adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for pattern ${e}:`,n)}),this.channelPatterns.push({pattern:e,options:s})}enableRecordPersistence(e,t={}){let s={adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for record pattern ${e}:`,n)}),this.recordPatterns.push({pattern:e,options:s})}getChannelPersistenceOptions(e){for(let{pattern:t,options:s}of this.channelPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}getRecordPersistenceOptions(e){for(let{pattern:t,options:s}of this.recordPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}handleChannelMessage(e,t,s,n){if(!this.initialized||this.isShuttingDown)return;let i=this.getChannelPersistenceOptions(e);if(!i||!i.filter(t,e))return;let o={id:(0,ye.v4)(),channel:e,message:t,instanceId:s,timestamp:n||Date.now()};if(this.messageBuffer.has(e)||this.messageBuffer.set(e,[]),this.messageBuffer.get(e).push(o),this.messageBuffer.get(e).length>=i.maxBufferSize){this.flushChannel(e);return}if(!this.flushTimers.has(e)){let c=setTimeout(()=>{this.flushChannel(e)},i.flushInterval);c.unref&&c.unref(),this.flushTimers.set(e,c)}}async flushChannel(e){if(!this.messageBuffer.has(e))return;this.flushTimers.has(e)&&(clearTimeout(this.flushTimers.get(e)),this.flushTimers.delete(e));let t=this.messageBuffer.get(e);if(t.length===0)return;this.messageBuffer.set(e,[]);let s=this.getChannelPersistenceOptions(e);if(s)try{await s.adapter.storeMessages(t),this.emit("flushed",{channel:e,count:t.length}),d.debug(`Flushed ${t.length} messages for channel ${e}`)}catch(n){if(d.error(`Failed to flush messages for channel ${e}:`,n),!this.isShuttingDown){let i=this.messageBuffer.get(e)||[];if(this.messageBuffer.set(e,[...t,...i]),!this.flushTimers.has(e)){let o=setTimeout(()=>{this.flushChannel(e)},1e3);o.unref&&o.unref(),this.flushTimers.set(e,o)}}}}async flushAll(){let e=Array.from(this.messageBuffer.keys());for(let t of e)await this.flushChannel(t)}async getMessages(e,t,s){if(!this.initialized)throw new Error("Persistence manager not initialized");let n=this.getChannelPersistenceOptions(e);if(!n)throw new Error(`Channel ${e} does not have persistence enabled`);return await this.flushChannel(e),n.adapter.getMessages(e,t,s||n.historyLimit)}handleRecordUpdate(e,t,s){if(this.isShuttingDown)return;if(!this.initialized){this.pendingRecordUpdates.push({recordId:e,value:t,version:s}),d.debug(`Buffered record update for ${e} (pending initialization)`);return}let n=this.getRecordPersistenceOptions(e);if(!n)return;let i={recordId:e,value:JSON.stringify(t),version:s,timestamp:Date.now()};if(this.recordBuffer.set(e,i),d.debug(`Added record ${e} to buffer, buffer size: ${this.recordBuffer.size}`),this.recordBuffer.size>=n.maxBufferSize){d.debug(`Buffer size ${this.recordBuffer.size} exceeds limit ${n.maxBufferSize}, flushing records`),this.flushRecords();return}this.recordFlushTimer||(d.debug(`Scheduling record flush in ${n.flushInterval}ms`),this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},n.flushInterval),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}async flushRecords(){if(this.recordBuffer.size===0)return;d.debug(`Flushing ${this.recordBuffer.size} records to storage`),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null);let e=Array.from(this.recordBuffer.values());this.recordBuffer.clear();let t=new Map;for(let s of e){let n=this.getRecordPersistenceOptions(s.recordId);if(!n)continue;let{adapter:i}=n;t.has(i)||t.set(i,[]),t.get(i).push(s)}for(let[s,n]of t.entries())try{s.storeRecords?(d.debug(`Storing ${n.length} records with adapter`),await s.storeRecords(n),this.emit("recordsFlushed",{count:n.length})):d.warn("Adapter does not support storing records")}catch(i){if(d.error("Failed to flush records:",i),!this.isShuttingDown){for(let o of n)this.recordBuffer.set(o.recordId,o);this.recordFlushTimer||(this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},1e3),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}}}async getPersistedRecords(e){if(!this.initialized)throw new Error("Persistence manager not initialized");await this.flushRecords();try{let t=this.defaultAdapter;if(t.getRecords)return await t.getRecords(e)}catch(t){d.error(`Failed to get persisted records for pattern ${e}:`,t)}return[]}async shutdown(){if(this.isShuttingDown)return;this.isShuttingDown=!0,this.messageStream.unsubscribeFromMessages(this.handleStreamMessage.bind(this));for(let t of this.flushTimers.values())clearTimeout(t);this.flushTimers.clear(),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null),await this.flushAll(),await this.flushRecords();let e=new Set([this.defaultAdapter]);for(let{options:t}of this.channelPatterns)e.add(t.adapter);for(let{options:t}of this.recordPatterns)e.add(t.adapter);for(let t of e)try{await t.close()}catch(s){d.error("Error closing persistence adapter:",s)}this.initialized=!1}};var W=class extends ve.WebSocketServer{constructor(e){super(e);this.persistenceManager=null;this.status=f.OFFLINE;this._listening=!1;this.instanceId=(0,be.v4)(),this.serverOptions={...e,pingInterval:e.pingInterval??3e4,latencyInterval:e.latencyInterval??5e3,maxMissedPongs:e.maxMissedPongs??1,logLevel:e.logLevel??H.ERROR},d.configure({level:this.serverOptions.logLevel,styling:!1}),this.redisManager=new X,this.redisManager.initialize(e.redisOptions,t=>this.emit("error",t)),this.instanceManager=new G({redis:this.redisManager.redis,instanceId:this.instanceId}),this.roomManager=new I({redis:this.redisManager.redis}),this.recordManager=new S({redis:this.redisManager.redis,server:this}),this.connectionManager=new P({redis:this.redisManager.pubClient,instanceId:this.instanceId,roomManager:this.roomManager}),this.presenceManager=new E({redis:this.redisManager.redis,roomManager:this.roomManager,redisManager:this.redisManager,enableExpirationEvents:this.serverOptions.enablePresenceExpirationEvents}),this.serverOptions.enablePresenceExpirationEvents&&this.redisManager.enableKeyspaceNotifications().catch(t=>this.emit("error",new Error(`Failed to enable keyspace notifications: ${t}`))),this.commandManager=new B,this.persistenceManager=new $({defaultAdapterOptions:this.serverOptions.persistenceOptions,adapterType:this.serverOptions.persistenceAdapter}),this.persistenceManager.initialize().catch(t=>{this.emit("error",new Error(`Failed to initialize persistence manager: ${t}`))}),this.channelManager=new z({redis:this.redisManager.redis,pubClient:this.redisManager.pubClient,subClient:this.redisManager.subClient}),this.channelManager.setPersistenceManager(this.persistenceManager),this.recordSubscriptionManager=new J({pubClient:this.redisManager.pubClient,recordManager:this.recordManager,emitError:t=>this.emit("error",t),persistenceManager:this.persistenceManager}),this.collectionManager=new V({redis:this.redisManager.redis,emitError:t=>this.emit("error",t)}),this.persistenceManager&&this.persistenceManager.setRecordManager(this.recordManager),this.recordManager.onRecordUpdate(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record update for collection check: ${s}`))}}),this.recordManager.onRecordRemoved(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record removal for collection check: ${s}`))}}),this.pubSubManager=new q({subClient:this.redisManager.subClient,instanceId:this.instanceId,connectionManager:this.connectionManager,recordManager:this.recordManager,recordSubscriptions:this.recordSubscriptionManager.getRecordSubscriptions(),getChannelSubscriptions:this.channelManager.getSubscribers.bind(this.channelManager),emitError:t=>this.emit("error",t),collectionManager:this.collectionManager,pubClient:this.redisManager.pubClient}),this.broadcastManager=new k({connectionManager:this.connectionManager,roomManager:this.roomManager,instanceId:this.instanceId,pubClient:this.redisManager.pubClient,getPubSubChannel:t=>`${M}${t}`,emitError:t=>this.emit("error",t)}),this.on("listening",()=>{this.listening=!0,this.instanceManager.start()}),this.on("error",t=>{d.error(`Error: ${t}`)}),this.on("close",()=>{this.listening=!1}),this.pubSubManager.subscribeToInstanceChannel(),this.registerBuiltinCommands(),this.registerRecordCommands(),this.applyListeners()}get listening(){return this._listening}set listening(e){this._listening=e,this.status=e?f.ONLINE:f.OFFLINE}get port(){return this.address().port}async ready(){let e=this.listening?Promise.resolve():new Promise(s=>this.once("listening",s)),t=this.persistenceManager?this.persistenceManager.ready():Promise.resolve();await Promise.all([e,this.pubSubManager.getSubscriptionPromise(),t])}applyListeners(){this.on("connection",async(e,t)=>{let s=new R(e,t,this.serverOptions,this);s.on("message",n=>{try{let i=n.toString(),o=K(i);o.id!==void 0&&!["latency:response","pong"].includes(o.command)&&this.commandManager.runCommand(o.id,o.command,o.payload,s,this)}catch(i){this.emit("error",i)}});try{await this.connectionManager.registerConnection(s),s.send({command:"mesh/assign-id",payload:s.id})}catch{s.close();return}this.emit("connected",s),s.on("close",async()=>{await this.cleanupConnection(s),this.emit("disconnected",s)}),s.on("error",n=>{this.emit("clientError",n,s)}),s.on("pong",async n=>{try{let i=await this.roomManager.getRoomsForConnection(n);for(let o of i)await this.presenceManager.isRoomTracked(o)&&await this.presenceManager.refreshPresence(n,o)}catch(i){this.emit("error",new Error(`Failed to refresh presence: ${i}`))}})})}exposeCommand(e,t,s=[]){this.commandManager.exposeCommand(e,t,s)}useMiddleware(...e){this.commandManager.useMiddleware(...e)}useMiddlewareWithCommand(e,t){this.commandManager.useMiddlewareWithCommand(e,t)}exposeChannel(e,t){this.channelManager.exposeChannel(e,t)}async writeChannel(e,t,s=0){return this.channelManager.writeChannel(e,t,s,this.instanceId)}enableChannelPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableChannelPersistence(e,t)}enableRecordPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableRecordPersistence(e,t)}exposeRecord(e,t){this.recordSubscriptionManager.exposeRecord(e,t)}exposeWritableRecord(e,t){this.recordSubscriptionManager.exposeWritableRecord(e,t)}async writeRecord(e,t,s){return this.recordSubscriptionManager.writeRecord(e,t,s)}async getRecord(e){return this.recordManager.getRecord(e)}async deleteRecord(e){let t=await this.recordManager.deleteRecord(e);t&&await this.recordSubscriptionManager.publishRecordDeletion(e,t.version)}async listRecordsMatching(e,t){return this.collectionManager.listRecordsMatching(e,t)}exposeCollection(e,t){this.collectionManager.exposeCollection(e,t)}async isInRoom(e,t){let s=typeof t=="string"?t:t.id;return this.roomManager.connectionIsInRoom(e,s)}async addToRoom(e,t){let s=typeof t=="string"?t:t.id;await this.roomManager.addToRoom(e,t),await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOnline(s,e)}async removeFromRoom(e,t){let s=typeof t=="string"?t:t.id;return await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOffline(s,e),this.roomManager.removeFromRoom(e,t)}async removeFromAllRooms(e){return this.roomManager.removeFromAllRooms(e)}async clearRoom(e){return this.roomManager.clearRoom(e)}async deleteRoom(e){return this.roomManager.deleteRoom(e)}async getRoomMembers(e){return this.roomManager.getRoomConnectionIds(e)}async getRoomMembersWithMetadata(e){let t=await this.roomManager.getRoomConnectionIds(e);return Promise.all(t.map(async s=>{try{let n=this.connectionManager.getLocalConnection(s),i;if(n)i=await this.connectionManager.getMetadata(n);else{let o=await this.redisManager.redis.hget("mesh:connections",s);i=o?JSON.parse(o):null}return{id:s,metadata:i}}catch{return{id:s,metadata:null}}}))}async getAllRooms(){return this.roomManager.getAllRooms()}async broadcast(e,t,s){return this.broadcastManager.broadcast(e,t,s)}async broadcastRoom(e,t,s){return this.broadcastManager.broadcastRoom(e,t,s)}async broadcastExclude(e,t,s){return this.broadcastManager.broadcastExclude(e,t,s)}async broadcastRoomExclude(e,t,s,n){return this.broadcastManager.broadcastRoomExclude(e,t,s,n)}trackPresence(e,t){this.presenceManager.trackRoom(e,t)}registerBuiltinCommands(){this.exposeCommand("mesh/noop",async()=>!0),this.exposeCommand("mesh/subscribe-channel",async e=>{let{channel:t,historyLimit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.channelManager.getSubscribers(t)||await this.channelManager.subscribeToRedisChannel(t),this.channelManager.addSubscription(t,e.connection),{success:!0,history:s&&s>0?await this.channelManager.getChannelHistory(t,s,n):[]}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/unsubscribe-channel",async e=>{let{channel:t}=e.payload,s=this.channelManager.removeSubscription(t,e.connection);return s&&!this.channelManager.getSubscribers(t)&&await this.channelManager.unsubscribeFromRedisChannel(t),s}),this.exposeCommand("mesh/get-channel-history",async e=>{let{channel:t,limit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.persistenceManager?.getChannelPersistenceOptions(t)?{success:!0,history:(await this.persistenceManager.getMessages(t,n,s||this.persistenceManager.getChannelPersistenceOptions(t)?.historyLimit)).map(o=>o.message)}:{success:!0,history:await this.channelManager.getChannelHistory(t,s||50,n)}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/join-room",async e=>{let{roomName:t}=e.payload;return await this.addToRoom(t,e.connection),{success:!0,present:await this.getRoomMembersWithMetadata(t)}}),this.exposeCommand("mesh/leave-room",async e=>{let{roomName:t}=e.payload;return await this.removeFromRoom(t,e.connection),{success:!0}}),this.exposeCommand("mesh/get-connection-metadata",async e=>{let{connectionId:t}=e.payload,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/get-my-connection-metadata",async e=>{let t=e.connection.id,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/set-my-connection-metadata",async e=>{let{metadata:t,options:s}=e.payload,n=e.connection.id,i=this.connectionManager.getLocalConnection(n);if(i)try{return await this.connectionManager.setMetadata(i,t,s),{success:!0}}catch{return{success:!1}}else return{success:!1}}),this.exposeCommand("mesh/get-room-metadata",async e=>{let{roomName:t}=e.payload;return{metadata:await this.roomManager.getMetadata(t)}})}registerRecordCommands(){this.exposeCommand("mesh/subscribe-record",async e=>{let{recordId:t,mode:s="full"}=e.payload,n=e.connection.id;if(!await this.recordSubscriptionManager.isRecordExposed(t,e.connection))return{success:!1};try{let{record:i,version:o}=await this.recordManager.getRecordAndVersion(t);return this.recordSubscriptionManager.addSubscription(t,n,s),{success:!0,record:i,version:o}}catch(i){return console.error(`Failed to subscribe to record ${t}:`,i),{success:!1}}}),this.exposeCommand("mesh/unsubscribe-record",async e=>{let{recordId:t}=e.payload,s=e.connection.id;return this.recordSubscriptionManager.removeSubscription(t,s)}),this.exposeCommand("mesh/publish-record-update",async e=>{let{recordId:t,newValue:s,options:n}=e.payload;if(!await this.recordSubscriptionManager.isRecordWritable(t,e.connection))throw new Error(`Record "${t}" is not writable by this connection.`);try{return await this.writeRecord(t,s,n),{success:!0}}catch(i){throw new Error(`Failed to publish update for record "${t}": ${i.message}`)}}),this.exposeCommand("mesh/subscribe-presence",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=`mesh:presence:updates:${t}`;this.channelManager.addSubscription(s,e.connection),(!this.channelManager.getSubscribers(s)||this.channelManager.getSubscribers(s)?.size===1)&&await this.channelManager.subscribeToRedisChannel(s);let n=await this.getRoomMembersWithMetadata(t),i=await this.presenceManager.getAllPresenceStates(t),o={};return i.forEach((c,l)=>{o[l]=c}),{success:!0,present:n,states:o}}catch(s){return console.error(`Failed to subscribe to presence for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/unsubscribe-presence",async e=>{let{roomName:t}=e.payload,s=`mesh:presence:updates:${t}`;return this.channelManager.removeSubscription(s,e.connection)}),this.exposeCommand("mesh/publish-presence-state",async e=>{let{roomName:t,state:s,expireAfter:n,silent:i}=e.payload,o=e.connection.id;if(!s||!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,o))return!1;try{return await this.presenceManager.publishPresenceState(o,t,s,n,i),!0}catch(c){return console.error(`Failed to publish presence state for room ${t}:`,c),!1}}),this.exposeCommand("mesh/clear-presence-state",async e=>{let{roomName:t}=e.payload,s=e.connection.id;if(!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,s))return!1;try{return await this.presenceManager.clearPresenceState(s,t),!0}catch(n){return console.error(`Failed to clear presence state for room ${t}:`,n),!1}}),this.exposeCommand("mesh/get-presence-state",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=await this.presenceManager.getPresentConnections(t),n=await this.presenceManager.getAllPresenceStates(t),i={};return n.forEach((o,c)=>{i[c]=o}),{success:!0,present:s,states:i}}catch(s){return console.error(`Failed to get presence state for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/subscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;if(!await this.collectionManager.isCollectionExposed(t,e.connection))return{success:!1,ids:[],records:[],version:0};try{let{ids:n,records:i,version:o}=await this.collectionManager.addSubscription(t,s,e.connection),c=i.map(l=>({id:l.id,record:l}));return{success:!0,ids:n,records:c,version:o}}catch(n){return console.error(`Failed to subscribe to collection ${t}:`,n),{success:!1,ids:[],records:[],version:0}}}),this.exposeCommand("mesh/unsubscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;return this.collectionManager.removeSubscription(t,s)})}async cleanupConnection(e){d.info("Cleaning up connection:",e.id),e.stopIntervals();try{await this.presenceManager.cleanupConnection(e),await this.connectionManager.cleanupConnection(e),await this.roomManager.cleanupConnection(e),this.recordSubscriptionManager.cleanupConnection(e),this.channelManager.cleanupConnection(e),await this.collectionManager.cleanupConnection(e)}catch(t){this.emit("error",new Error(`Failed to clean up connection: ${t}`))}}async close(e){this.redisManager.isShuttingDown=!0;let t=Object.values(this.connectionManager.getLocalConnections());if(await Promise.all(t.map(async s=>{s.isDead||await s.close(),await this.cleanupConnection(s)})),await new Promise((s,n)=>{super.close(i=>{i?n(i):s()})}),this.persistenceManager)try{await this.persistenceManager.shutdown()}catch(s){d.error("Error shutting down persistence manager:",s)}await this.instanceManager.stop(),await this.pubSubManager.cleanup(),await this.presenceManager.cleanup(),this.redisManager.disconnect(),this.listening=!1,this.removeAllListeners(),e&&e()}onConnection(e){return this.on("connected",e),this}onDisconnection(e){return this.on("disconnected",e),this}onRecordUpdate(e){return this.recordManager.onRecordUpdate(e)}onRecordRemoved(e){return this.recordManager.onRecordRemoved(e)}};0&&(module.exports={Connection,ConnectionManager,MeshContext,MeshServer,MessageStream,PersistenceManager,PostgreSQLPersistenceAdapter,PresenceManager,RecordManager,RoomManager,SQLitePersistenceAdapter});
|
|
56
|
+
ORDER BY timestamp DESC`,[e]);return d.debug(`PostgreSQL: Found ${t.rows.length} records matching pattern ${r}`),t.rows.map(s=>({recordId:s.record_id,version:s.version,value:s.value,timestamp:parseInt(s.timestamp)}))}async close(){this.pool&&(await this.pool.end(),this.pool=null,this.initialized=!1)}};var $=class extends fe.EventEmitter{constructor(e){super();this.channelPatterns=[];this.recordPatterns=[];this.messageBuffer=new Map;this.recordBuffer=new Map;this.flushTimers=new Map;this.recordFlushTimer=null;this.isShuttingDown=!1;this.initialized=!1;this.recordManager=null;this.pendingRecordUpdates=[];let{defaultAdapterOptions:t={},adapterType:s="sqlite"}=e;s==="postgres"?this.defaultAdapter=new x(t):this.defaultAdapter=new N(t),this.messageStream=y.getInstance()}setRecordManager(e){this.recordManager=e}async ready(){return this.initialized?Promise.resolve():new Promise(e=>{this.once("initialized",e)})}async processPendingRecordUpdates(){if(this.pendingRecordUpdates.length===0)return;d.info(`Processing ${this.pendingRecordUpdates.length} pending record updates`);let e=[...this.pendingRecordUpdates];this.pendingRecordUpdates=[];for(let{recordId:t,value:s,version:n}of e)this.handleRecordUpdate(t,s,n)}async initialize(){if(!this.initialized)try{await this.defaultAdapter.initialize(),this.messageStream.subscribeToMessages(this.handleStreamMessage.bind(this)),this.initialized=!0,await this.processPendingRecordUpdates(),this.emit("initialized")}catch(e){throw d.error("Failed to initialize persistence manager:",e),e}}async restorePersistedRecords(){if(!this.recordManager){d.warn("Cannot restore persisted records: record manager not available");return}let e=this.recordManager.getRedis();if(!e){d.warn("Cannot restore records: Redis not available");return}try{d.info("Restoring persisted records...");let t=this.recordPatterns.map(s=>typeof s.pattern=="string"?s.pattern:s.pattern.source);if(t.length===0){d.info("No record patterns to restore");return}for(let s of t)try{let n=O(s),i=await this.defaultAdapter.getRecords?.(n)||[];if(i.length>0){d.info(`Restoring ${i.length} records for pattern ${s}`);for(let o of i)try{let{recordId:c,value:l,version:p}=o,h=typeof l=="string"?JSON.parse(l):l,u=this.recordManager.recordKey(c),b=this.recordManager.recordVersionKey(c),A=e.pipeline();A.set(u,JSON.stringify(h)),A.set(b,p.toString()),await A.exec(),d.debug(`Restored record ${c} (version ${p})`)}catch(c){d.error(`Failed to parse record value for recordId: ${o.recordId}: ${c}`)}}else d.debug(`No records found for pattern ${s}`)}catch(n){d.error(`Error restoring records for pattern ${s}: ${n}`)}d.info("Finished restoring persisted records")}catch(t){d.error("Failed to restore persisted records:",t)}}handleStreamMessage(e){let{channel:t,message:s,instanceId:n,timestamp:i}=e;this.handleChannelMessage(t,s,n,i)}enableChannelPersistence(e,t={}){let s={historyLimit:t.historyLimit??50,filter:t.filter??(()=>!0),adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for pattern ${e}:`,n)}),this.channelPatterns.push({pattern:e,options:s})}enableRecordPersistence(e,t={}){let s={adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for record pattern ${e}:`,n)}),this.recordPatterns.push({pattern:e,options:s})}getChannelPersistenceOptions(e){for(let{pattern:t,options:s}of this.channelPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}getRecordPersistenceOptions(e){for(let{pattern:t,options:s}of this.recordPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}handleChannelMessage(e,t,s,n){if(!this.initialized||this.isShuttingDown)return;let i=this.getChannelPersistenceOptions(e);if(!i||!i.filter(t,e))return;let o={id:(0,ye.v4)(),channel:e,message:t,instanceId:s,timestamp:n||Date.now()};if(this.messageBuffer.has(e)||this.messageBuffer.set(e,[]),this.messageBuffer.get(e).push(o),this.messageBuffer.get(e).length>=i.maxBufferSize){this.flushChannel(e);return}if(!this.flushTimers.has(e)){let c=setTimeout(()=>{this.flushChannel(e)},i.flushInterval);c.unref&&c.unref(),this.flushTimers.set(e,c)}}async flushChannel(e){if(!this.messageBuffer.has(e))return;this.flushTimers.has(e)&&(clearTimeout(this.flushTimers.get(e)),this.flushTimers.delete(e));let t=this.messageBuffer.get(e);if(t.length===0)return;this.messageBuffer.set(e,[]);let s=this.getChannelPersistenceOptions(e);if(s)try{await s.adapter.storeMessages(t),this.emit("flushed",{channel:e,count:t.length}),d.debug(`Flushed ${t.length} messages for channel ${e}`)}catch(n){if(d.error(`Failed to flush messages for channel ${e}:`,n),!this.isShuttingDown){let i=this.messageBuffer.get(e)||[];if(this.messageBuffer.set(e,[...t,...i]),!this.flushTimers.has(e)){let o=setTimeout(()=>{this.flushChannel(e)},1e3);o.unref&&o.unref(),this.flushTimers.set(e,o)}}}}async flushAll(){let e=Array.from(this.messageBuffer.keys());for(let t of e)await this.flushChannel(t)}async getMessages(e,t,s){if(!this.initialized)throw new Error("Persistence manager not initialized");let n=this.getChannelPersistenceOptions(e);if(!n)throw new Error(`Channel ${e} does not have persistence enabled`);return await this.flushChannel(e),n.adapter.getMessages(e,t,s||n.historyLimit)}handleRecordUpdate(e,t,s){if(this.isShuttingDown)return;if(!this.initialized){this.pendingRecordUpdates.push({recordId:e,value:t,version:s}),d.debug(`Buffered record update for ${e} (pending initialization)`);return}let n=this.getRecordPersistenceOptions(e);if(!n)return;let i={recordId:e,value:JSON.stringify(t),version:s,timestamp:Date.now()};if(this.recordBuffer.set(e,i),d.debug(`Added record ${e} to buffer, buffer size: ${this.recordBuffer.size}`),this.recordBuffer.size>=n.maxBufferSize){d.debug(`Buffer size ${this.recordBuffer.size} exceeds limit ${n.maxBufferSize}, flushing records`),this.flushRecords();return}this.recordFlushTimer||(d.debug(`Scheduling record flush in ${n.flushInterval}ms`),this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},n.flushInterval),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}async flushRecords(){if(this.recordBuffer.size===0)return;d.debug(`Flushing ${this.recordBuffer.size} records to storage`),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null);let e=Array.from(this.recordBuffer.values());this.recordBuffer.clear();let t=new Map;for(let s of e){let n=this.getRecordPersistenceOptions(s.recordId);if(!n)continue;let{adapter:i}=n;t.has(i)||t.set(i,[]),t.get(i).push(s)}for(let[s,n]of t.entries())try{s.storeRecords?(d.debug(`Storing ${n.length} records with adapter`),await s.storeRecords(n),this.emit("recordsFlushed",{count:n.length})):d.warn("Adapter does not support storing records")}catch(i){if(d.error("Failed to flush records:",i),!this.isShuttingDown){for(let o of n)this.recordBuffer.set(o.recordId,o);this.recordFlushTimer||(this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},1e3),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}}}async getPersistedRecords(e){if(!this.initialized)throw new Error("Persistence manager not initialized");await this.flushRecords();try{let t=this.defaultAdapter;if(t.getRecords)return await t.getRecords(e)}catch(t){d.error(`Failed to get persisted records for pattern ${e}:`,t)}return[]}async shutdown(){if(this.isShuttingDown)return;this.isShuttingDown=!0,this.messageStream.unsubscribeFromMessages(this.handleStreamMessage.bind(this));for(let t of this.flushTimers.values())clearTimeout(t);this.flushTimers.clear(),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null),await this.flushAll(),await this.flushRecords();let e=new Set([this.defaultAdapter]);for(let{options:t}of this.channelPatterns)e.add(t.adapter);for(let{options:t}of this.recordPatterns)e.add(t.adapter);for(let t of e)try{await t.close()}catch(s){d.error("Error closing persistence adapter:",s)}this.initialized=!1}};var W=class extends ve.WebSocketServer{constructor(e){super(e);this.persistenceManager=null;this.status=f.OFFLINE;this._listening=!1;this.instanceId=(0,be.v4)(),this.serverOptions={...e,pingInterval:e.pingInterval??3e4,latencyInterval:e.latencyInterval??5e3,maxMissedPongs:e.maxMissedPongs??1,logLevel:e.logLevel??H.ERROR},d.configure({level:this.serverOptions.logLevel,styling:!1}),this.redisManager=new X,this.redisManager.initialize(e.redisOptions,t=>this.emit("error",t)),this.instanceManager=new G({redis:this.redisManager.redis,instanceId:this.instanceId}),this.roomManager=new I({redis:this.redisManager.redis}),this.recordManager=new S({redis:this.redisManager.redis,server:this}),this.connectionManager=new P({redis:this.redisManager.pubClient,instanceId:this.instanceId,roomManager:this.roomManager}),this.presenceManager=new E({redis:this.redisManager.redis,roomManager:this.roomManager,redisManager:this.redisManager,enableExpirationEvents:this.serverOptions.enablePresenceExpirationEvents}),this.serverOptions.enablePresenceExpirationEvents&&this.redisManager.enableKeyspaceNotifications().catch(t=>this.emit("error",new Error(`Failed to enable keyspace notifications: ${t}`))),this.commandManager=new B,this.persistenceManager=new $({defaultAdapterOptions:this.serverOptions.persistenceOptions,adapterType:this.serverOptions.persistenceAdapter}),this.persistenceManager.initialize().catch(t=>{this.emit("error",new Error(`Failed to initialize persistence manager: ${t}`))}),this.channelManager=new z({redis:this.redisManager.redis,pubClient:this.redisManager.pubClient,subClient:this.redisManager.subClient}),this.channelManager.setPersistenceManager(this.persistenceManager),this.recordSubscriptionManager=new J({pubClient:this.redisManager.pubClient,recordManager:this.recordManager,emitError:t=>this.emit("error",t),persistenceManager:this.persistenceManager}),this.collectionManager=new V({redis:this.redisManager.redis,emitError:t=>this.emit("error",t)}),this.persistenceManager&&this.persistenceManager.setRecordManager(this.recordManager),this.recordManager.onRecordUpdate(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record update for collection check: ${s}`))}}),this.recordManager.onRecordRemoved(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record removal for collection check: ${s}`))}}),this.pubSubManager=new q({subClient:this.redisManager.subClient,instanceId:this.instanceId,connectionManager:this.connectionManager,recordManager:this.recordManager,recordSubscriptions:this.recordSubscriptionManager.getRecordSubscriptions(),getChannelSubscriptions:this.channelManager.getSubscribers.bind(this.channelManager),emitError:t=>this.emit("error",t),collectionManager:this.collectionManager,pubClient:this.redisManager.pubClient}),this.broadcastManager=new k({connectionManager:this.connectionManager,roomManager:this.roomManager,instanceId:this.instanceId,pubClient:this.redisManager.pubClient,getPubSubChannel:t=>`${M}${t}`,emitError:t=>this.emit("error",t)}),this.on("listening",()=>{this.listening=!0,this.instanceManager.start()}),this.on("error",t=>{d.error(`Error: ${t}`)}),this.on("close",()=>{this.listening=!1}),this.pubSubManager.subscribeToInstanceChannel(),this.registerBuiltinCommands(),this.registerRecordCommands(),this.applyListeners()}get listening(){return this._listening}set listening(e){this._listening=e,this.status=e?f.ONLINE:f.OFFLINE}get port(){return this.address().port}async ready(){let e=this.listening?Promise.resolve():new Promise(s=>this.once("listening",s)),t=this.persistenceManager?this.persistenceManager.ready():Promise.resolve();await Promise.all([e,this.pubSubManager.getSubscriptionPromise(),t]),this.persistenceManager&&await this.persistenceManager.restorePersistedRecords()}applyListeners(){this.on("connection",async(e,t)=>{let s=new R(e,t,this.serverOptions,this);s.on("message",n=>{try{let i=n.toString(),o=K(i);o.id!==void 0&&!["latency:response","pong"].includes(o.command)&&this.commandManager.runCommand(o.id,o.command,o.payload,s,this)}catch(i){this.emit("error",i)}});try{await this.connectionManager.registerConnection(s),s.send({command:"mesh/assign-id",payload:s.id})}catch{s.close();return}this.emit("connected",s),s.on("close",async()=>{await this.cleanupConnection(s),this.emit("disconnected",s)}),s.on("error",n=>{this.emit("clientError",n,s)}),s.on("pong",async n=>{try{let i=await this.roomManager.getRoomsForConnection(n);for(let o of i)await this.presenceManager.isRoomTracked(o)&&await this.presenceManager.refreshPresence(n,o)}catch(i){this.emit("error",new Error(`Failed to refresh presence: ${i}`))}})})}exposeCommand(e,t,s=[]){this.commandManager.exposeCommand(e,t,s)}useMiddleware(...e){this.commandManager.useMiddleware(...e)}useMiddlewareWithCommand(e,t){this.commandManager.useMiddlewareWithCommand(e,t)}exposeChannel(e,t){this.channelManager.exposeChannel(e,t)}async writeChannel(e,t,s=0){return this.channelManager.writeChannel(e,t,s,this.instanceId)}enableChannelPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableChannelPersistence(e,t)}enableRecordPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableRecordPersistence(e,t)}exposeRecord(e,t){this.recordSubscriptionManager.exposeRecord(e,t)}exposeWritableRecord(e,t){this.recordSubscriptionManager.exposeWritableRecord(e,t)}async writeRecord(e,t,s){return this.recordSubscriptionManager.writeRecord(e,t,s)}async getRecord(e){return this.recordManager.getRecord(e)}async deleteRecord(e){let t=await this.recordManager.deleteRecord(e);t&&await this.recordSubscriptionManager.publishRecordDeletion(e,t.version)}async listRecordsMatching(e,t){return this.collectionManager.listRecordsMatching(e,t)}exposeCollection(e,t){this.collectionManager.exposeCollection(e,t)}async isInRoom(e,t){let s=typeof t=="string"?t:t.id;return this.roomManager.connectionIsInRoom(e,s)}async addToRoom(e,t){let s=typeof t=="string"?t:t.id;await this.roomManager.addToRoom(e,t),await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOnline(s,e)}async removeFromRoom(e,t){let s=typeof t=="string"?t:t.id;return await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOffline(s,e),this.roomManager.removeFromRoom(e,t)}async removeFromAllRooms(e){return this.roomManager.removeFromAllRooms(e)}async clearRoom(e){return this.roomManager.clearRoom(e)}async deleteRoom(e){return this.roomManager.deleteRoom(e)}async getRoomMembers(e){return this.roomManager.getRoomConnectionIds(e)}async getRoomMembersWithMetadata(e){let t=await this.roomManager.getRoomConnectionIds(e);return Promise.all(t.map(async s=>{try{let n=this.connectionManager.getLocalConnection(s),i;if(n)i=await this.connectionManager.getMetadata(n);else{let o=await this.redisManager.redis.hget("mesh:connections",s);i=o?JSON.parse(o):null}return{id:s,metadata:i}}catch{return{id:s,metadata:null}}}))}async getAllRooms(){return this.roomManager.getAllRooms()}async broadcast(e,t,s){return this.broadcastManager.broadcast(e,t,s)}async broadcastRoom(e,t,s){return this.broadcastManager.broadcastRoom(e,t,s)}async broadcastExclude(e,t,s){return this.broadcastManager.broadcastExclude(e,t,s)}async broadcastRoomExclude(e,t,s,n){return this.broadcastManager.broadcastRoomExclude(e,t,s,n)}trackPresence(e,t){this.presenceManager.trackRoom(e,t)}registerBuiltinCommands(){this.exposeCommand("mesh/noop",async()=>!0),this.exposeCommand("mesh/subscribe-channel",async e=>{let{channel:t,historyLimit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.channelManager.getSubscribers(t)||await this.channelManager.subscribeToRedisChannel(t),this.channelManager.addSubscription(t,e.connection),{success:!0,history:s&&s>0?await this.channelManager.getChannelHistory(t,s,n):[]}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/unsubscribe-channel",async e=>{let{channel:t}=e.payload,s=this.channelManager.removeSubscription(t,e.connection);return s&&!this.channelManager.getSubscribers(t)&&await this.channelManager.unsubscribeFromRedisChannel(t),s}),this.exposeCommand("mesh/get-channel-history",async e=>{let{channel:t,limit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.persistenceManager?.getChannelPersistenceOptions(t)?{success:!0,history:(await this.persistenceManager.getMessages(t,n,s||this.persistenceManager.getChannelPersistenceOptions(t)?.historyLimit)).map(o=>o.message)}:{success:!0,history:await this.channelManager.getChannelHistory(t,s||50,n)}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/join-room",async e=>{let{roomName:t}=e.payload;return await this.addToRoom(t,e.connection),{success:!0,present:await this.getRoomMembersWithMetadata(t)}}),this.exposeCommand("mesh/leave-room",async e=>{let{roomName:t}=e.payload;return await this.removeFromRoom(t,e.connection),{success:!0}}),this.exposeCommand("mesh/get-connection-metadata",async e=>{let{connectionId:t}=e.payload,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/get-my-connection-metadata",async e=>{let t=e.connection.id,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/set-my-connection-metadata",async e=>{let{metadata:t,options:s}=e.payload,n=e.connection.id,i=this.connectionManager.getLocalConnection(n);if(i)try{return await this.connectionManager.setMetadata(i,t,s),{success:!0}}catch{return{success:!1}}else return{success:!1}}),this.exposeCommand("mesh/get-room-metadata",async e=>{let{roomName:t}=e.payload;return{metadata:await this.roomManager.getMetadata(t)}})}registerRecordCommands(){this.exposeCommand("mesh/subscribe-record",async e=>{let{recordId:t,mode:s="full"}=e.payload,n=e.connection.id;if(!await this.recordSubscriptionManager.isRecordExposed(t,e.connection))return{success:!1};try{let{record:i,version:o}=await this.recordManager.getRecordAndVersion(t);return this.recordSubscriptionManager.addSubscription(t,n,s),{success:!0,record:i,version:o}}catch(i){return console.error(`Failed to subscribe to record ${t}:`,i),{success:!1}}}),this.exposeCommand("mesh/unsubscribe-record",async e=>{let{recordId:t}=e.payload,s=e.connection.id;return this.recordSubscriptionManager.removeSubscription(t,s)}),this.exposeCommand("mesh/publish-record-update",async e=>{let{recordId:t,newValue:s,options:n}=e.payload;if(!await this.recordSubscriptionManager.isRecordWritable(t,e.connection))throw new Error(`Record "${t}" is not writable by this connection.`);try{return await this.writeRecord(t,s,n),{success:!0}}catch(i){throw new Error(`Failed to publish update for record "${t}": ${i.message}`)}}),this.exposeCommand("mesh/subscribe-presence",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=`mesh:presence:updates:${t}`;this.channelManager.addSubscription(s,e.connection),(!this.channelManager.getSubscribers(s)||this.channelManager.getSubscribers(s)?.size===1)&&await this.channelManager.subscribeToRedisChannel(s);let n=await this.getRoomMembersWithMetadata(t),i=await this.presenceManager.getAllPresenceStates(t),o={};return i.forEach((c,l)=>{o[l]=c}),{success:!0,present:n,states:o}}catch(s){return console.error(`Failed to subscribe to presence for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/unsubscribe-presence",async e=>{let{roomName:t}=e.payload,s=`mesh:presence:updates:${t}`;return this.channelManager.removeSubscription(s,e.connection)}),this.exposeCommand("mesh/publish-presence-state",async e=>{let{roomName:t,state:s,expireAfter:n,silent:i}=e.payload,o=e.connection.id;if(!s||!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,o))return!1;try{return await this.presenceManager.publishPresenceState(o,t,s,n,i),!0}catch(c){return console.error(`Failed to publish presence state for room ${t}:`,c),!1}}),this.exposeCommand("mesh/clear-presence-state",async e=>{let{roomName:t}=e.payload,s=e.connection.id;if(!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,s))return!1;try{return await this.presenceManager.clearPresenceState(s,t),!0}catch(n){return console.error(`Failed to clear presence state for room ${t}:`,n),!1}}),this.exposeCommand("mesh/get-presence-state",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=await this.presenceManager.getPresentConnections(t),n=await this.presenceManager.getAllPresenceStates(t),i={};return n.forEach((o,c)=>{i[c]=o}),{success:!0,present:s,states:i}}catch(s){return console.error(`Failed to get presence state for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/subscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;if(!await this.collectionManager.isCollectionExposed(t,e.connection))return{success:!1,ids:[],records:[],version:0};try{let{ids:n,records:i,version:o}=await this.collectionManager.addSubscription(t,s,e.connection),c=i.map(l=>({id:l.id,record:l}));return{success:!0,ids:n,records:c,version:o}}catch(n){return console.error(`Failed to subscribe to collection ${t}:`,n),{success:!1,ids:[],records:[],version:0}}}),this.exposeCommand("mesh/unsubscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;return this.collectionManager.removeSubscription(t,s)})}async cleanupConnection(e){d.info("Cleaning up connection:",e.id),e.stopIntervals();try{await this.presenceManager.cleanupConnection(e),await this.connectionManager.cleanupConnection(e),await this.roomManager.cleanupConnection(e),this.recordSubscriptionManager.cleanupConnection(e),this.channelManager.cleanupConnection(e),await this.collectionManager.cleanupConnection(e)}catch(t){this.emit("error",new Error(`Failed to clean up connection: ${t}`))}}async close(e){this.redisManager.isShuttingDown=!0;let t=Object.values(this.connectionManager.getLocalConnections());if(await Promise.all(t.map(async s=>{s.isDead||await s.close(),await this.cleanupConnection(s)})),await new Promise((s,n)=>{super.close(i=>{i?n(i):s()})}),this.persistenceManager)try{await this.persistenceManager.shutdown()}catch(s){d.error("Error shutting down persistence manager:",s)}await this.instanceManager.stop(),await this.pubSubManager.cleanup(),await this.presenceManager.cleanup(),this.redisManager.disconnect(),this.listening=!1,this.removeAllListeners(),e&&e()}onConnection(e){return this.on("connected",e),this}onDisconnection(e){return this.on("disconnected",e),this}onRecordUpdate(e){return this.recordManager.onRecordUpdate(e)}onRecordRemoved(e){return this.recordManager.onRecordRemoved(e)}};0&&(module.exports={Connection,ConnectionManager,MeshContext,MeshServer,MessageStream,PersistenceManager,PostgreSQLPersistenceAdapter,PresenceManager,RecordManager,RoomManager,SQLitePersistenceAdapter});
|
package/dist/index.mjs
CHANGED
|
@@ -53,4 +53,4 @@ import{v4 as Pe}from"uuid";import{WebSocketServer as Ee}from"ws";var Z=class ext
|
|
|
53
53
|
DO UPDATE SET version = $2, value = $3, timestamp = $4`,[t.recordId,t.version,t.value,t.timestamp]);await e.query("COMMIT")}catch(t){throw await e.query("ROLLBACK"),d.error("Error in storeRecords:",t),t}finally{e.release()}}async getRecords(r){if(!this.pool)throw new Error("Database not initialized");let e=R(r);d.debug(`PostgreSQL: Getting records matching pattern: ${r} (SQL: ${e})`);let t=await this.pool.query(`SELECT record_id, version, value, timestamp
|
|
54
54
|
FROM records
|
|
55
55
|
WHERE record_id LIKE $1
|
|
56
|
-
ORDER BY timestamp DESC`,[e]);return d.debug(`PostgreSQL: Found ${t.rows.length} records matching pattern ${r}`),t.rows.map(s=>({recordId:s.record_id,version:s.version,value:s.value,timestamp:parseInt(s.timestamp)}))}async close(){this.pool&&(await this.pool.end(),this.pool=null,this.initialized=!1)}};var L=class extends we{constructor(e){super();this.channelPatterns=[];this.recordPatterns=[];this.messageBuffer=new Map;this.recordBuffer=new Map;this.flushTimers=new Map;this.recordFlushTimer=null;this.isShuttingDown=!1;this.initialized=!1;this.recordManager=null;this.pendingRecordUpdates=[];let{defaultAdapterOptions:t={},adapterType:s="sqlite"}=e;s==="postgres"?this.defaultAdapter=new A(t):this.defaultAdapter=new $(t),this.messageStream=w.getInstance()}setRecordManager(e){this.recordManager=e}async ready(){return this.initialized?Promise.resolve():new Promise(e=>{this.once("initialized",e)})}async processPendingRecordUpdates(){if(this.pendingRecordUpdates.length===0)return;d.info(`Processing ${this.pendingRecordUpdates.length} pending record updates`);let e=[...this.pendingRecordUpdates];this.pendingRecordUpdates=[];for(let{recordId:t,value:s,version:n}of e)this.handleRecordUpdate(t,s,n)}async initialize(){if(!this.initialized)try{await this.defaultAdapter.initialize(),this.messageStream.subscribeToMessages(this.handleStreamMessage.bind(this)),await this.restorePersistedRecords(),this.initialized=!0,await this.processPendingRecordUpdates(),this.emit("initialized")}catch(e){throw d.error("Failed to initialize persistence manager:",e),e}}async restorePersistedRecords(){if(!this.recordManager){d.warn("Cannot restore persisted records: record manager not available");return}let e=this.recordManager.getRedis();if(!e){d.warn("Cannot restore records: Redis not available");return}try{d.info("Restoring persisted records...");let t=this.recordPatterns.map(s=>typeof s.pattern=="string"?s.pattern:s.pattern.source);if(t.length===0){d.info("No record patterns to restore");return}for(let s of t)try{let n=R(s),i=await this.defaultAdapter.getRecords?.(n)||[];if(i.length>0){d.info(`Restoring ${i.length} records for pattern ${s}`);for(let o of i)try{let{recordId:a,value:l,version:p}=o,h=typeof l=="string"?JSON.parse(l):l,u=this.recordManager.recordKey(a),y=this.recordManager.recordVersionKey(a),P=e.pipeline();P.set(u,JSON.stringify(h)),P.set(y,p.toString()),await P.exec(),d.debug(`Restored record ${a} (version ${p})`)}catch(a){d.error(`Failed to parse record value for recordId: ${o.recordId}: ${a}`)}}else d.debug(`No records found for pattern ${s}`)}catch(n){d.error(`Error restoring records for pattern ${s}: ${n}`)}d.info("Finished restoring persisted records")}catch(t){d.error("Failed to restore persisted records:",t)}}handleStreamMessage(e){let{channel:t,message:s,instanceId:n,timestamp:i}=e;this.handleChannelMessage(t,s,n,i)}enableChannelPersistence(e,t={}){let s={historyLimit:t.historyLimit??50,filter:t.filter??(()=>!0),adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for pattern ${e}:`,n)}),this.channelPatterns.push({pattern:e,options:s})}enableRecordPersistence(e,t={}){let s={adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for record pattern ${e}:`,n)}),this.recordPatterns.push({pattern:e,options:s})}getChannelPersistenceOptions(e){for(let{pattern:t,options:s}of this.channelPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}getRecordPersistenceOptions(e){for(let{pattern:t,options:s}of this.recordPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}handleChannelMessage(e,t,s,n){if(!this.initialized||this.isShuttingDown)return;let i=this.getChannelPersistenceOptions(e);if(!i||!i.filter(t,e))return;let o={id:Re(),channel:e,message:t,instanceId:s,timestamp:n||Date.now()};if(this.messageBuffer.has(e)||this.messageBuffer.set(e,[]),this.messageBuffer.get(e).push(o),this.messageBuffer.get(e).length>=i.maxBufferSize){this.flushChannel(e);return}if(!this.flushTimers.has(e)){let a=setTimeout(()=>{this.flushChannel(e)},i.flushInterval);a.unref&&a.unref(),this.flushTimers.set(e,a)}}async flushChannel(e){if(!this.messageBuffer.has(e))return;this.flushTimers.has(e)&&(clearTimeout(this.flushTimers.get(e)),this.flushTimers.delete(e));let t=this.messageBuffer.get(e);if(t.length===0)return;this.messageBuffer.set(e,[]);let s=this.getChannelPersistenceOptions(e);if(s)try{await s.adapter.storeMessages(t),this.emit("flushed",{channel:e,count:t.length}),d.debug(`Flushed ${t.length} messages for channel ${e}`)}catch(n){if(d.error(`Failed to flush messages for channel ${e}:`,n),!this.isShuttingDown){let i=this.messageBuffer.get(e)||[];if(this.messageBuffer.set(e,[...t,...i]),!this.flushTimers.has(e)){let o=setTimeout(()=>{this.flushChannel(e)},1e3);o.unref&&o.unref(),this.flushTimers.set(e,o)}}}}async flushAll(){let e=Array.from(this.messageBuffer.keys());for(let t of e)await this.flushChannel(t)}async getMessages(e,t,s){if(!this.initialized)throw new Error("Persistence manager not initialized");let n=this.getChannelPersistenceOptions(e);if(!n)throw new Error(`Channel ${e} does not have persistence enabled`);return await this.flushChannel(e),n.adapter.getMessages(e,t,s||n.historyLimit)}handleRecordUpdate(e,t,s){if(this.isShuttingDown)return;if(!this.initialized){this.pendingRecordUpdates.push({recordId:e,value:t,version:s}),d.debug(`Buffered record update for ${e} (pending initialization)`);return}let n=this.getRecordPersistenceOptions(e);if(!n)return;let i={recordId:e,value:JSON.stringify(t),version:s,timestamp:Date.now()};if(this.recordBuffer.set(e,i),d.debug(`Added record ${e} to buffer, buffer size: ${this.recordBuffer.size}`),this.recordBuffer.size>=n.maxBufferSize){d.debug(`Buffer size ${this.recordBuffer.size} exceeds limit ${n.maxBufferSize}, flushing records`),this.flushRecords();return}this.recordFlushTimer||(d.debug(`Scheduling record flush in ${n.flushInterval}ms`),this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},n.flushInterval),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}async flushRecords(){if(this.recordBuffer.size===0)return;d.debug(`Flushing ${this.recordBuffer.size} records to storage`),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null);let e=Array.from(this.recordBuffer.values());this.recordBuffer.clear();let t=new Map;for(let s of e){let n=this.getRecordPersistenceOptions(s.recordId);if(!n)continue;let{adapter:i}=n;t.has(i)||t.set(i,[]),t.get(i).push(s)}for(let[s,n]of t.entries())try{s.storeRecords?(d.debug(`Storing ${n.length} records with adapter`),await s.storeRecords(n),this.emit("recordsFlushed",{count:n.length})):d.warn("Adapter does not support storing records")}catch(i){if(d.error("Failed to flush records:",i),!this.isShuttingDown){for(let o of n)this.recordBuffer.set(o.recordId,o);this.recordFlushTimer||(this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},1e3),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}}}async getPersistedRecords(e){if(!this.initialized)throw new Error("Persistence manager not initialized");await this.flushRecords();try{let t=this.defaultAdapter;if(t.getRecords)return await t.getRecords(e)}catch(t){d.error(`Failed to get persisted records for pattern ${e}:`,t)}return[]}async shutdown(){if(this.isShuttingDown)return;this.isShuttingDown=!0,this.messageStream.unsubscribeFromMessages(this.handleStreamMessage.bind(this));for(let t of this.flushTimers.values())clearTimeout(t);this.flushTimers.clear(),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null),await this.flushAll(),await this.flushRecords();let e=new Set([this.defaultAdapter]);for(let{options:t}of this.channelPatterns)e.add(t.adapter);for(let{options:t}of this.recordPatterns)e.add(t.adapter);for(let t of e)try{await t.close()}catch(s){d.error("Error closing persistence adapter:",s)}this.initialized=!1}};var H=class extends Ee{constructor(e){super(e);this.persistenceManager=null;this.status=f.OFFLINE;this._listening=!1;this.instanceId=Pe(),this.serverOptions={...e,pingInterval:e.pingInterval??3e4,latencyInterval:e.latencyInterval??5e3,maxMissedPongs:e.maxMissedPongs??1,logLevel:e.logLevel??V.ERROR},d.configure({level:this.serverOptions.logLevel,styling:!1}),this.redisManager=new J,this.redisManager.initialize(e.redisOptions,t=>this.emit("error",t)),this.instanceManager=new X({redis:this.redisManager.redis,instanceId:this.instanceId}),this.roomManager=new N({redis:this.redisManager.redis}),this.recordManager=new O({redis:this.redisManager.redis,server:this}),this.connectionManager=new I({redis:this.redisManager.pubClient,instanceId:this.instanceId,roomManager:this.roomManager}),this.presenceManager=new T({redis:this.redisManager.redis,roomManager:this.roomManager,redisManager:this.redisManager,enableExpirationEvents:this.serverOptions.enablePresenceExpirationEvents}),this.serverOptions.enablePresenceExpirationEvents&&this.redisManager.enableKeyspaceNotifications().catch(t=>this.emit("error",new Error(`Failed to enable keyspace notifications: ${t}`))),this.commandManager=new z,this.persistenceManager=new L({defaultAdapterOptions:this.serverOptions.persistenceOptions,adapterType:this.serverOptions.persistenceAdapter}),this.persistenceManager.initialize().catch(t=>{this.emit("error",new Error(`Failed to initialize persistence manager: ${t}`))}),this.channelManager=new k({redis:this.redisManager.redis,pubClient:this.redisManager.pubClient,subClient:this.redisManager.subClient}),this.channelManager.setPersistenceManager(this.persistenceManager),this.recordSubscriptionManager=new q({pubClient:this.redisManager.pubClient,recordManager:this.recordManager,emitError:t=>this.emit("error",t),persistenceManager:this.persistenceManager}),this.collectionManager=new G({redis:this.redisManager.redis,emitError:t=>this.emit("error",t)}),this.persistenceManager&&this.persistenceManager.setRecordManager(this.recordManager),this.recordManager.onRecordUpdate(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record update for collection check: ${s}`))}}),this.recordManager.onRecordRemoved(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record removal for collection check: ${s}`))}}),this.pubSubManager=new B({subClient:this.redisManager.subClient,instanceId:this.instanceId,connectionManager:this.connectionManager,recordManager:this.recordManager,recordSubscriptions:this.recordSubscriptionManager.getRecordSubscriptions(),getChannelSubscriptions:this.channelManager.getSubscribers.bind(this.channelManager),emitError:t=>this.emit("error",t),collectionManager:this.collectionManager,pubClient:this.redisManager.pubClient}),this.broadcastManager=new U({connectionManager:this.connectionManager,roomManager:this.roomManager,instanceId:this.instanceId,pubClient:this.redisManager.pubClient,getPubSubChannel:t=>`${v}${t}`,emitError:t=>this.emit("error",t)}),this.on("listening",()=>{this.listening=!0,this.instanceManager.start()}),this.on("error",t=>{d.error(`Error: ${t}`)}),this.on("close",()=>{this.listening=!1}),this.pubSubManager.subscribeToInstanceChannel(),this.registerBuiltinCommands(),this.registerRecordCommands(),this.applyListeners()}get listening(){return this._listening}set listening(e){this._listening=e,this.status=e?f.ONLINE:f.OFFLINE}get port(){return this.address().port}async ready(){let e=this.listening?Promise.resolve():new Promise(s=>this.once("listening",s)),t=this.persistenceManager?this.persistenceManager.ready():Promise.resolve();await Promise.all([e,this.pubSubManager.getSubscriptionPromise(),t])}applyListeners(){this.on("connection",async(e,t)=>{let s=new S(e,t,this.serverOptions,this);s.on("message",n=>{try{let i=n.toString(),o=F(i);o.id!==void 0&&!["latency:response","pong"].includes(o.command)&&this.commandManager.runCommand(o.id,o.command,o.payload,s,this)}catch(i){this.emit("error",i)}});try{await this.connectionManager.registerConnection(s),s.send({command:"mesh/assign-id",payload:s.id})}catch{s.close();return}this.emit("connected",s),s.on("close",async()=>{await this.cleanupConnection(s),this.emit("disconnected",s)}),s.on("error",n=>{this.emit("clientError",n,s)}),s.on("pong",async n=>{try{let i=await this.roomManager.getRoomsForConnection(n);for(let o of i)await this.presenceManager.isRoomTracked(o)&&await this.presenceManager.refreshPresence(n,o)}catch(i){this.emit("error",new Error(`Failed to refresh presence: ${i}`))}})})}exposeCommand(e,t,s=[]){this.commandManager.exposeCommand(e,t,s)}useMiddleware(...e){this.commandManager.useMiddleware(...e)}useMiddlewareWithCommand(e,t){this.commandManager.useMiddlewareWithCommand(e,t)}exposeChannel(e,t){this.channelManager.exposeChannel(e,t)}async writeChannel(e,t,s=0){return this.channelManager.writeChannel(e,t,s,this.instanceId)}enableChannelPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableChannelPersistence(e,t)}enableRecordPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableRecordPersistence(e,t)}exposeRecord(e,t){this.recordSubscriptionManager.exposeRecord(e,t)}exposeWritableRecord(e,t){this.recordSubscriptionManager.exposeWritableRecord(e,t)}async writeRecord(e,t,s){return this.recordSubscriptionManager.writeRecord(e,t,s)}async getRecord(e){return this.recordManager.getRecord(e)}async deleteRecord(e){let t=await this.recordManager.deleteRecord(e);t&&await this.recordSubscriptionManager.publishRecordDeletion(e,t.version)}async listRecordsMatching(e,t){return this.collectionManager.listRecordsMatching(e,t)}exposeCollection(e,t){this.collectionManager.exposeCollection(e,t)}async isInRoom(e,t){let s=typeof t=="string"?t:t.id;return this.roomManager.connectionIsInRoom(e,s)}async addToRoom(e,t){let s=typeof t=="string"?t:t.id;await this.roomManager.addToRoom(e,t),await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOnline(s,e)}async removeFromRoom(e,t){let s=typeof t=="string"?t:t.id;return await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOffline(s,e),this.roomManager.removeFromRoom(e,t)}async removeFromAllRooms(e){return this.roomManager.removeFromAllRooms(e)}async clearRoom(e){return this.roomManager.clearRoom(e)}async deleteRoom(e){return this.roomManager.deleteRoom(e)}async getRoomMembers(e){return this.roomManager.getRoomConnectionIds(e)}async getRoomMembersWithMetadata(e){let t=await this.roomManager.getRoomConnectionIds(e);return Promise.all(t.map(async s=>{try{let n=this.connectionManager.getLocalConnection(s),i;if(n)i=await this.connectionManager.getMetadata(n);else{let o=await this.redisManager.redis.hget("mesh:connections",s);i=o?JSON.parse(o):null}return{id:s,metadata:i}}catch{return{id:s,metadata:null}}}))}async getAllRooms(){return this.roomManager.getAllRooms()}async broadcast(e,t,s){return this.broadcastManager.broadcast(e,t,s)}async broadcastRoom(e,t,s){return this.broadcastManager.broadcastRoom(e,t,s)}async broadcastExclude(e,t,s){return this.broadcastManager.broadcastExclude(e,t,s)}async broadcastRoomExclude(e,t,s,n){return this.broadcastManager.broadcastRoomExclude(e,t,s,n)}trackPresence(e,t){this.presenceManager.trackRoom(e,t)}registerBuiltinCommands(){this.exposeCommand("mesh/noop",async()=>!0),this.exposeCommand("mesh/subscribe-channel",async e=>{let{channel:t,historyLimit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.channelManager.getSubscribers(t)||await this.channelManager.subscribeToRedisChannel(t),this.channelManager.addSubscription(t,e.connection),{success:!0,history:s&&s>0?await this.channelManager.getChannelHistory(t,s,n):[]}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/unsubscribe-channel",async e=>{let{channel:t}=e.payload,s=this.channelManager.removeSubscription(t,e.connection);return s&&!this.channelManager.getSubscribers(t)&&await this.channelManager.unsubscribeFromRedisChannel(t),s}),this.exposeCommand("mesh/get-channel-history",async e=>{let{channel:t,limit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.persistenceManager?.getChannelPersistenceOptions(t)?{success:!0,history:(await this.persistenceManager.getMessages(t,n,s||this.persistenceManager.getChannelPersistenceOptions(t)?.historyLimit)).map(o=>o.message)}:{success:!0,history:await this.channelManager.getChannelHistory(t,s||50,n)}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/join-room",async e=>{let{roomName:t}=e.payload;return await this.addToRoom(t,e.connection),{success:!0,present:await this.getRoomMembersWithMetadata(t)}}),this.exposeCommand("mesh/leave-room",async e=>{let{roomName:t}=e.payload;return await this.removeFromRoom(t,e.connection),{success:!0}}),this.exposeCommand("mesh/get-connection-metadata",async e=>{let{connectionId:t}=e.payload,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/get-my-connection-metadata",async e=>{let t=e.connection.id,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/set-my-connection-metadata",async e=>{let{metadata:t,options:s}=e.payload,n=e.connection.id,i=this.connectionManager.getLocalConnection(n);if(i)try{return await this.connectionManager.setMetadata(i,t,s),{success:!0}}catch{return{success:!1}}else return{success:!1}}),this.exposeCommand("mesh/get-room-metadata",async e=>{let{roomName:t}=e.payload;return{metadata:await this.roomManager.getMetadata(t)}})}registerRecordCommands(){this.exposeCommand("mesh/subscribe-record",async e=>{let{recordId:t,mode:s="full"}=e.payload,n=e.connection.id;if(!await this.recordSubscriptionManager.isRecordExposed(t,e.connection))return{success:!1};try{let{record:i,version:o}=await this.recordManager.getRecordAndVersion(t);return this.recordSubscriptionManager.addSubscription(t,n,s),{success:!0,record:i,version:o}}catch(i){return console.error(`Failed to subscribe to record ${t}:`,i),{success:!1}}}),this.exposeCommand("mesh/unsubscribe-record",async e=>{let{recordId:t}=e.payload,s=e.connection.id;return this.recordSubscriptionManager.removeSubscription(t,s)}),this.exposeCommand("mesh/publish-record-update",async e=>{let{recordId:t,newValue:s,options:n}=e.payload;if(!await this.recordSubscriptionManager.isRecordWritable(t,e.connection))throw new Error(`Record "${t}" is not writable by this connection.`);try{return await this.writeRecord(t,s,n),{success:!0}}catch(i){throw new Error(`Failed to publish update for record "${t}": ${i.message}`)}}),this.exposeCommand("mesh/subscribe-presence",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=`mesh:presence:updates:${t}`;this.channelManager.addSubscription(s,e.connection),(!this.channelManager.getSubscribers(s)||this.channelManager.getSubscribers(s)?.size===1)&&await this.channelManager.subscribeToRedisChannel(s);let n=await this.getRoomMembersWithMetadata(t),i=await this.presenceManager.getAllPresenceStates(t),o={};return i.forEach((a,l)=>{o[l]=a}),{success:!0,present:n,states:o}}catch(s){return console.error(`Failed to subscribe to presence for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/unsubscribe-presence",async e=>{let{roomName:t}=e.payload,s=`mesh:presence:updates:${t}`;return this.channelManager.removeSubscription(s,e.connection)}),this.exposeCommand("mesh/publish-presence-state",async e=>{let{roomName:t,state:s,expireAfter:n,silent:i}=e.payload,o=e.connection.id;if(!s||!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,o))return!1;try{return await this.presenceManager.publishPresenceState(o,t,s,n,i),!0}catch(a){return console.error(`Failed to publish presence state for room ${t}:`,a),!1}}),this.exposeCommand("mesh/clear-presence-state",async e=>{let{roomName:t}=e.payload,s=e.connection.id;if(!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,s))return!1;try{return await this.presenceManager.clearPresenceState(s,t),!0}catch(n){return console.error(`Failed to clear presence state for room ${t}:`,n),!1}}),this.exposeCommand("mesh/get-presence-state",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=await this.presenceManager.getPresentConnections(t),n=await this.presenceManager.getAllPresenceStates(t),i={};return n.forEach((o,a)=>{i[a]=o}),{success:!0,present:s,states:i}}catch(s){return console.error(`Failed to get presence state for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/subscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;if(!await this.collectionManager.isCollectionExposed(t,e.connection))return{success:!1,ids:[],records:[],version:0};try{let{ids:n,records:i,version:o}=await this.collectionManager.addSubscription(t,s,e.connection),a=i.map(l=>({id:l.id,record:l}));return{success:!0,ids:n,records:a,version:o}}catch(n){return console.error(`Failed to subscribe to collection ${t}:`,n),{success:!1,ids:[],records:[],version:0}}}),this.exposeCommand("mesh/unsubscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;return this.collectionManager.removeSubscription(t,s)})}async cleanupConnection(e){d.info("Cleaning up connection:",e.id),e.stopIntervals();try{await this.presenceManager.cleanupConnection(e),await this.connectionManager.cleanupConnection(e),await this.roomManager.cleanupConnection(e),this.recordSubscriptionManager.cleanupConnection(e),this.channelManager.cleanupConnection(e),await this.collectionManager.cleanupConnection(e)}catch(t){this.emit("error",new Error(`Failed to clean up connection: ${t}`))}}async close(e){this.redisManager.isShuttingDown=!0;let t=Object.values(this.connectionManager.getLocalConnections());if(await Promise.all(t.map(async s=>{s.isDead||await s.close(),await this.cleanupConnection(s)})),await new Promise((s,n)=>{super.close(i=>{i?n(i):s()})}),this.persistenceManager)try{await this.persistenceManager.shutdown()}catch(s){d.error("Error shutting down persistence manager:",s)}await this.instanceManager.stop(),await this.pubSubManager.cleanup(),await this.presenceManager.cleanup(),this.redisManager.disconnect(),this.listening=!1,this.removeAllListeners(),e&&e()}onConnection(e){return this.on("connected",e),this}onDisconnection(e){return this.on("disconnected",e),this}onRecordUpdate(e){return this.recordManager.onRecordUpdate(e)}onRecordRemoved(e){return this.recordManager.onRecordRemoved(e)}};export{S as Connection,I as ConnectionManager,x as MeshContext,H as MeshServer,w as MessageStream,L as PersistenceManager,A as PostgreSQLPersistenceAdapter,T as PresenceManager,O as RecordManager,N as RoomManager,$ as SQLitePersistenceAdapter};
|
|
56
|
+
ORDER BY timestamp DESC`,[e]);return d.debug(`PostgreSQL: Found ${t.rows.length} records matching pattern ${r}`),t.rows.map(s=>({recordId:s.record_id,version:s.version,value:s.value,timestamp:parseInt(s.timestamp)}))}async close(){this.pool&&(await this.pool.end(),this.pool=null,this.initialized=!1)}};var L=class extends we{constructor(e){super();this.channelPatterns=[];this.recordPatterns=[];this.messageBuffer=new Map;this.recordBuffer=new Map;this.flushTimers=new Map;this.recordFlushTimer=null;this.isShuttingDown=!1;this.initialized=!1;this.recordManager=null;this.pendingRecordUpdates=[];let{defaultAdapterOptions:t={},adapterType:s="sqlite"}=e;s==="postgres"?this.defaultAdapter=new A(t):this.defaultAdapter=new $(t),this.messageStream=w.getInstance()}setRecordManager(e){this.recordManager=e}async ready(){return this.initialized?Promise.resolve():new Promise(e=>{this.once("initialized",e)})}async processPendingRecordUpdates(){if(this.pendingRecordUpdates.length===0)return;d.info(`Processing ${this.pendingRecordUpdates.length} pending record updates`);let e=[...this.pendingRecordUpdates];this.pendingRecordUpdates=[];for(let{recordId:t,value:s,version:n}of e)this.handleRecordUpdate(t,s,n)}async initialize(){if(!this.initialized)try{await this.defaultAdapter.initialize(),this.messageStream.subscribeToMessages(this.handleStreamMessage.bind(this)),this.initialized=!0,await this.processPendingRecordUpdates(),this.emit("initialized")}catch(e){throw d.error("Failed to initialize persistence manager:",e),e}}async restorePersistedRecords(){if(!this.recordManager){d.warn("Cannot restore persisted records: record manager not available");return}let e=this.recordManager.getRedis();if(!e){d.warn("Cannot restore records: Redis not available");return}try{d.info("Restoring persisted records...");let t=this.recordPatterns.map(s=>typeof s.pattern=="string"?s.pattern:s.pattern.source);if(t.length===0){d.info("No record patterns to restore");return}for(let s of t)try{let n=R(s),i=await this.defaultAdapter.getRecords?.(n)||[];if(i.length>0){d.info(`Restoring ${i.length} records for pattern ${s}`);for(let o of i)try{let{recordId:a,value:l,version:p}=o,h=typeof l=="string"?JSON.parse(l):l,u=this.recordManager.recordKey(a),y=this.recordManager.recordVersionKey(a),P=e.pipeline();P.set(u,JSON.stringify(h)),P.set(y,p.toString()),await P.exec(),d.debug(`Restored record ${a} (version ${p})`)}catch(a){d.error(`Failed to parse record value for recordId: ${o.recordId}: ${a}`)}}else d.debug(`No records found for pattern ${s}`)}catch(n){d.error(`Error restoring records for pattern ${s}: ${n}`)}d.info("Finished restoring persisted records")}catch(t){d.error("Failed to restore persisted records:",t)}}handleStreamMessage(e){let{channel:t,message:s,instanceId:n,timestamp:i}=e;this.handleChannelMessage(t,s,n,i)}enableChannelPersistence(e,t={}){let s={historyLimit:t.historyLimit??50,filter:t.filter??(()=>!0),adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for pattern ${e}:`,n)}),this.channelPatterns.push({pattern:e,options:s})}enableRecordPersistence(e,t={}){let s={adapter:t.adapter??this.defaultAdapter,flushInterval:t.flushInterval??500,maxBufferSize:t.maxBufferSize??100};s.adapter!==this.defaultAdapter&&!this.isShuttingDown&&s.adapter.initialize().catch(n=>{d.error(`Failed to initialize adapter for record pattern ${e}:`,n)}),this.recordPatterns.push({pattern:e,options:s})}getChannelPersistenceOptions(e){for(let{pattern:t,options:s}of this.channelPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}getRecordPersistenceOptions(e){for(let{pattern:t,options:s}of this.recordPatterns)if(typeof t=="string"&&t===e||t instanceof RegExp&&t.test(e))return s}handleChannelMessage(e,t,s,n){if(!this.initialized||this.isShuttingDown)return;let i=this.getChannelPersistenceOptions(e);if(!i||!i.filter(t,e))return;let o={id:Re(),channel:e,message:t,instanceId:s,timestamp:n||Date.now()};if(this.messageBuffer.has(e)||this.messageBuffer.set(e,[]),this.messageBuffer.get(e).push(o),this.messageBuffer.get(e).length>=i.maxBufferSize){this.flushChannel(e);return}if(!this.flushTimers.has(e)){let a=setTimeout(()=>{this.flushChannel(e)},i.flushInterval);a.unref&&a.unref(),this.flushTimers.set(e,a)}}async flushChannel(e){if(!this.messageBuffer.has(e))return;this.flushTimers.has(e)&&(clearTimeout(this.flushTimers.get(e)),this.flushTimers.delete(e));let t=this.messageBuffer.get(e);if(t.length===0)return;this.messageBuffer.set(e,[]);let s=this.getChannelPersistenceOptions(e);if(s)try{await s.adapter.storeMessages(t),this.emit("flushed",{channel:e,count:t.length}),d.debug(`Flushed ${t.length} messages for channel ${e}`)}catch(n){if(d.error(`Failed to flush messages for channel ${e}:`,n),!this.isShuttingDown){let i=this.messageBuffer.get(e)||[];if(this.messageBuffer.set(e,[...t,...i]),!this.flushTimers.has(e)){let o=setTimeout(()=>{this.flushChannel(e)},1e3);o.unref&&o.unref(),this.flushTimers.set(e,o)}}}}async flushAll(){let e=Array.from(this.messageBuffer.keys());for(let t of e)await this.flushChannel(t)}async getMessages(e,t,s){if(!this.initialized)throw new Error("Persistence manager not initialized");let n=this.getChannelPersistenceOptions(e);if(!n)throw new Error(`Channel ${e} does not have persistence enabled`);return await this.flushChannel(e),n.adapter.getMessages(e,t,s||n.historyLimit)}handleRecordUpdate(e,t,s){if(this.isShuttingDown)return;if(!this.initialized){this.pendingRecordUpdates.push({recordId:e,value:t,version:s}),d.debug(`Buffered record update for ${e} (pending initialization)`);return}let n=this.getRecordPersistenceOptions(e);if(!n)return;let i={recordId:e,value:JSON.stringify(t),version:s,timestamp:Date.now()};if(this.recordBuffer.set(e,i),d.debug(`Added record ${e} to buffer, buffer size: ${this.recordBuffer.size}`),this.recordBuffer.size>=n.maxBufferSize){d.debug(`Buffer size ${this.recordBuffer.size} exceeds limit ${n.maxBufferSize}, flushing records`),this.flushRecords();return}this.recordFlushTimer||(d.debug(`Scheduling record flush in ${n.flushInterval}ms`),this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},n.flushInterval),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}async flushRecords(){if(this.recordBuffer.size===0)return;d.debug(`Flushing ${this.recordBuffer.size} records to storage`),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null);let e=Array.from(this.recordBuffer.values());this.recordBuffer.clear();let t=new Map;for(let s of e){let n=this.getRecordPersistenceOptions(s.recordId);if(!n)continue;let{adapter:i}=n;t.has(i)||t.set(i,[]),t.get(i).push(s)}for(let[s,n]of t.entries())try{s.storeRecords?(d.debug(`Storing ${n.length} records with adapter`),await s.storeRecords(n),this.emit("recordsFlushed",{count:n.length})):d.warn("Adapter does not support storing records")}catch(i){if(d.error("Failed to flush records:",i),!this.isShuttingDown){for(let o of n)this.recordBuffer.set(o.recordId,o);this.recordFlushTimer||(this.recordFlushTimer=setTimeout(()=>{this.flushRecords()},1e3),this.recordFlushTimer.unref&&this.recordFlushTimer.unref())}}}async getPersistedRecords(e){if(!this.initialized)throw new Error("Persistence manager not initialized");await this.flushRecords();try{let t=this.defaultAdapter;if(t.getRecords)return await t.getRecords(e)}catch(t){d.error(`Failed to get persisted records for pattern ${e}:`,t)}return[]}async shutdown(){if(this.isShuttingDown)return;this.isShuttingDown=!0,this.messageStream.unsubscribeFromMessages(this.handleStreamMessage.bind(this));for(let t of this.flushTimers.values())clearTimeout(t);this.flushTimers.clear(),this.recordFlushTimer&&(clearTimeout(this.recordFlushTimer),this.recordFlushTimer=null),await this.flushAll(),await this.flushRecords();let e=new Set([this.defaultAdapter]);for(let{options:t}of this.channelPatterns)e.add(t.adapter);for(let{options:t}of this.recordPatterns)e.add(t.adapter);for(let t of e)try{await t.close()}catch(s){d.error("Error closing persistence adapter:",s)}this.initialized=!1}};var H=class extends Ee{constructor(e){super(e);this.persistenceManager=null;this.status=f.OFFLINE;this._listening=!1;this.instanceId=Pe(),this.serverOptions={...e,pingInterval:e.pingInterval??3e4,latencyInterval:e.latencyInterval??5e3,maxMissedPongs:e.maxMissedPongs??1,logLevel:e.logLevel??V.ERROR},d.configure({level:this.serverOptions.logLevel,styling:!1}),this.redisManager=new J,this.redisManager.initialize(e.redisOptions,t=>this.emit("error",t)),this.instanceManager=new X({redis:this.redisManager.redis,instanceId:this.instanceId}),this.roomManager=new N({redis:this.redisManager.redis}),this.recordManager=new O({redis:this.redisManager.redis,server:this}),this.connectionManager=new I({redis:this.redisManager.pubClient,instanceId:this.instanceId,roomManager:this.roomManager}),this.presenceManager=new T({redis:this.redisManager.redis,roomManager:this.roomManager,redisManager:this.redisManager,enableExpirationEvents:this.serverOptions.enablePresenceExpirationEvents}),this.serverOptions.enablePresenceExpirationEvents&&this.redisManager.enableKeyspaceNotifications().catch(t=>this.emit("error",new Error(`Failed to enable keyspace notifications: ${t}`))),this.commandManager=new z,this.persistenceManager=new L({defaultAdapterOptions:this.serverOptions.persistenceOptions,adapterType:this.serverOptions.persistenceAdapter}),this.persistenceManager.initialize().catch(t=>{this.emit("error",new Error(`Failed to initialize persistence manager: ${t}`))}),this.channelManager=new k({redis:this.redisManager.redis,pubClient:this.redisManager.pubClient,subClient:this.redisManager.subClient}),this.channelManager.setPersistenceManager(this.persistenceManager),this.recordSubscriptionManager=new q({pubClient:this.redisManager.pubClient,recordManager:this.recordManager,emitError:t=>this.emit("error",t),persistenceManager:this.persistenceManager}),this.collectionManager=new G({redis:this.redisManager.redis,emitError:t=>this.emit("error",t)}),this.persistenceManager&&this.persistenceManager.setRecordManager(this.recordManager),this.recordManager.onRecordUpdate(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record update for collection check: ${s}`))}}),this.recordManager.onRecordRemoved(async({recordId:t})=>{try{await this.collectionManager.publishRecordChange(t)}catch(s){this.emit("error",new Error(`Failed to publish record removal for collection check: ${s}`))}}),this.pubSubManager=new B({subClient:this.redisManager.subClient,instanceId:this.instanceId,connectionManager:this.connectionManager,recordManager:this.recordManager,recordSubscriptions:this.recordSubscriptionManager.getRecordSubscriptions(),getChannelSubscriptions:this.channelManager.getSubscribers.bind(this.channelManager),emitError:t=>this.emit("error",t),collectionManager:this.collectionManager,pubClient:this.redisManager.pubClient}),this.broadcastManager=new U({connectionManager:this.connectionManager,roomManager:this.roomManager,instanceId:this.instanceId,pubClient:this.redisManager.pubClient,getPubSubChannel:t=>`${v}${t}`,emitError:t=>this.emit("error",t)}),this.on("listening",()=>{this.listening=!0,this.instanceManager.start()}),this.on("error",t=>{d.error(`Error: ${t}`)}),this.on("close",()=>{this.listening=!1}),this.pubSubManager.subscribeToInstanceChannel(),this.registerBuiltinCommands(),this.registerRecordCommands(),this.applyListeners()}get listening(){return this._listening}set listening(e){this._listening=e,this.status=e?f.ONLINE:f.OFFLINE}get port(){return this.address().port}async ready(){let e=this.listening?Promise.resolve():new Promise(s=>this.once("listening",s)),t=this.persistenceManager?this.persistenceManager.ready():Promise.resolve();await Promise.all([e,this.pubSubManager.getSubscriptionPromise(),t]),this.persistenceManager&&await this.persistenceManager.restorePersistedRecords()}applyListeners(){this.on("connection",async(e,t)=>{let s=new S(e,t,this.serverOptions,this);s.on("message",n=>{try{let i=n.toString(),o=F(i);o.id!==void 0&&!["latency:response","pong"].includes(o.command)&&this.commandManager.runCommand(o.id,o.command,o.payload,s,this)}catch(i){this.emit("error",i)}});try{await this.connectionManager.registerConnection(s),s.send({command:"mesh/assign-id",payload:s.id})}catch{s.close();return}this.emit("connected",s),s.on("close",async()=>{await this.cleanupConnection(s),this.emit("disconnected",s)}),s.on("error",n=>{this.emit("clientError",n,s)}),s.on("pong",async n=>{try{let i=await this.roomManager.getRoomsForConnection(n);for(let o of i)await this.presenceManager.isRoomTracked(o)&&await this.presenceManager.refreshPresence(n,o)}catch(i){this.emit("error",new Error(`Failed to refresh presence: ${i}`))}})})}exposeCommand(e,t,s=[]){this.commandManager.exposeCommand(e,t,s)}useMiddleware(...e){this.commandManager.useMiddleware(...e)}useMiddlewareWithCommand(e,t){this.commandManager.useMiddlewareWithCommand(e,t)}exposeChannel(e,t){this.channelManager.exposeChannel(e,t)}async writeChannel(e,t,s=0){return this.channelManager.writeChannel(e,t,s,this.instanceId)}enableChannelPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableChannelPersistence(e,t)}enableRecordPersistence(e,t={}){if(!this.persistenceManager)throw new Error("Persistence not enabled. Initialize the persistence manager first.");this.persistenceManager.enableRecordPersistence(e,t)}exposeRecord(e,t){this.recordSubscriptionManager.exposeRecord(e,t)}exposeWritableRecord(e,t){this.recordSubscriptionManager.exposeWritableRecord(e,t)}async writeRecord(e,t,s){return this.recordSubscriptionManager.writeRecord(e,t,s)}async getRecord(e){return this.recordManager.getRecord(e)}async deleteRecord(e){let t=await this.recordManager.deleteRecord(e);t&&await this.recordSubscriptionManager.publishRecordDeletion(e,t.version)}async listRecordsMatching(e,t){return this.collectionManager.listRecordsMatching(e,t)}exposeCollection(e,t){this.collectionManager.exposeCollection(e,t)}async isInRoom(e,t){let s=typeof t=="string"?t:t.id;return this.roomManager.connectionIsInRoom(e,s)}async addToRoom(e,t){let s=typeof t=="string"?t:t.id;await this.roomManager.addToRoom(e,t),await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOnline(s,e)}async removeFromRoom(e,t){let s=typeof t=="string"?t:t.id;return await this.presenceManager.isRoomTracked(e)&&await this.presenceManager.markOffline(s,e),this.roomManager.removeFromRoom(e,t)}async removeFromAllRooms(e){return this.roomManager.removeFromAllRooms(e)}async clearRoom(e){return this.roomManager.clearRoom(e)}async deleteRoom(e){return this.roomManager.deleteRoom(e)}async getRoomMembers(e){return this.roomManager.getRoomConnectionIds(e)}async getRoomMembersWithMetadata(e){let t=await this.roomManager.getRoomConnectionIds(e);return Promise.all(t.map(async s=>{try{let n=this.connectionManager.getLocalConnection(s),i;if(n)i=await this.connectionManager.getMetadata(n);else{let o=await this.redisManager.redis.hget("mesh:connections",s);i=o?JSON.parse(o):null}return{id:s,metadata:i}}catch{return{id:s,metadata:null}}}))}async getAllRooms(){return this.roomManager.getAllRooms()}async broadcast(e,t,s){return this.broadcastManager.broadcast(e,t,s)}async broadcastRoom(e,t,s){return this.broadcastManager.broadcastRoom(e,t,s)}async broadcastExclude(e,t,s){return this.broadcastManager.broadcastExclude(e,t,s)}async broadcastRoomExclude(e,t,s,n){return this.broadcastManager.broadcastRoomExclude(e,t,s,n)}trackPresence(e,t){this.presenceManager.trackRoom(e,t)}registerBuiltinCommands(){this.exposeCommand("mesh/noop",async()=>!0),this.exposeCommand("mesh/subscribe-channel",async e=>{let{channel:t,historyLimit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.channelManager.getSubscribers(t)||await this.channelManager.subscribeToRedisChannel(t),this.channelManager.addSubscription(t,e.connection),{success:!0,history:s&&s>0?await this.channelManager.getChannelHistory(t,s,n):[]}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/unsubscribe-channel",async e=>{let{channel:t}=e.payload,s=this.channelManager.removeSubscription(t,e.connection);return s&&!this.channelManager.getSubscribers(t)&&await this.channelManager.unsubscribeFromRedisChannel(t),s}),this.exposeCommand("mesh/get-channel-history",async e=>{let{channel:t,limit:s,since:n}=e.payload;if(!await this.channelManager.isChannelExposed(t,e.connection))return{success:!1,history:[]};try{return this.persistenceManager?.getChannelPersistenceOptions(t)?{success:!0,history:(await this.persistenceManager.getMessages(t,n,s||this.persistenceManager.getChannelPersistenceOptions(t)?.historyLimit)).map(o=>o.message)}:{success:!0,history:await this.channelManager.getChannelHistory(t,s||50,n)}}catch{return{success:!1,history:[]}}}),this.exposeCommand("mesh/join-room",async e=>{let{roomName:t}=e.payload;return await this.addToRoom(t,e.connection),{success:!0,present:await this.getRoomMembersWithMetadata(t)}}),this.exposeCommand("mesh/leave-room",async e=>{let{roomName:t}=e.payload;return await this.removeFromRoom(t,e.connection),{success:!0}}),this.exposeCommand("mesh/get-connection-metadata",async e=>{let{connectionId:t}=e.payload,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/get-my-connection-metadata",async e=>{let t=e.connection.id,s=this.connectionManager.getLocalConnection(t);if(s)return{metadata:await this.connectionManager.getMetadata(s)};{let n=await this.redisManager.redis.hget("mesh:connections",t);return{metadata:n?JSON.parse(n):null}}}),this.exposeCommand("mesh/set-my-connection-metadata",async e=>{let{metadata:t,options:s}=e.payload,n=e.connection.id,i=this.connectionManager.getLocalConnection(n);if(i)try{return await this.connectionManager.setMetadata(i,t,s),{success:!0}}catch{return{success:!1}}else return{success:!1}}),this.exposeCommand("mesh/get-room-metadata",async e=>{let{roomName:t}=e.payload;return{metadata:await this.roomManager.getMetadata(t)}})}registerRecordCommands(){this.exposeCommand("mesh/subscribe-record",async e=>{let{recordId:t,mode:s="full"}=e.payload,n=e.connection.id;if(!await this.recordSubscriptionManager.isRecordExposed(t,e.connection))return{success:!1};try{let{record:i,version:o}=await this.recordManager.getRecordAndVersion(t);return this.recordSubscriptionManager.addSubscription(t,n,s),{success:!0,record:i,version:o}}catch(i){return console.error(`Failed to subscribe to record ${t}:`,i),{success:!1}}}),this.exposeCommand("mesh/unsubscribe-record",async e=>{let{recordId:t}=e.payload,s=e.connection.id;return this.recordSubscriptionManager.removeSubscription(t,s)}),this.exposeCommand("mesh/publish-record-update",async e=>{let{recordId:t,newValue:s,options:n}=e.payload;if(!await this.recordSubscriptionManager.isRecordWritable(t,e.connection))throw new Error(`Record "${t}" is not writable by this connection.`);try{return await this.writeRecord(t,s,n),{success:!0}}catch(i){throw new Error(`Failed to publish update for record "${t}": ${i.message}`)}}),this.exposeCommand("mesh/subscribe-presence",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=`mesh:presence:updates:${t}`;this.channelManager.addSubscription(s,e.connection),(!this.channelManager.getSubscribers(s)||this.channelManager.getSubscribers(s)?.size===1)&&await this.channelManager.subscribeToRedisChannel(s);let n=await this.getRoomMembersWithMetadata(t),i=await this.presenceManager.getAllPresenceStates(t),o={};return i.forEach((a,l)=>{o[l]=a}),{success:!0,present:n,states:o}}catch(s){return console.error(`Failed to subscribe to presence for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/unsubscribe-presence",async e=>{let{roomName:t}=e.payload,s=`mesh:presence:updates:${t}`;return this.channelManager.removeSubscription(s,e.connection)}),this.exposeCommand("mesh/publish-presence-state",async e=>{let{roomName:t,state:s,expireAfter:n,silent:i}=e.payload,o=e.connection.id;if(!s||!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,o))return!1;try{return await this.presenceManager.publishPresenceState(o,t,s,n,i),!0}catch(a){return console.error(`Failed to publish presence state for room ${t}:`,a),!1}}),this.exposeCommand("mesh/clear-presence-state",async e=>{let{roomName:t}=e.payload,s=e.connection.id;if(!await this.presenceManager.isRoomTracked(t,e.connection)||!await this.isInRoom(t,s))return!1;try{return await this.presenceManager.clearPresenceState(s,t),!0}catch(n){return console.error(`Failed to clear presence state for room ${t}:`,n),!1}}),this.exposeCommand("mesh/get-presence-state",async e=>{let{roomName:t}=e.payload;if(!await this.presenceManager.isRoomTracked(t,e.connection))return{success:!1,present:[]};try{let s=await this.presenceManager.getPresentConnections(t),n=await this.presenceManager.getAllPresenceStates(t),i={};return n.forEach((o,a)=>{i[a]=o}),{success:!0,present:s,states:i}}catch(s){return console.error(`Failed to get presence state for room ${t}:`,s),{success:!1,present:[]}}}),this.exposeCommand("mesh/subscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;if(!await this.collectionManager.isCollectionExposed(t,e.connection))return{success:!1,ids:[],records:[],version:0};try{let{ids:n,records:i,version:o}=await this.collectionManager.addSubscription(t,s,e.connection),a=i.map(l=>({id:l.id,record:l}));return{success:!0,ids:n,records:a,version:o}}catch(n){return console.error(`Failed to subscribe to collection ${t}:`,n),{success:!1,ids:[],records:[],version:0}}}),this.exposeCommand("mesh/unsubscribe-collection",async e=>{let{collectionId:t}=e.payload,s=e.connection.id;return this.collectionManager.removeSubscription(t,s)})}async cleanupConnection(e){d.info("Cleaning up connection:",e.id),e.stopIntervals();try{await this.presenceManager.cleanupConnection(e),await this.connectionManager.cleanupConnection(e),await this.roomManager.cleanupConnection(e),this.recordSubscriptionManager.cleanupConnection(e),this.channelManager.cleanupConnection(e),await this.collectionManager.cleanupConnection(e)}catch(t){this.emit("error",new Error(`Failed to clean up connection: ${t}`))}}async close(e){this.redisManager.isShuttingDown=!0;let t=Object.values(this.connectionManager.getLocalConnections());if(await Promise.all(t.map(async s=>{s.isDead||await s.close(),await this.cleanupConnection(s)})),await new Promise((s,n)=>{super.close(i=>{i?n(i):s()})}),this.persistenceManager)try{await this.persistenceManager.shutdown()}catch(s){d.error("Error shutting down persistence manager:",s)}await this.instanceManager.stop(),await this.pubSubManager.cleanup(),await this.presenceManager.cleanup(),this.redisManager.disconnect(),this.listening=!1,this.removeAllListeners(),e&&e()}onConnection(e){return this.on("connected",e),this}onDisconnection(e){return this.on("disconnected",e),this}onRecordUpdate(e){return this.recordManager.onRecordUpdate(e)}onRecordRemoved(e){return this.recordManager.onRecordRemoved(e)}};export{S as Connection,I as ConnectionManager,x as MeshContext,H as MeshServer,w as MessageStream,L as PersistenceManager,A as PostgreSQLPersistenceAdapter,T as PresenceManager,O as RecordManager,N as RoomManager,$ as SQLitePersistenceAdapter};
|