@solarpunkltd/swarm-chat-js 6.0.5 → 6.0.7

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.esm.js CHANGED
@@ -55547,11 +55547,11 @@ class Waku {
55547
55547
  }
55548
55548
  createWakuDecoder(u) {
55549
55549
  if (!(u != null && u.trim())) throw new Error("Topic name must be a non-empty string");
55550
- const b = createRoutingInfo({
55550
+ const h = {
55551
55551
  clusterId: WAKU_CLUSTER_ID,
55552
55552
  numShardsInCluster: 8
55553
- }, { contentTopic: u });
55554
- return createDecoder(u, b);
55553
+ }, b = `/solarpunk-msrs/1/${u}/proto`, v = createRoutingInfo(h, { contentTopic: b });
55554
+ return createDecoder(b, v);
55555
55555
  }
55556
55556
  async subscribeToTopic() {
55557
55557
  var u;
@@ -55826,14 +55826,13 @@ class SwarmChat {
55826
55826
  async startWakuMessageFetch(u) {
55827
55827
  this.logger.info("Waku is enabled"), u && (this.isExternalWakuNode = !0, this.logger.info("Using external Waku node instance"));
55828
55828
  const h = this.swarmSettings.chatTopic;
55829
- if (this.waku = new Waku(
55829
+ this.waku = new Waku(
55830
55830
  h,
55831
- (v) => {
55832
- this.emitter.emit(EVENTS.MESSAGE_RECEIVED, v);
55831
+ (b) => {
55832
+ this.emitter.emit(EVENTS.MESSAGE_RECEIVED, b);
55833
55833
  },
55834
55834
  u
55835
- ), !await this.waku.isReady())
55836
- throw new Error("Waku node is not reachable");
55835
+ );
55837
55836
  }
55838
55837
  getEmitter() {
55839
55838
  return this.emitter;
package/dist/index.js CHANGED
@@ -115,4 +115,4 @@ message MessagePayload {
115
115
  MessageData message = 1;
116
116
  repeated MessageStateRef messageStateRefs = 2;
117
117
  }
118
- `;let protoRoot=null,messagePayloadType=null;const errorHandler=ErrorHandler.getInstance();async function initializeProtobuf(){protoRoot||(protoRoot=protobufjs.Root.fromJSON(protobuf.parse(protoSchema).root),messagePayloadType=protoRoot.lookupType("MessagePayload"))}async function decodeMessagePayload(y){if(await initializeProtobuf(),!messagePayloadType)throw new Error("MessagePayload type not initialized");const u={0:"text",1:"thread",2:"reaction"};try{const h=messagePayloadType.decode(y),b=messagePayloadType.toObject(h,{longs:String,enums:String,defaults:!0});if(!b.message)throw new Error("Decoded object has no message field");let v=b.message.additionalProps;if(v&&typeof v=="string")try{v=JSON.parse(v)}catch{console.warn("Failed to parse additionalProps as JSON:",v)}const w={...b.message,type:u[b.message.type]??"text",timestamp:Number(b.message.timestamp),additionalProps:v},_=(b.messageStateRefs||[]).map(E=>({...E,timestamp:Number(E.timestamp)}));return{message:w,messageStateRefs:_}}catch(h){throw errorHandler.handleError(h,"ProtoMessage.decodeMessagePayload"),h}}class Waku{constructor(u,h,b){this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.wakuNode=null,this.handleWakuMessage=async({payload:v})=>{try{if(!v)return this.logger.warn("Received Waku message without payload");const{message:w}=await decodeMessagePayload(v);if(!(w!=null&&w.id))return this.logger.warn("Received invalid message structure via Waku");this.onMessage(w)}catch(w){this.errorHandler.handleError(w,"Waku.handleWakuMessage")}},this.chatTopic=u,this.onMessage=h,this.wakuNode=b||null,this.initPromise=this.init()}async init(){if(this.wakuNode=this.wakuNode??await this.createWakuLightNode(),!await this.subscribeToTopic())throw new Error("Failed to subscribe to Waku topic");this.logger.info(`Subscribed to Waku topic: ${this.chatTopic}`)}async createWakuLightNode(){const u=await createLightNode({defaultBootstrap:!0,networkConfig:{clusterId:WAKU_CLUSTER_ID}});return await u.start(),await u.waitForPeers([Protocols.LightPush,Protocols.Filter],3e4),this.logger.info(`Waku node ready: ${u.libp2p.peerId.toString()}`),u}createWakuDecoder(u){if(!(u!=null&&u.trim()))throw new Error("Topic name must be a non-empty string");const b=createRoutingInfo({clusterId:WAKU_CLUSTER_ID,numShardsInCluster:8},{contentTopic:u});return createDecoder(u,b)}async subscribeToTopic(){var u;if(!((u=this.wakuNode)!=null&&u.isStarted()))throw new Error("Waku node is not running");return this.wakuNode.filter.subscribe(this.createWakuDecoder(this.chatTopic),this.handleWakuMessage)}async isReady(){var u;try{return await this.initPromise,((u=this.wakuNode)==null?void 0:u.isStarted())??!1}catch{return!1}}async stop(){try{await this.initPromise,this.wakuNode&&(await this.wakuNode.stop(),this.wakuNode=null,this.logger.info("Waku node stopped successfully"))}catch(u){throw this.errorHandler.handleError(u,"Waku.stop"),u}}}class SwarmHistory{constructor(u,h){this.utils=u,this.emitter=h,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.processedRefs=new Set,this.refRetryCount=new Map,this.bannedRefs=new Set,this.latestStatefulMessage=null,this.MAX_RETRIES=3,this.RETRY_DELAY=1e3}async init(){try{const{data:u,index:h}=await this.utils.fetchLatestChatMessage();this.latestStatefulMessage=u;try{await this.initMessageState(u)}catch(b){this.errorHandler.handleError(b,"SwarmHistory.initMessageState")}return h}catch(u){return this.errorHandler.handleError(u,"SwarmHistory.init"),beeJs.FeedIndex.fromBigInt(0n)}}async initMessageState(u){try{if(!validateGsocMessage(u)){this.logger.warn("Invalid GSOC message during message state initialization");return}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to initialize");return}const h=this.findLatestRef(u.messageStateRefs);if(!h){this.logger.warn("No valid latest reference found");return}this.logger.debug("Initializing message state with latest ref:",h.reference),await this.processMessageRefWithRetry(h.reference)}catch(h){if(this.utils.isNotFoundError(h)){this.logger.debug("No latest GSOC message found for message state initialization");return}this.errorHandler.handleError(h,"SwarmHistory.init")}}async fetchPreviousMessageState(){let u=this.latestStatefulMessage;if(!u){const{data:b}=await this.utils.fetchLatestChatMessage();u=b,this.latestStatefulMessage=b}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to fetch");return}const h=[...u.messageStateRefs].sort((b,v)=>b.timestamp-v.timestamp);for(const b of h)!this.processedRefs.has(b.reference)&&!this.bannedRefs.has(b.reference)&&(this.logger.debug("Fetching previous message ref:",b.reference),await this.processMessageRefWithRetry(b.reference))}findLatestRef(u){return u.length===0?null:u.reduce((h,b)=>b.timestamp>h.timestamp?b:h)}async processMessageRefWithRetry(u){if(!(this.processedRefs.has(u)||this.bannedRefs.has(u)))try{await this.processMessageRef(u)}catch(h){await this.handleRefError(u,h)}}async processMessageRef(u){const h=await this.utils.downloadObjectFromBee(u);if(!validateMessageState(h))throw new Error(`Invalid message state for ref: ${u}`);this.processedRefs.add(u),this.refRetryCount.delete(u);for(const v of h)this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)}async handleRefError(u,h){const b=this.refRetryCount.get(u)||0,v=b+1;if(this.logger.warn(`Error processing ref ${u} (attempt ${v}/${this.MAX_RETRIES}):`,h),v>=this.MAX_RETRIES)this.bannedRefs.add(u),this.refRetryCount.delete(u),this.logger.error(`Ref ${u} has been banned after ${this.MAX_RETRIES} failed attempts`),this.errorHandler.handleError(h,"SwarmHistory.processMessageRef");else{this.refRetryCount.set(u,v);const w=this.RETRY_DELAY*Math.pow(2,b);this.logger.debug(`Retrying ref ${u} after ${w}ms delay`),await sleep(w),await this.processMessageRefWithRetry(u)}}hasPreviousMessages(){return!!this.latestStatefulMessage&&!!this.latestStatefulMessage.messageStateRefs&&this.latestStatefulMessage.messageStateRefs.length>1}cleanup(){this.processedRefs.clear(),this.refRetryCount.clear(),this.bannedRefs.clear(),this.latestStatefulMessage=null}}class SwarmChatUtils{constructor(u,h){this.userDetails=u,this.swarmSettings=h,this.depth=26,this.errorHandler=ErrorHandler.getInstance(),this.UPLOAD_GSOC_TIMEOUT=2e3}generateUserOwnedFeedId(u,h){return`${u}_EthercastChat_${h}`}isNotFoundError(u){return u.stack.includes("404")||u.message.includes("Not Found")||u.message.includes("404")||u.code===404}async writeOwnFeedDataByIndex(u,h){const{enveloped:b,stamp:v}=this.swarmSettings;if(!b)await this.writeOwnFeedDataByIndexOwned(u,h);else if(v)await this.writeOwnFeedDataByIndexEnvelope(u,h);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async writeOwnFeedDataByIndexOwned(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=this.generateUserOwnedFeedId(w,E),x=beeJs.Topic.fromString(S);await b.makeFeedWriter(x,new beeJs.PrivateKey(_)).uploadPayload(v,JSON.stringify(h),{index:u})}async writeOwnFeedDataByIndexEnvelope(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=new beeJs.PrivateKey(_),x=beeJs.Stamper.fromBlank(_,v,this.depth),T=this.generateUserOwnedFeedId(w,E),C=beeJs.Topic.fromString(T),O=makeFeedIdentifier(C,u),D=makeContentAddressedChunk(h),U=makeSingleOwnerChunk(D,O,S),te={hash:()=>U.address.toUint8Array()},X=x.stamp(te),{upload:ie}=b.makeSOCWriter(S),ee=beeJs.Bytes.fromUtf8(h);return(await ie(X,O,ee.toUint8Array())).reference.toHex()}orderMessages(u){return u.sort((h,b)=>h.timestamp-b.timestamp)}async downloadObjectFromBee(u){try{const{bee:h}=this.swarmSettings,b=await h.downloadData(u);return b?b.toJSON():null}catch(h){return this.errorHandler.handleError(h,"Utils.downloadObjectFromBee"),null}}async uploadObjectToBee(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(h){if(b)return this.uploadObjectToBeeEnvelope(u);throw new Error("Enveloped mode is enabled, but stamp is not provided")}else return this.uploadObjectToBeeOwn(u)}async uploadObjectToBeeOwn(u){try{const{bee:h,stamp:b}=this.swarmSettings;return(await h.uploadData(b,JSON.stringify(u),{redundancyLevel:4})).reference.toString()}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async uploadObjectToBeeEnvelope(u){try{const{bee:h,stamp:b}=this.swarmSettings,{privateKey:v}=this.userDetails,w=beeJs.Stamper.fromBlank(v,b,this.depth),_=beeJs.Bytes.fromUtf8(JSON.stringify(u)),E=new MerkleTree(async x=>{await h.uploadChunk(w.stamp(x),x.build())});await E.append(_.toUint8Array());const S=await E.finalize();return Binary.uint8ArrayToHex(S.hash())}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async getOwnLatestFeedIndex(){try{const{bee:u,chatTopic:h}=this.swarmSettings,{ownAddress:b}=this.userDetails,v=this.generateUserOwnedFeedId(h,b),w=beeJs.Topic.fromString(v),E=await u.makeFeedReader(w,b).downloadPayload(),S=Number(E.feedIndex.toBigInt()),x=S+1;return{latestIndex:S,nextIndex:x}}catch(u){if(this.isNotFoundError(u))return{latestIndex:-1,nextIndex:0};throw u}}async fetchLatestChatMessage(){const{bee:u,chatTopic:h,chatAddress:b}=this.swarmSettings,w=await u.makeFeedReader(beeJs.Topic.fromString(h),remove0x(b)).downloadPayload();return{data:w.payload.toJSON(),index:w.feedIndex}}async fetchChatMessage(u){const{bee:h,chatTopic:b,chatAddress:v}=this.swarmSettings;return(await h.makeFeedReader(beeJs.Topic.fromString(b),remove0x(v)).downloadPayload({index:u})).payload.toJSON()}async sendMessageToGsoc(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(!h)await this.sendMessageToGsocOwn(u);else if(b)await this.sendMessageToGsocEnvelope(u);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async rawSocDownload(u,h){const{beeUrl:b}=this.swarmSettings,v=await fetch(`${b}/soc/${u}/${h}`,{headers:{"swarm-chunk-retrieval-timeout":"2000ms"}});if(!v.ok){const w=new Error(`Failed to fetch: ${u}/${h} [${v.status}]`);throw w.status=v.status,w}return v.text()}async sendMessageToGsocOwn(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,_=new beeJs.PrivateKey(w),E=beeJs.Identifier.fromString(v),S=beeJs.Bytes.fromUtf8(u),{upload:x}=h.makeSOCWriter(_,{timeout:this.UPLOAD_GSOC_TIMEOUT});await x(b,E,S.toUint8Array())}async sendMessageToGsocEnvelope(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,{privateKey:_}=this.userDetails,E=beeJs.Stamper.fromBlank(_,b,this.depth),S=new beeJs.PrivateKey(w),x=beeJs.Identifier.fromString(v),T=beeJs.Bytes.fromUtf8(u),C=makeContentAddressedChunk(T.toUint8Array()),O=makeSingleOwnerChunk(C,x,S),D={hash:()=>O.address.toUint8Array()},U=E.stamp(D),{upload:te}=h.makeSOCWriter(S,{timeout:this.UPLOAD_GSOC_TIMEOUT});await te(U,x,T.toUint8Array())}}class SwarmChat{constructor(u){this.waku=null,this.isExternalWakuNode=!1,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.gsocIndex=null,this.fetchProcessRunning=!1,this.stopFetch=!1;const h=new beeJs.PrivateKey(remove0x(u.user.privateKey));this.userDetails={privateKey:u.user.privateKey,ownAddress:h.publicKey().address().toString(),nickname:u.user.nickname,ownIndex:-1},this.swarmSettings={bee:new beeJs.Bee(u.infra.beeUrl),beeUrl:u.infra.beeUrl,stamp:u.infra.stamp||"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",enveloped:u.infra.enveloped,gsocTopic:u.infra.gsocTopic,gsocResourceId:u.infra.gsocResourceId,chatTopic:u.infra.chatTopic,chatAddress:u.infra.chatAddress,waku:u.infra.waku},this.emitter=new EventEmitter$3,this.utils=new SwarmChatUtils(this.userDetails,this.swarmSettings),this.history=new SwarmHistory(this.utils,this.emitter)}async start(){await this.init(),await this.startMessageFetchProcess()}async startMessageFetchProcess(){const{waku:u}=this.swarmSettings;u!=null&&u.enabled?await this.startWakuMessageFetch(u.node):await this.startPollingMessageFetch()}stop(){this.emitter.cleanAll(),this.stopPollingMessageFetch(),this.history.cleanup(),this.waku&&!this.isExternalWakuNode&&this.waku.stop(),this.waku=null}async startWakuMessageFetch(u){this.logger.info("Waku is enabled"),u&&(this.isExternalWakuNode=!0,this.logger.info("Using external Waku node instance"));const h=this.swarmSettings.chatTopic;if(this.waku=new Waku(h,v=>{this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)},u),!await this.waku.isReady())throw new Error("Waku node is not reachable")}getEmitter(){return this.emitter}orderMessages(u){return this.utils.orderMessages(u)}async sendMessage(u,h,b,v,w){const _=this.userDetails.ownIndex===-1?0:this.userDetails.ownIndex+1,E={id:v||v4$1(),username:this.userDetails.nickname,address:this.userDetails.ownAddress,chatTopic:this.swarmSettings.chatTopic,userTopic:this.utils.generateUserOwnedFeedId(this.swarmSettings.chatTopic,this.userDetails.ownAddress),signature:this.getSignature(u),timestamp:Date.now(),index:_,type:h,targetMessageId:b,message:u,additionalProps:w},S=validateMessageWithAdditionalProperties(E);if(!S.isValid){const x=`Invalid message: ${S.errors.join(", ")}`;throw this.logger.error(x),new Error(x)}try{this.emitter.emit(EVENTS.MESSAGE_REQUEST_INITIATED,E),await this.utils.writeOwnFeedDataByIndex(_,JSON.stringify(E)),this.userDetails.ownIndex=_,this.emitter.emit(EVENTS.MESSAGE_REQUEST_UPLOADED,E),await this.broadcastUserMessage(E)}catch(x){this.emitter.emit(EVENTS.MESSAGE_REQUEST_ERROR,E),this.errorHandler.handleError(x,"Chat.sendMessage")}}async fetchPreviousMessages(){try{return this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!0),await this.history.fetchPreviousMessageState()}finally{this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasPreviousMessages()}async retrySendMessage(u){this.sendMessage(u.message,u.type,u.targetMessageId,u.id,u.additionalProps)}async retryBroadcastUserMessage(u){await this.broadcastUserMessage(u)}async init(){try{this.emitter.emit(EVENTS.LOADING_INIT,!0);const[u,h]=await Promise.allSettled([this.initOwnIndex(),this.history.init()]);if(u.status==="rejected")throw u.reason;h.status==="fulfilled"&&(this.gsocIndex=h.value),this.emitter.emit(EVENTS.LOADING_INIT,!1)}catch(u){this.errorHandler.handleError(u,"Chat.initSelfState"),this.emitter.emit(EVENTS.CRITICAL_ERROR,u)}}async initOwnIndex(){const{latestIndex:b}=await retryAwaitableAsync(()=>this.utils.getOwnLatestFeedIndex(),10,1e3);this.userDetails.ownIndex=b}async fetchLatestMessage(){try{if(!this.gsocIndex)return;const u=beeJs.Topic.fromString(this.swarmSettings.chatTopic),h=makeFeedIdentifier(u,this.gsocIndex),b=await this.utils.rawSocDownload(this.swarmSettings.chatAddress,h.toString()),v=JSON.parse(b);if(!validateGsocMessage(v)){this.logger.warn("Invalid GSOC message during fetching");return}this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v.message),this.gsocIndex=this.gsocIndex.next()}catch(u){if(this.utils.isNotFoundError(u))return;this.errorHandler.handleError(u,"Chat.fetchLatestMessage")}}async broadcastUserMessage(u){try{return retryAwaitableAsync(()=>this.utils.sendMessageToGsoc(JSON.stringify(u)))}catch(h){this.errorHandler.handleError(h,"Chat.broadcastUserMessage")}}getSignature(u){const{ownAddress:h,privateKey:b,nickname:v}=this.userDetails,w=new beeJs.EthAddress(h).toString(),_=new beeJs.PrivateKey(b);if(_.publicKey().address().toString()!==w)throw new Error("The provided address does not match the address derived from the private key");const S=Date.now();return _.sign(JSON.stringify({username:v,address:w,message:u,timestamp:S})).toHex()}async startPollingMessageFetch(){if(this.fetchProcessRunning)return;this.fetchProcessRunning=!0,this.stopFetch=!1;const u=async()=>{if(this.stopFetch){this.fetchProcessRunning=!1;return}await this.fetchLatestMessage(),setTimeout(u,200)};u()}stopPollingMessageFetch(){this.stopFetch=!0}}exports.EVENTS=EVENTS;exports.MessageType=MessageType;exports.SwarmChat=SwarmChat;
118
+ `;let protoRoot=null,messagePayloadType=null;const errorHandler=ErrorHandler.getInstance();async function initializeProtobuf(){protoRoot||(protoRoot=protobufjs.Root.fromJSON(protobuf.parse(protoSchema).root),messagePayloadType=protoRoot.lookupType("MessagePayload"))}async function decodeMessagePayload(y){if(await initializeProtobuf(),!messagePayloadType)throw new Error("MessagePayload type not initialized");const u={0:"text",1:"thread",2:"reaction"};try{const h=messagePayloadType.decode(y),b=messagePayloadType.toObject(h,{longs:String,enums:String,defaults:!0});if(!b.message)throw new Error("Decoded object has no message field");let v=b.message.additionalProps;if(v&&typeof v=="string")try{v=JSON.parse(v)}catch{console.warn("Failed to parse additionalProps as JSON:",v)}const w={...b.message,type:u[b.message.type]??"text",timestamp:Number(b.message.timestamp),additionalProps:v},_=(b.messageStateRefs||[]).map(E=>({...E,timestamp:Number(E.timestamp)}));return{message:w,messageStateRefs:_}}catch(h){throw errorHandler.handleError(h,"ProtoMessage.decodeMessagePayload"),h}}class Waku{constructor(u,h,b){this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.wakuNode=null,this.handleWakuMessage=async({payload:v})=>{try{if(!v)return this.logger.warn("Received Waku message without payload");const{message:w}=await decodeMessagePayload(v);if(!(w!=null&&w.id))return this.logger.warn("Received invalid message structure via Waku");this.onMessage(w)}catch(w){this.errorHandler.handleError(w,"Waku.handleWakuMessage")}},this.chatTopic=u,this.onMessage=h,this.wakuNode=b||null,this.initPromise=this.init()}async init(){if(this.wakuNode=this.wakuNode??await this.createWakuLightNode(),!await this.subscribeToTopic())throw new Error("Failed to subscribe to Waku topic");this.logger.info(`Subscribed to Waku topic: ${this.chatTopic}`)}async createWakuLightNode(){const u=await createLightNode({defaultBootstrap:!0,networkConfig:{clusterId:WAKU_CLUSTER_ID}});return await u.start(),await u.waitForPeers([Protocols.LightPush,Protocols.Filter],3e4),this.logger.info(`Waku node ready: ${u.libp2p.peerId.toString()}`),u}createWakuDecoder(u){if(!(u!=null&&u.trim()))throw new Error("Topic name must be a non-empty string");const h={clusterId:WAKU_CLUSTER_ID,numShardsInCluster:8},b=`/solarpunk-msrs/1/${u}/proto`,v=createRoutingInfo(h,{contentTopic:b});return createDecoder(b,v)}async subscribeToTopic(){var u;if(!((u=this.wakuNode)!=null&&u.isStarted()))throw new Error("Waku node is not running");return this.wakuNode.filter.subscribe(this.createWakuDecoder(this.chatTopic),this.handleWakuMessage)}async isReady(){var u;try{return await this.initPromise,((u=this.wakuNode)==null?void 0:u.isStarted())??!1}catch{return!1}}async stop(){try{await this.initPromise,this.wakuNode&&(await this.wakuNode.stop(),this.wakuNode=null,this.logger.info("Waku node stopped successfully"))}catch(u){throw this.errorHandler.handleError(u,"Waku.stop"),u}}}class SwarmHistory{constructor(u,h){this.utils=u,this.emitter=h,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.processedRefs=new Set,this.refRetryCount=new Map,this.bannedRefs=new Set,this.latestStatefulMessage=null,this.MAX_RETRIES=3,this.RETRY_DELAY=1e3}async init(){try{const{data:u,index:h}=await this.utils.fetchLatestChatMessage();this.latestStatefulMessage=u;try{await this.initMessageState(u)}catch(b){this.errorHandler.handleError(b,"SwarmHistory.initMessageState")}return h}catch(u){return this.errorHandler.handleError(u,"SwarmHistory.init"),beeJs.FeedIndex.fromBigInt(0n)}}async initMessageState(u){try{if(!validateGsocMessage(u)){this.logger.warn("Invalid GSOC message during message state initialization");return}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to initialize");return}const h=this.findLatestRef(u.messageStateRefs);if(!h){this.logger.warn("No valid latest reference found");return}this.logger.debug("Initializing message state with latest ref:",h.reference),await this.processMessageRefWithRetry(h.reference)}catch(h){if(this.utils.isNotFoundError(h)){this.logger.debug("No latest GSOC message found for message state initialization");return}this.errorHandler.handleError(h,"SwarmHistory.init")}}async fetchPreviousMessageState(){let u=this.latestStatefulMessage;if(!u){const{data:b}=await this.utils.fetchLatestChatMessage();u=b,this.latestStatefulMessage=b}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to fetch");return}const h=[...u.messageStateRefs].sort((b,v)=>b.timestamp-v.timestamp);for(const b of h)!this.processedRefs.has(b.reference)&&!this.bannedRefs.has(b.reference)&&(this.logger.debug("Fetching previous message ref:",b.reference),await this.processMessageRefWithRetry(b.reference))}findLatestRef(u){return u.length===0?null:u.reduce((h,b)=>b.timestamp>h.timestamp?b:h)}async processMessageRefWithRetry(u){if(!(this.processedRefs.has(u)||this.bannedRefs.has(u)))try{await this.processMessageRef(u)}catch(h){await this.handleRefError(u,h)}}async processMessageRef(u){const h=await this.utils.downloadObjectFromBee(u);if(!validateMessageState(h))throw new Error(`Invalid message state for ref: ${u}`);this.processedRefs.add(u),this.refRetryCount.delete(u);for(const v of h)this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)}async handleRefError(u,h){const b=this.refRetryCount.get(u)||0,v=b+1;if(this.logger.warn(`Error processing ref ${u} (attempt ${v}/${this.MAX_RETRIES}):`,h),v>=this.MAX_RETRIES)this.bannedRefs.add(u),this.refRetryCount.delete(u),this.logger.error(`Ref ${u} has been banned after ${this.MAX_RETRIES} failed attempts`),this.errorHandler.handleError(h,"SwarmHistory.processMessageRef");else{this.refRetryCount.set(u,v);const w=this.RETRY_DELAY*Math.pow(2,b);this.logger.debug(`Retrying ref ${u} after ${w}ms delay`),await sleep(w),await this.processMessageRefWithRetry(u)}}hasPreviousMessages(){return!!this.latestStatefulMessage&&!!this.latestStatefulMessage.messageStateRefs&&this.latestStatefulMessage.messageStateRefs.length>1}cleanup(){this.processedRefs.clear(),this.refRetryCount.clear(),this.bannedRefs.clear(),this.latestStatefulMessage=null}}class SwarmChatUtils{constructor(u,h){this.userDetails=u,this.swarmSettings=h,this.depth=26,this.errorHandler=ErrorHandler.getInstance(),this.UPLOAD_GSOC_TIMEOUT=2e3}generateUserOwnedFeedId(u,h){return`${u}_EthercastChat_${h}`}isNotFoundError(u){return u.stack.includes("404")||u.message.includes("Not Found")||u.message.includes("404")||u.code===404}async writeOwnFeedDataByIndex(u,h){const{enveloped:b,stamp:v}=this.swarmSettings;if(!b)await this.writeOwnFeedDataByIndexOwned(u,h);else if(v)await this.writeOwnFeedDataByIndexEnvelope(u,h);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async writeOwnFeedDataByIndexOwned(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=this.generateUserOwnedFeedId(w,E),x=beeJs.Topic.fromString(S);await b.makeFeedWriter(x,new beeJs.PrivateKey(_)).uploadPayload(v,JSON.stringify(h),{index:u})}async writeOwnFeedDataByIndexEnvelope(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=new beeJs.PrivateKey(_),x=beeJs.Stamper.fromBlank(_,v,this.depth),T=this.generateUserOwnedFeedId(w,E),C=beeJs.Topic.fromString(T),O=makeFeedIdentifier(C,u),D=makeContentAddressedChunk(h),U=makeSingleOwnerChunk(D,O,S),te={hash:()=>U.address.toUint8Array()},X=x.stamp(te),{upload:ie}=b.makeSOCWriter(S),ee=beeJs.Bytes.fromUtf8(h);return(await ie(X,O,ee.toUint8Array())).reference.toHex()}orderMessages(u){return u.sort((h,b)=>h.timestamp-b.timestamp)}async downloadObjectFromBee(u){try{const{bee:h}=this.swarmSettings,b=await h.downloadData(u);return b?b.toJSON():null}catch(h){return this.errorHandler.handleError(h,"Utils.downloadObjectFromBee"),null}}async uploadObjectToBee(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(h){if(b)return this.uploadObjectToBeeEnvelope(u);throw new Error("Enveloped mode is enabled, but stamp is not provided")}else return this.uploadObjectToBeeOwn(u)}async uploadObjectToBeeOwn(u){try{const{bee:h,stamp:b}=this.swarmSettings;return(await h.uploadData(b,JSON.stringify(u),{redundancyLevel:4})).reference.toString()}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async uploadObjectToBeeEnvelope(u){try{const{bee:h,stamp:b}=this.swarmSettings,{privateKey:v}=this.userDetails,w=beeJs.Stamper.fromBlank(v,b,this.depth),_=beeJs.Bytes.fromUtf8(JSON.stringify(u)),E=new MerkleTree(async x=>{await h.uploadChunk(w.stamp(x),x.build())});await E.append(_.toUint8Array());const S=await E.finalize();return Binary.uint8ArrayToHex(S.hash())}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async getOwnLatestFeedIndex(){try{const{bee:u,chatTopic:h}=this.swarmSettings,{ownAddress:b}=this.userDetails,v=this.generateUserOwnedFeedId(h,b),w=beeJs.Topic.fromString(v),E=await u.makeFeedReader(w,b).downloadPayload(),S=Number(E.feedIndex.toBigInt()),x=S+1;return{latestIndex:S,nextIndex:x}}catch(u){if(this.isNotFoundError(u))return{latestIndex:-1,nextIndex:0};throw u}}async fetchLatestChatMessage(){const{bee:u,chatTopic:h,chatAddress:b}=this.swarmSettings,w=await u.makeFeedReader(beeJs.Topic.fromString(h),remove0x(b)).downloadPayload();return{data:w.payload.toJSON(),index:w.feedIndex}}async fetchChatMessage(u){const{bee:h,chatTopic:b,chatAddress:v}=this.swarmSettings;return(await h.makeFeedReader(beeJs.Topic.fromString(b),remove0x(v)).downloadPayload({index:u})).payload.toJSON()}async sendMessageToGsoc(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(!h)await this.sendMessageToGsocOwn(u);else if(b)await this.sendMessageToGsocEnvelope(u);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async rawSocDownload(u,h){const{beeUrl:b}=this.swarmSettings,v=await fetch(`${b}/soc/${u}/${h}`,{headers:{"swarm-chunk-retrieval-timeout":"2000ms"}});if(!v.ok){const w=new Error(`Failed to fetch: ${u}/${h} [${v.status}]`);throw w.status=v.status,w}return v.text()}async sendMessageToGsocOwn(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,_=new beeJs.PrivateKey(w),E=beeJs.Identifier.fromString(v),S=beeJs.Bytes.fromUtf8(u),{upload:x}=h.makeSOCWriter(_,{timeout:this.UPLOAD_GSOC_TIMEOUT});await x(b,E,S.toUint8Array())}async sendMessageToGsocEnvelope(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,{privateKey:_}=this.userDetails,E=beeJs.Stamper.fromBlank(_,b,this.depth),S=new beeJs.PrivateKey(w),x=beeJs.Identifier.fromString(v),T=beeJs.Bytes.fromUtf8(u),C=makeContentAddressedChunk(T.toUint8Array()),O=makeSingleOwnerChunk(C,x,S),D={hash:()=>O.address.toUint8Array()},U=E.stamp(D),{upload:te}=h.makeSOCWriter(S,{timeout:this.UPLOAD_GSOC_TIMEOUT});await te(U,x,T.toUint8Array())}}class SwarmChat{constructor(u){this.waku=null,this.isExternalWakuNode=!1,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.gsocIndex=null,this.fetchProcessRunning=!1,this.stopFetch=!1;const h=new beeJs.PrivateKey(remove0x(u.user.privateKey));this.userDetails={privateKey:u.user.privateKey,ownAddress:h.publicKey().address().toString(),nickname:u.user.nickname,ownIndex:-1},this.swarmSettings={bee:new beeJs.Bee(u.infra.beeUrl),beeUrl:u.infra.beeUrl,stamp:u.infra.stamp||"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",enveloped:u.infra.enveloped,gsocTopic:u.infra.gsocTopic,gsocResourceId:u.infra.gsocResourceId,chatTopic:u.infra.chatTopic,chatAddress:u.infra.chatAddress,waku:u.infra.waku},this.emitter=new EventEmitter$3,this.utils=new SwarmChatUtils(this.userDetails,this.swarmSettings),this.history=new SwarmHistory(this.utils,this.emitter)}async start(){await this.init(),await this.startMessageFetchProcess()}async startMessageFetchProcess(){const{waku:u}=this.swarmSettings;u!=null&&u.enabled?await this.startWakuMessageFetch(u.node):await this.startPollingMessageFetch()}stop(){this.emitter.cleanAll(),this.stopPollingMessageFetch(),this.history.cleanup(),this.waku&&!this.isExternalWakuNode&&this.waku.stop(),this.waku=null}async startWakuMessageFetch(u){this.logger.info("Waku is enabled"),u&&(this.isExternalWakuNode=!0,this.logger.info("Using external Waku node instance"));const h=this.swarmSettings.chatTopic;this.waku=new Waku(h,b=>{this.emitter.emit(EVENTS.MESSAGE_RECEIVED,b)},u)}getEmitter(){return this.emitter}orderMessages(u){return this.utils.orderMessages(u)}async sendMessage(u,h,b,v,w){const _=this.userDetails.ownIndex===-1?0:this.userDetails.ownIndex+1,E={id:v||v4$1(),username:this.userDetails.nickname,address:this.userDetails.ownAddress,chatTopic:this.swarmSettings.chatTopic,userTopic:this.utils.generateUserOwnedFeedId(this.swarmSettings.chatTopic,this.userDetails.ownAddress),signature:this.getSignature(u),timestamp:Date.now(),index:_,type:h,targetMessageId:b,message:u,additionalProps:w},S=validateMessageWithAdditionalProperties(E);if(!S.isValid){const x=`Invalid message: ${S.errors.join(", ")}`;throw this.logger.error(x),new Error(x)}try{this.emitter.emit(EVENTS.MESSAGE_REQUEST_INITIATED,E),await this.utils.writeOwnFeedDataByIndex(_,JSON.stringify(E)),this.userDetails.ownIndex=_,this.emitter.emit(EVENTS.MESSAGE_REQUEST_UPLOADED,E),await this.broadcastUserMessage(E)}catch(x){this.emitter.emit(EVENTS.MESSAGE_REQUEST_ERROR,E),this.errorHandler.handleError(x,"Chat.sendMessage")}}async fetchPreviousMessages(){try{return this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!0),await this.history.fetchPreviousMessageState()}finally{this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasPreviousMessages()}async retrySendMessage(u){this.sendMessage(u.message,u.type,u.targetMessageId,u.id,u.additionalProps)}async retryBroadcastUserMessage(u){await this.broadcastUserMessage(u)}async init(){try{this.emitter.emit(EVENTS.LOADING_INIT,!0);const[u,h]=await Promise.allSettled([this.initOwnIndex(),this.history.init()]);if(u.status==="rejected")throw u.reason;h.status==="fulfilled"&&(this.gsocIndex=h.value),this.emitter.emit(EVENTS.LOADING_INIT,!1)}catch(u){this.errorHandler.handleError(u,"Chat.initSelfState"),this.emitter.emit(EVENTS.CRITICAL_ERROR,u)}}async initOwnIndex(){const{latestIndex:b}=await retryAwaitableAsync(()=>this.utils.getOwnLatestFeedIndex(),10,1e3);this.userDetails.ownIndex=b}async fetchLatestMessage(){try{if(!this.gsocIndex)return;const u=beeJs.Topic.fromString(this.swarmSettings.chatTopic),h=makeFeedIdentifier(u,this.gsocIndex),b=await this.utils.rawSocDownload(this.swarmSettings.chatAddress,h.toString()),v=JSON.parse(b);if(!validateGsocMessage(v)){this.logger.warn("Invalid GSOC message during fetching");return}this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v.message),this.gsocIndex=this.gsocIndex.next()}catch(u){if(this.utils.isNotFoundError(u))return;this.errorHandler.handleError(u,"Chat.fetchLatestMessage")}}async broadcastUserMessage(u){try{return retryAwaitableAsync(()=>this.utils.sendMessageToGsoc(JSON.stringify(u)))}catch(h){this.errorHandler.handleError(h,"Chat.broadcastUserMessage")}}getSignature(u){const{ownAddress:h,privateKey:b,nickname:v}=this.userDetails,w=new beeJs.EthAddress(h).toString(),_=new beeJs.PrivateKey(b);if(_.publicKey().address().toString()!==w)throw new Error("The provided address does not match the address derived from the private key");const S=Date.now();return _.sign(JSON.stringify({username:v,address:w,message:u,timestamp:S})).toHex()}async startPollingMessageFetch(){if(this.fetchProcessRunning)return;this.fetchProcessRunning=!0,this.stopFetch=!1;const u=async()=>{if(this.stopFetch){this.fetchProcessRunning=!1;return}await this.fetchLatestMessage(),setTimeout(u,200)};u()}stopPollingMessageFetch(){this.stopFetch=!0}}exports.EVENTS=EVENTS;exports.MessageType=MessageType;exports.SwarmChat=SwarmChat;
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/lib/core.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,2BAA2B,EAC3B,YAAY,EAGZ,WAAW,EACX,WAAW,EAEZ,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AASrD,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,kBAAkB,CAAS;IAEnC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAElD,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;gBAEd,QAAQ,EAAE,YAAY;IA2BrB,KAAK;YAKJ,wBAAwB;IAU/B,IAAI;YAYG,qBAAqB;IAwB5B,UAAU;IAIV,aAAa,CAAC,QAAQ,EAAE,GAAG,EAAE;IAIvB,WAAW,CACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,EACjB,eAAe,CAAC,EAAE,MAAM,EACxB,EAAE,CAAC,EAAE,MAAM,EACX,eAAe,CAAC,EAAE,2BAA2B,GAC5C,OAAO,CAAC,IAAI,CAAC;IAwCH,qBAAqB;IAW3B,mBAAmB,IAAI,OAAO;IAIxB,gBAAgB,CAAC,OAAO,EAAE,WAAW;IAIrC,yBAAyB,CAAC,OAAO,EAAE,WAAW;YAI7C,IAAI;YAqBJ,YAAY;YAUZ,kBAAkB;YA4BlB,oBAAoB;IAQlC,OAAO,CAAC,YAAY;YAkBN,wBAAwB;IAmBtC,OAAO,CAAC,uBAAuB;CAGhC"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/lib/core.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,2BAA2B,EAC3B,YAAY,EAGZ,WAAW,EACX,WAAW,EAEZ,MAAM,eAAe,CAAC;AAIvB,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AASrD,qBAAa,SAAS;IACpB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,kBAAkB,CAAS;IAEnC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,KAAK,CAAiB;IAC9B,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,WAAW,CAAmB;IACtC,OAAO,CAAC,aAAa,CAAoB;IAEzC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,YAAY,CAA8B;IAElD,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,SAAS,CAAS;gBAEd,QAAQ,EAAE,YAAY;IA2BrB,KAAK;YAKJ,wBAAwB;IAU/B,IAAI;YAYG,qBAAqB;IAmB5B,UAAU;IAIV,aAAa,CAAC,QAAQ,EAAE,GAAG,EAAE;IAIvB,WAAW,CACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,WAAW,EACjB,eAAe,CAAC,EAAE,MAAM,EACxB,EAAE,CAAC,EAAE,MAAM,EACX,eAAe,CAAC,EAAE,2BAA2B,GAC5C,OAAO,CAAC,IAAI,CAAC;IAwCH,qBAAqB;IAW3B,mBAAmB,IAAI,OAAO;IAIxB,gBAAgB,CAAC,OAAO,EAAE,WAAW;IAIrC,yBAAyB,CAAC,OAAO,EAAE,WAAW;YAI7C,IAAI;YAqBJ,YAAY;YAUZ,kBAAkB;YA4BlB,oBAAoB;IAQlC,OAAO,CAAC,YAAY;YAkBN,wBAAwB;IAmBtC,OAAO,CAAC,uBAAuB;CAGhC"}
@@ -115,4 +115,4 @@ message MessagePayload {
115
115
  MessageData message = 1;
116
116
  repeated MessageStateRef messageStateRefs = 2;
117
117
  }
118
- `;let protoRoot=null,messagePayloadType=null;const errorHandler=ErrorHandler.getInstance();async function initializeProtobuf(){protoRoot||(protoRoot=protobufjs.Root.fromJSON(protobuf.parse(protoSchema).root),messagePayloadType=protoRoot.lookupType("MessagePayload"))}async function decodeMessagePayload(y){if(await initializeProtobuf(),!messagePayloadType)throw new Error("MessagePayload type not initialized");const u={0:"text",1:"thread",2:"reaction"};try{const h=messagePayloadType.decode(y),b=messagePayloadType.toObject(h,{longs:String,enums:String,defaults:!0});if(!b.message)throw new Error("Decoded object has no message field");let v=b.message.additionalProps;if(v&&typeof v=="string")try{v=JSON.parse(v)}catch{console.warn("Failed to parse additionalProps as JSON:",v)}const w={...b.message,type:u[b.message.type]??"text",timestamp:Number(b.message.timestamp),additionalProps:v},_=(b.messageStateRefs||[]).map(E=>({...E,timestamp:Number(E.timestamp)}));return{message:w,messageStateRefs:_}}catch(h){throw errorHandler.handleError(h,"ProtoMessage.decodeMessagePayload"),h}}class Waku{constructor(u,h,b){this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.wakuNode=null,this.handleWakuMessage=async({payload:v})=>{try{if(!v)return this.logger.warn("Received Waku message without payload");const{message:w}=await decodeMessagePayload(v);if(!(w!=null&&w.id))return this.logger.warn("Received invalid message structure via Waku");this.onMessage(w)}catch(w){this.errorHandler.handleError(w,"Waku.handleWakuMessage")}},this.chatTopic=u,this.onMessage=h,this.wakuNode=b||null,this.initPromise=this.init()}async init(){if(this.wakuNode=this.wakuNode??await this.createWakuLightNode(),!await this.subscribeToTopic())throw new Error("Failed to subscribe to Waku topic");this.logger.info(`Subscribed to Waku topic: ${this.chatTopic}`)}async createWakuLightNode(){const u=await createLightNode({defaultBootstrap:!0,networkConfig:{clusterId:WAKU_CLUSTER_ID}});return await u.start(),await u.waitForPeers([Protocols.LightPush,Protocols.Filter],3e4),this.logger.info(`Waku node ready: ${u.libp2p.peerId.toString()}`),u}createWakuDecoder(u){if(!(u!=null&&u.trim()))throw new Error("Topic name must be a non-empty string");const b=createRoutingInfo({clusterId:WAKU_CLUSTER_ID,numShardsInCluster:8},{contentTopic:u});return createDecoder(u,b)}async subscribeToTopic(){var u;if(!((u=this.wakuNode)!=null&&u.isStarted()))throw new Error("Waku node is not running");return this.wakuNode.filter.subscribe(this.createWakuDecoder(this.chatTopic),this.handleWakuMessage)}async isReady(){var u;try{return await this.initPromise,((u=this.wakuNode)==null?void 0:u.isStarted())??!1}catch{return!1}}async stop(){try{await this.initPromise,this.wakuNode&&(await this.wakuNode.stop(),this.wakuNode=null,this.logger.info("Waku node stopped successfully"))}catch(u){throw this.errorHandler.handleError(u,"Waku.stop"),u}}}class SwarmHistory{constructor(u,h){this.utils=u,this.emitter=h,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.processedRefs=new Set,this.refRetryCount=new Map,this.bannedRefs=new Set,this.latestStatefulMessage=null,this.MAX_RETRIES=3,this.RETRY_DELAY=1e3}async init(){try{const{data:u,index:h}=await this.utils.fetchLatestChatMessage();this.latestStatefulMessage=u;try{await this.initMessageState(u)}catch(b){this.errorHandler.handleError(b,"SwarmHistory.initMessageState")}return h}catch(u){return this.errorHandler.handleError(u,"SwarmHistory.init"),beeJs.FeedIndex.fromBigInt(0n)}}async initMessageState(u){try{if(!validateGsocMessage(u)){this.logger.warn("Invalid GSOC message during message state initialization");return}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to initialize");return}const h=this.findLatestRef(u.messageStateRefs);if(!h){this.logger.warn("No valid latest reference found");return}this.logger.debug("Initializing message state with latest ref:",h.reference),await this.processMessageRefWithRetry(h.reference)}catch(h){if(this.utils.isNotFoundError(h)){this.logger.debug("No latest GSOC message found for message state initialization");return}this.errorHandler.handleError(h,"SwarmHistory.init")}}async fetchPreviousMessageState(){let u=this.latestStatefulMessage;if(!u){const{data:b}=await this.utils.fetchLatestChatMessage();u=b,this.latestStatefulMessage=b}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to fetch");return}const h=[...u.messageStateRefs].sort((b,v)=>b.timestamp-v.timestamp);for(const b of h)!this.processedRefs.has(b.reference)&&!this.bannedRefs.has(b.reference)&&(this.logger.debug("Fetching previous message ref:",b.reference),await this.processMessageRefWithRetry(b.reference))}findLatestRef(u){return u.length===0?null:u.reduce((h,b)=>b.timestamp>h.timestamp?b:h)}async processMessageRefWithRetry(u){if(!(this.processedRefs.has(u)||this.bannedRefs.has(u)))try{await this.processMessageRef(u)}catch(h){await this.handleRefError(u,h)}}async processMessageRef(u){const h=await this.utils.downloadObjectFromBee(u);if(!validateMessageState(h))throw new Error(`Invalid message state for ref: ${u}`);this.processedRefs.add(u),this.refRetryCount.delete(u);for(const v of h)this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)}async handleRefError(u,h){const b=this.refRetryCount.get(u)||0,v=b+1;if(this.logger.warn(`Error processing ref ${u} (attempt ${v}/${this.MAX_RETRIES}):`,h),v>=this.MAX_RETRIES)this.bannedRefs.add(u),this.refRetryCount.delete(u),this.logger.error(`Ref ${u} has been banned after ${this.MAX_RETRIES} failed attempts`),this.errorHandler.handleError(h,"SwarmHistory.processMessageRef");else{this.refRetryCount.set(u,v);const w=this.RETRY_DELAY*Math.pow(2,b);this.logger.debug(`Retrying ref ${u} after ${w}ms delay`),await sleep(w),await this.processMessageRefWithRetry(u)}}hasPreviousMessages(){return!!this.latestStatefulMessage&&!!this.latestStatefulMessage.messageStateRefs&&this.latestStatefulMessage.messageStateRefs.length>1}cleanup(){this.processedRefs.clear(),this.refRetryCount.clear(),this.bannedRefs.clear(),this.latestStatefulMessage=null}}class SwarmChatUtils{constructor(u,h){this.userDetails=u,this.swarmSettings=h,this.depth=26,this.errorHandler=ErrorHandler.getInstance(),this.UPLOAD_GSOC_TIMEOUT=2e3}generateUserOwnedFeedId(u,h){return`${u}_EthercastChat_${h}`}isNotFoundError(u){return u.stack.includes("404")||u.message.includes("Not Found")||u.message.includes("404")||u.code===404}async writeOwnFeedDataByIndex(u,h){const{enveloped:b,stamp:v}=this.swarmSettings;if(!b)await this.writeOwnFeedDataByIndexOwned(u,h);else if(v)await this.writeOwnFeedDataByIndexEnvelope(u,h);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async writeOwnFeedDataByIndexOwned(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=this.generateUserOwnedFeedId(w,E),x=beeJs.Topic.fromString(S);await b.makeFeedWriter(x,new beeJs.PrivateKey(_)).uploadPayload(v,JSON.stringify(h),{index:u})}async writeOwnFeedDataByIndexEnvelope(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=new beeJs.PrivateKey(_),x=beeJs.Stamper.fromBlank(_,v,this.depth),T=this.generateUserOwnedFeedId(w,E),C=beeJs.Topic.fromString(T),O=makeFeedIdentifier(C,u),D=makeContentAddressedChunk(h),U=makeSingleOwnerChunk(D,O,S),te={hash:()=>U.address.toUint8Array()},X=x.stamp(te),{upload:ie}=b.makeSOCWriter(S),ee=beeJs.Bytes.fromUtf8(h);return(await ie(X,O,ee.toUint8Array())).reference.toHex()}orderMessages(u){return u.sort((h,b)=>h.timestamp-b.timestamp)}async downloadObjectFromBee(u){try{const{bee:h}=this.swarmSettings,b=await h.downloadData(u);return b?b.toJSON():null}catch(h){return this.errorHandler.handleError(h,"Utils.downloadObjectFromBee"),null}}async uploadObjectToBee(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(h){if(b)return this.uploadObjectToBeeEnvelope(u);throw new Error("Enveloped mode is enabled, but stamp is not provided")}else return this.uploadObjectToBeeOwn(u)}async uploadObjectToBeeOwn(u){try{const{bee:h,stamp:b}=this.swarmSettings;return(await h.uploadData(b,JSON.stringify(u),{redundancyLevel:4})).reference.toString()}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async uploadObjectToBeeEnvelope(u){try{const{bee:h,stamp:b}=this.swarmSettings,{privateKey:v}=this.userDetails,w=beeJs.Stamper.fromBlank(v,b,this.depth),_=beeJs.Bytes.fromUtf8(JSON.stringify(u)),E=new MerkleTree(async x=>{await h.uploadChunk(w.stamp(x),x.build())});await E.append(_.toUint8Array());const S=await E.finalize();return Binary.uint8ArrayToHex(S.hash())}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async getOwnLatestFeedIndex(){try{const{bee:u,chatTopic:h}=this.swarmSettings,{ownAddress:b}=this.userDetails,v=this.generateUserOwnedFeedId(h,b),w=beeJs.Topic.fromString(v),E=await u.makeFeedReader(w,b).downloadPayload(),S=Number(E.feedIndex.toBigInt()),x=S+1;return{latestIndex:S,nextIndex:x}}catch(u){if(this.isNotFoundError(u))return{latestIndex:-1,nextIndex:0};throw u}}async fetchLatestChatMessage(){const{bee:u,chatTopic:h,chatAddress:b}=this.swarmSettings,w=await u.makeFeedReader(beeJs.Topic.fromString(h),remove0x(b)).downloadPayload();return{data:w.payload.toJSON(),index:w.feedIndex}}async fetchChatMessage(u){const{bee:h,chatTopic:b,chatAddress:v}=this.swarmSettings;return(await h.makeFeedReader(beeJs.Topic.fromString(b),remove0x(v)).downloadPayload({index:u})).payload.toJSON()}async sendMessageToGsoc(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(!h)await this.sendMessageToGsocOwn(u);else if(b)await this.sendMessageToGsocEnvelope(u);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async rawSocDownload(u,h){const{beeUrl:b}=this.swarmSettings,v=await fetch(`${b}/soc/${u}/${h}`,{headers:{"swarm-chunk-retrieval-timeout":"2000ms"}});if(!v.ok){const w=new Error(`Failed to fetch: ${u}/${h} [${v.status}]`);throw w.status=v.status,w}return v.text()}async sendMessageToGsocOwn(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,_=new beeJs.PrivateKey(w),E=beeJs.Identifier.fromString(v),S=beeJs.Bytes.fromUtf8(u),{upload:x}=h.makeSOCWriter(_,{timeout:this.UPLOAD_GSOC_TIMEOUT});await x(b,E,S.toUint8Array())}async sendMessageToGsocEnvelope(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,{privateKey:_}=this.userDetails,E=beeJs.Stamper.fromBlank(_,b,this.depth),S=new beeJs.PrivateKey(w),x=beeJs.Identifier.fromString(v),T=beeJs.Bytes.fromUtf8(u),C=makeContentAddressedChunk(T.toUint8Array()),O=makeSingleOwnerChunk(C,x,S),D={hash:()=>O.address.toUint8Array()},U=E.stamp(D),{upload:te}=h.makeSOCWriter(S,{timeout:this.UPLOAD_GSOC_TIMEOUT});await te(U,x,T.toUint8Array())}}class SwarmChat{constructor(u){this.waku=null,this.isExternalWakuNode=!1,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.gsocIndex=null,this.fetchProcessRunning=!1,this.stopFetch=!1;const h=new beeJs.PrivateKey(remove0x(u.user.privateKey));this.userDetails={privateKey:u.user.privateKey,ownAddress:h.publicKey().address().toString(),nickname:u.user.nickname,ownIndex:-1},this.swarmSettings={bee:new beeJs.Bee(u.infra.beeUrl),beeUrl:u.infra.beeUrl,stamp:u.infra.stamp||"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",enveloped:u.infra.enveloped,gsocTopic:u.infra.gsocTopic,gsocResourceId:u.infra.gsocResourceId,chatTopic:u.infra.chatTopic,chatAddress:u.infra.chatAddress,waku:u.infra.waku},this.emitter=new EventEmitter$3,this.utils=new SwarmChatUtils(this.userDetails,this.swarmSettings),this.history=new SwarmHistory(this.utils,this.emitter)}async start(){await this.init(),await this.startMessageFetchProcess()}async startMessageFetchProcess(){const{waku:u}=this.swarmSettings;u!=null&&u.enabled?await this.startWakuMessageFetch(u.node):await this.startPollingMessageFetch()}stop(){this.emitter.cleanAll(),this.stopPollingMessageFetch(),this.history.cleanup(),this.waku&&!this.isExternalWakuNode&&this.waku.stop(),this.waku=null}async startWakuMessageFetch(u){this.logger.info("Waku is enabled"),u&&(this.isExternalWakuNode=!0,this.logger.info("Using external Waku node instance"));const h=this.swarmSettings.chatTopic;if(this.waku=new Waku(h,v=>{this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)},u),!await this.waku.isReady())throw new Error("Waku node is not reachable")}getEmitter(){return this.emitter}orderMessages(u){return this.utils.orderMessages(u)}async sendMessage(u,h,b,v,w){const _=this.userDetails.ownIndex===-1?0:this.userDetails.ownIndex+1,E={id:v||v4$1(),username:this.userDetails.nickname,address:this.userDetails.ownAddress,chatTopic:this.swarmSettings.chatTopic,userTopic:this.utils.generateUserOwnedFeedId(this.swarmSettings.chatTopic,this.userDetails.ownAddress),signature:this.getSignature(u),timestamp:Date.now(),index:_,type:h,targetMessageId:b,message:u,additionalProps:w},S=validateMessageWithAdditionalProperties(E);if(!S.isValid){const x=`Invalid message: ${S.errors.join(", ")}`;throw this.logger.error(x),new Error(x)}try{this.emitter.emit(EVENTS.MESSAGE_REQUEST_INITIATED,E),await this.utils.writeOwnFeedDataByIndex(_,JSON.stringify(E)),this.userDetails.ownIndex=_,this.emitter.emit(EVENTS.MESSAGE_REQUEST_UPLOADED,E),await this.broadcastUserMessage(E)}catch(x){this.emitter.emit(EVENTS.MESSAGE_REQUEST_ERROR,E),this.errorHandler.handleError(x,"Chat.sendMessage")}}async fetchPreviousMessages(){try{return this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!0),await this.history.fetchPreviousMessageState()}finally{this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasPreviousMessages()}async retrySendMessage(u){this.sendMessage(u.message,u.type,u.targetMessageId,u.id,u.additionalProps)}async retryBroadcastUserMessage(u){await this.broadcastUserMessage(u)}async init(){try{this.emitter.emit(EVENTS.LOADING_INIT,!0);const[u,h]=await Promise.allSettled([this.initOwnIndex(),this.history.init()]);if(u.status==="rejected")throw u.reason;h.status==="fulfilled"&&(this.gsocIndex=h.value),this.emitter.emit(EVENTS.LOADING_INIT,!1)}catch(u){this.errorHandler.handleError(u,"Chat.initSelfState"),this.emitter.emit(EVENTS.CRITICAL_ERROR,u)}}async initOwnIndex(){const{latestIndex:b}=await retryAwaitableAsync(()=>this.utils.getOwnLatestFeedIndex(),10,1e3);this.userDetails.ownIndex=b}async fetchLatestMessage(){try{if(!this.gsocIndex)return;const u=beeJs.Topic.fromString(this.swarmSettings.chatTopic),h=makeFeedIdentifier(u,this.gsocIndex),b=await this.utils.rawSocDownload(this.swarmSettings.chatAddress,h.toString()),v=JSON.parse(b);if(!validateGsocMessage(v)){this.logger.warn("Invalid GSOC message during fetching");return}this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v.message),this.gsocIndex=this.gsocIndex.next()}catch(u){if(this.utils.isNotFoundError(u))return;this.errorHandler.handleError(u,"Chat.fetchLatestMessage")}}async broadcastUserMessage(u){try{return retryAwaitableAsync(()=>this.utils.sendMessageToGsoc(JSON.stringify(u)))}catch(h){this.errorHandler.handleError(h,"Chat.broadcastUserMessage")}}getSignature(u){const{ownAddress:h,privateKey:b,nickname:v}=this.userDetails,w=new beeJs.EthAddress(h).toString(),_=new beeJs.PrivateKey(b);if(_.publicKey().address().toString()!==w)throw new Error("The provided address does not match the address derived from the private key");const S=Date.now();return _.sign(JSON.stringify({username:v,address:w,message:u,timestamp:S})).toHex()}async startPollingMessageFetch(){if(this.fetchProcessRunning)return;this.fetchProcessRunning=!0,this.stopFetch=!1;const u=async()=>{if(this.stopFetch){this.fetchProcessRunning=!1;return}await this.fetchLatestMessage(),setTimeout(u,200)};u()}stopPollingMessageFetch(){this.stopFetch=!0}}exports.EVENTS=EVENTS,exports.MessageType=MessageType,exports.SwarmChat=SwarmChat,Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"})});
118
+ `;let protoRoot=null,messagePayloadType=null;const errorHandler=ErrorHandler.getInstance();async function initializeProtobuf(){protoRoot||(protoRoot=protobufjs.Root.fromJSON(protobuf.parse(protoSchema).root),messagePayloadType=protoRoot.lookupType("MessagePayload"))}async function decodeMessagePayload(y){if(await initializeProtobuf(),!messagePayloadType)throw new Error("MessagePayload type not initialized");const u={0:"text",1:"thread",2:"reaction"};try{const h=messagePayloadType.decode(y),b=messagePayloadType.toObject(h,{longs:String,enums:String,defaults:!0});if(!b.message)throw new Error("Decoded object has no message field");let v=b.message.additionalProps;if(v&&typeof v=="string")try{v=JSON.parse(v)}catch{console.warn("Failed to parse additionalProps as JSON:",v)}const w={...b.message,type:u[b.message.type]??"text",timestamp:Number(b.message.timestamp),additionalProps:v},_=(b.messageStateRefs||[]).map(E=>({...E,timestamp:Number(E.timestamp)}));return{message:w,messageStateRefs:_}}catch(h){throw errorHandler.handleError(h,"ProtoMessage.decodeMessagePayload"),h}}class Waku{constructor(u,h,b){this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.wakuNode=null,this.handleWakuMessage=async({payload:v})=>{try{if(!v)return this.logger.warn("Received Waku message without payload");const{message:w}=await decodeMessagePayload(v);if(!(w!=null&&w.id))return this.logger.warn("Received invalid message structure via Waku");this.onMessage(w)}catch(w){this.errorHandler.handleError(w,"Waku.handleWakuMessage")}},this.chatTopic=u,this.onMessage=h,this.wakuNode=b||null,this.initPromise=this.init()}async init(){if(this.wakuNode=this.wakuNode??await this.createWakuLightNode(),!await this.subscribeToTopic())throw new Error("Failed to subscribe to Waku topic");this.logger.info(`Subscribed to Waku topic: ${this.chatTopic}`)}async createWakuLightNode(){const u=await createLightNode({defaultBootstrap:!0,networkConfig:{clusterId:WAKU_CLUSTER_ID}});return await u.start(),await u.waitForPeers([Protocols.LightPush,Protocols.Filter],3e4),this.logger.info(`Waku node ready: ${u.libp2p.peerId.toString()}`),u}createWakuDecoder(u){if(!(u!=null&&u.trim()))throw new Error("Topic name must be a non-empty string");const h={clusterId:WAKU_CLUSTER_ID,numShardsInCluster:8},b=`/solarpunk-msrs/1/${u}/proto`,v=createRoutingInfo(h,{contentTopic:b});return createDecoder(b,v)}async subscribeToTopic(){var u;if(!((u=this.wakuNode)!=null&&u.isStarted()))throw new Error("Waku node is not running");return this.wakuNode.filter.subscribe(this.createWakuDecoder(this.chatTopic),this.handleWakuMessage)}async isReady(){var u;try{return await this.initPromise,((u=this.wakuNode)==null?void 0:u.isStarted())??!1}catch{return!1}}async stop(){try{await this.initPromise,this.wakuNode&&(await this.wakuNode.stop(),this.wakuNode=null,this.logger.info("Waku node stopped successfully"))}catch(u){throw this.errorHandler.handleError(u,"Waku.stop"),u}}}class SwarmHistory{constructor(u,h){this.utils=u,this.emitter=h,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.processedRefs=new Set,this.refRetryCount=new Map,this.bannedRefs=new Set,this.latestStatefulMessage=null,this.MAX_RETRIES=3,this.RETRY_DELAY=1e3}async init(){try{const{data:u,index:h}=await this.utils.fetchLatestChatMessage();this.latestStatefulMessage=u;try{await this.initMessageState(u)}catch(b){this.errorHandler.handleError(b,"SwarmHistory.initMessageState")}return h}catch(u){return this.errorHandler.handleError(u,"SwarmHistory.init"),beeJs.FeedIndex.fromBigInt(0n)}}async initMessageState(u){try{if(!validateGsocMessage(u)){this.logger.warn("Invalid GSOC message during message state initialization");return}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to initialize");return}const h=this.findLatestRef(u.messageStateRefs);if(!h){this.logger.warn("No valid latest reference found");return}this.logger.debug("Initializing message state with latest ref:",h.reference),await this.processMessageRefWithRetry(h.reference)}catch(h){if(this.utils.isNotFoundError(h)){this.logger.debug("No latest GSOC message found for message state initialization");return}this.errorHandler.handleError(h,"SwarmHistory.init")}}async fetchPreviousMessageState(){let u=this.latestStatefulMessage;if(!u){const{data:b}=await this.utils.fetchLatestChatMessage();u=b,this.latestStatefulMessage=b}if(!u.messageStateRefs||u.messageStateRefs.length===0){this.logger.debug("No message state refs to fetch");return}const h=[...u.messageStateRefs].sort((b,v)=>b.timestamp-v.timestamp);for(const b of h)!this.processedRefs.has(b.reference)&&!this.bannedRefs.has(b.reference)&&(this.logger.debug("Fetching previous message ref:",b.reference),await this.processMessageRefWithRetry(b.reference))}findLatestRef(u){return u.length===0?null:u.reduce((h,b)=>b.timestamp>h.timestamp?b:h)}async processMessageRefWithRetry(u){if(!(this.processedRefs.has(u)||this.bannedRefs.has(u)))try{await this.processMessageRef(u)}catch(h){await this.handleRefError(u,h)}}async processMessageRef(u){const h=await this.utils.downloadObjectFromBee(u);if(!validateMessageState(h))throw new Error(`Invalid message state for ref: ${u}`);this.processedRefs.add(u),this.refRetryCount.delete(u);for(const v of h)this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v)}async handleRefError(u,h){const b=this.refRetryCount.get(u)||0,v=b+1;if(this.logger.warn(`Error processing ref ${u} (attempt ${v}/${this.MAX_RETRIES}):`,h),v>=this.MAX_RETRIES)this.bannedRefs.add(u),this.refRetryCount.delete(u),this.logger.error(`Ref ${u} has been banned after ${this.MAX_RETRIES} failed attempts`),this.errorHandler.handleError(h,"SwarmHistory.processMessageRef");else{this.refRetryCount.set(u,v);const w=this.RETRY_DELAY*Math.pow(2,b);this.logger.debug(`Retrying ref ${u} after ${w}ms delay`),await sleep(w),await this.processMessageRefWithRetry(u)}}hasPreviousMessages(){return!!this.latestStatefulMessage&&!!this.latestStatefulMessage.messageStateRefs&&this.latestStatefulMessage.messageStateRefs.length>1}cleanup(){this.processedRefs.clear(),this.refRetryCount.clear(),this.bannedRefs.clear(),this.latestStatefulMessage=null}}class SwarmChatUtils{constructor(u,h){this.userDetails=u,this.swarmSettings=h,this.depth=26,this.errorHandler=ErrorHandler.getInstance(),this.UPLOAD_GSOC_TIMEOUT=2e3}generateUserOwnedFeedId(u,h){return`${u}_EthercastChat_${h}`}isNotFoundError(u){return u.stack.includes("404")||u.message.includes("Not Found")||u.message.includes("404")||u.code===404}async writeOwnFeedDataByIndex(u,h){const{enveloped:b,stamp:v}=this.swarmSettings;if(!b)await this.writeOwnFeedDataByIndexOwned(u,h);else if(v)await this.writeOwnFeedDataByIndexEnvelope(u,h);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async writeOwnFeedDataByIndexOwned(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=this.generateUserOwnedFeedId(w,E),x=beeJs.Topic.fromString(S);await b.makeFeedWriter(x,new beeJs.PrivateKey(_)).uploadPayload(v,JSON.stringify(h),{index:u})}async writeOwnFeedDataByIndexEnvelope(u,h){const{bee:b,stamp:v,chatTopic:w}=this.swarmSettings,{privateKey:_,ownAddress:E}=this.userDetails,S=new beeJs.PrivateKey(_),x=beeJs.Stamper.fromBlank(_,v,this.depth),T=this.generateUserOwnedFeedId(w,E),C=beeJs.Topic.fromString(T),O=makeFeedIdentifier(C,u),D=makeContentAddressedChunk(h),U=makeSingleOwnerChunk(D,O,S),te={hash:()=>U.address.toUint8Array()},X=x.stamp(te),{upload:ie}=b.makeSOCWriter(S),ee=beeJs.Bytes.fromUtf8(h);return(await ie(X,O,ee.toUint8Array())).reference.toHex()}orderMessages(u){return u.sort((h,b)=>h.timestamp-b.timestamp)}async downloadObjectFromBee(u){try{const{bee:h}=this.swarmSettings,b=await h.downloadData(u);return b?b.toJSON():null}catch(h){return this.errorHandler.handleError(h,"Utils.downloadObjectFromBee"),null}}async uploadObjectToBee(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(h){if(b)return this.uploadObjectToBeeEnvelope(u);throw new Error("Enveloped mode is enabled, but stamp is not provided")}else return this.uploadObjectToBeeOwn(u)}async uploadObjectToBeeOwn(u){try{const{bee:h,stamp:b}=this.swarmSettings;return(await h.uploadData(b,JSON.stringify(u),{redundancyLevel:4})).reference.toString()}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async uploadObjectToBeeEnvelope(u){try{const{bee:h,stamp:b}=this.swarmSettings,{privateKey:v}=this.userDetails,w=beeJs.Stamper.fromBlank(v,b,this.depth),_=beeJs.Bytes.fromUtf8(JSON.stringify(u)),E=new MerkleTree(async x=>{await h.uploadChunk(w.stamp(x),x.build())});await E.append(_.toUint8Array());const S=await E.finalize();return Binary.uint8ArrayToHex(S.hash())}catch(h){return this.errorHandler.handleError(h,"Utils.uploadObjectToBee"),null}}async getOwnLatestFeedIndex(){try{const{bee:u,chatTopic:h}=this.swarmSettings,{ownAddress:b}=this.userDetails,v=this.generateUserOwnedFeedId(h,b),w=beeJs.Topic.fromString(v),E=await u.makeFeedReader(w,b).downloadPayload(),S=Number(E.feedIndex.toBigInt()),x=S+1;return{latestIndex:S,nextIndex:x}}catch(u){if(this.isNotFoundError(u))return{latestIndex:-1,nextIndex:0};throw u}}async fetchLatestChatMessage(){const{bee:u,chatTopic:h,chatAddress:b}=this.swarmSettings,w=await u.makeFeedReader(beeJs.Topic.fromString(h),remove0x(b)).downloadPayload();return{data:w.payload.toJSON(),index:w.feedIndex}}async fetchChatMessage(u){const{bee:h,chatTopic:b,chatAddress:v}=this.swarmSettings;return(await h.makeFeedReader(beeJs.Topic.fromString(b),remove0x(v)).downloadPayload({index:u})).payload.toJSON()}async sendMessageToGsoc(u){const{enveloped:h,stamp:b}=this.swarmSettings;if(!h)await this.sendMessageToGsocOwn(u);else if(b)await this.sendMessageToGsocEnvelope(u);else throw new Error("Enveloped mode is enabled, but stamp is not provided")}async rawSocDownload(u,h){const{beeUrl:b}=this.swarmSettings,v=await fetch(`${b}/soc/${u}/${h}`,{headers:{"swarm-chunk-retrieval-timeout":"2000ms"}});if(!v.ok){const w=new Error(`Failed to fetch: ${u}/${h} [${v.status}]`);throw w.status=v.status,w}return v.text()}async sendMessageToGsocOwn(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,_=new beeJs.PrivateKey(w),E=beeJs.Identifier.fromString(v),S=beeJs.Bytes.fromUtf8(u),{upload:x}=h.makeSOCWriter(_,{timeout:this.UPLOAD_GSOC_TIMEOUT});await x(b,E,S.toUint8Array())}async sendMessageToGsocEnvelope(u){const{bee:h,stamp:b,gsocTopic:v,gsocResourceId:w}=this.swarmSettings,{privateKey:_}=this.userDetails,E=beeJs.Stamper.fromBlank(_,b,this.depth),S=new beeJs.PrivateKey(w),x=beeJs.Identifier.fromString(v),T=beeJs.Bytes.fromUtf8(u),C=makeContentAddressedChunk(T.toUint8Array()),O=makeSingleOwnerChunk(C,x,S),D={hash:()=>O.address.toUint8Array()},U=E.stamp(D),{upload:te}=h.makeSOCWriter(S,{timeout:this.UPLOAD_GSOC_TIMEOUT});await te(U,x,T.toUint8Array())}}class SwarmChat{constructor(u){this.waku=null,this.isExternalWakuNode=!1,this.logger=Logger$2.getInstance(),this.errorHandler=ErrorHandler.getInstance(),this.gsocIndex=null,this.fetchProcessRunning=!1,this.stopFetch=!1;const h=new beeJs.PrivateKey(remove0x(u.user.privateKey));this.userDetails={privateKey:u.user.privateKey,ownAddress:h.publicKey().address().toString(),nickname:u.user.nickname,ownIndex:-1},this.swarmSettings={bee:new beeJs.Bee(u.infra.beeUrl),beeUrl:u.infra.beeUrl,stamp:u.infra.stamp||"0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",enveloped:u.infra.enveloped,gsocTopic:u.infra.gsocTopic,gsocResourceId:u.infra.gsocResourceId,chatTopic:u.infra.chatTopic,chatAddress:u.infra.chatAddress,waku:u.infra.waku},this.emitter=new EventEmitter$3,this.utils=new SwarmChatUtils(this.userDetails,this.swarmSettings),this.history=new SwarmHistory(this.utils,this.emitter)}async start(){await this.init(),await this.startMessageFetchProcess()}async startMessageFetchProcess(){const{waku:u}=this.swarmSettings;u!=null&&u.enabled?await this.startWakuMessageFetch(u.node):await this.startPollingMessageFetch()}stop(){this.emitter.cleanAll(),this.stopPollingMessageFetch(),this.history.cleanup(),this.waku&&!this.isExternalWakuNode&&this.waku.stop(),this.waku=null}async startWakuMessageFetch(u){this.logger.info("Waku is enabled"),u&&(this.isExternalWakuNode=!0,this.logger.info("Using external Waku node instance"));const h=this.swarmSettings.chatTopic;this.waku=new Waku(h,b=>{this.emitter.emit(EVENTS.MESSAGE_RECEIVED,b)},u)}getEmitter(){return this.emitter}orderMessages(u){return this.utils.orderMessages(u)}async sendMessage(u,h,b,v,w){const _=this.userDetails.ownIndex===-1?0:this.userDetails.ownIndex+1,E={id:v||v4$1(),username:this.userDetails.nickname,address:this.userDetails.ownAddress,chatTopic:this.swarmSettings.chatTopic,userTopic:this.utils.generateUserOwnedFeedId(this.swarmSettings.chatTopic,this.userDetails.ownAddress),signature:this.getSignature(u),timestamp:Date.now(),index:_,type:h,targetMessageId:b,message:u,additionalProps:w},S=validateMessageWithAdditionalProperties(E);if(!S.isValid){const x=`Invalid message: ${S.errors.join(", ")}`;throw this.logger.error(x),new Error(x)}try{this.emitter.emit(EVENTS.MESSAGE_REQUEST_INITIATED,E),await this.utils.writeOwnFeedDataByIndex(_,JSON.stringify(E)),this.userDetails.ownIndex=_,this.emitter.emit(EVENTS.MESSAGE_REQUEST_UPLOADED,E),await this.broadcastUserMessage(E)}catch(x){this.emitter.emit(EVENTS.MESSAGE_REQUEST_ERROR,E),this.errorHandler.handleError(x,"Chat.sendMessage")}}async fetchPreviousMessages(){try{return this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!0),await this.history.fetchPreviousMessageState()}finally{this.emitter.emit(EVENTS.LOADING_PREVIOUS_MESSAGES,!1)}}hasPreviousMessages(){return this.history.hasPreviousMessages()}async retrySendMessage(u){this.sendMessage(u.message,u.type,u.targetMessageId,u.id,u.additionalProps)}async retryBroadcastUserMessage(u){await this.broadcastUserMessage(u)}async init(){try{this.emitter.emit(EVENTS.LOADING_INIT,!0);const[u,h]=await Promise.allSettled([this.initOwnIndex(),this.history.init()]);if(u.status==="rejected")throw u.reason;h.status==="fulfilled"&&(this.gsocIndex=h.value),this.emitter.emit(EVENTS.LOADING_INIT,!1)}catch(u){this.errorHandler.handleError(u,"Chat.initSelfState"),this.emitter.emit(EVENTS.CRITICAL_ERROR,u)}}async initOwnIndex(){const{latestIndex:b}=await retryAwaitableAsync(()=>this.utils.getOwnLatestFeedIndex(),10,1e3);this.userDetails.ownIndex=b}async fetchLatestMessage(){try{if(!this.gsocIndex)return;const u=beeJs.Topic.fromString(this.swarmSettings.chatTopic),h=makeFeedIdentifier(u,this.gsocIndex),b=await this.utils.rawSocDownload(this.swarmSettings.chatAddress,h.toString()),v=JSON.parse(b);if(!validateGsocMessage(v)){this.logger.warn("Invalid GSOC message during fetching");return}this.emitter.emit(EVENTS.MESSAGE_RECEIVED,v.message),this.gsocIndex=this.gsocIndex.next()}catch(u){if(this.utils.isNotFoundError(u))return;this.errorHandler.handleError(u,"Chat.fetchLatestMessage")}}async broadcastUserMessage(u){try{return retryAwaitableAsync(()=>this.utils.sendMessageToGsoc(JSON.stringify(u)))}catch(h){this.errorHandler.handleError(h,"Chat.broadcastUserMessage")}}getSignature(u){const{ownAddress:h,privateKey:b,nickname:v}=this.userDetails,w=new beeJs.EthAddress(h).toString(),_=new beeJs.PrivateKey(b);if(_.publicKey().address().toString()!==w)throw new Error("The provided address does not match the address derived from the private key");const S=Date.now();return _.sign(JSON.stringify({username:v,address:w,message:u,timestamp:S})).toHex()}async startPollingMessageFetch(){if(this.fetchProcessRunning)return;this.fetchProcessRunning=!0,this.stopFetch=!1;const u=async()=>{if(this.stopFetch){this.fetchProcessRunning=!1;return}await this.fetchLatestMessage(),setTimeout(u,200)};u()}stopPollingMessageFetch(){this.stopFetch=!0}}exports.EVENTS=EVENTS,exports.MessageType=MessageType,exports.SwarmChat=SwarmChat,Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"})});
@@ -1 +1 @@
1
- {"version":3,"file":"Waku.d.ts","sourceRoot":"","sources":["../../src/waku/Waku.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4D,KAAK,SAAS,EAAa,MAAM,WAAW,CAAC;AAGhH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAMvD,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAA0B;IAE1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,WAAW,CAAgB;gBAEvB,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;YAOxE,IAAI;YAMJ,mBAAmB;IAajC,OAAO,CAAC,iBAAiB;YAaX,gBAAgB;IAK9B,OAAO,CAAC,iBAAiB,CAWvB;IAEW,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAS3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAanC"}
1
+ {"version":3,"file":"Waku.d.ts","sourceRoot":"","sources":["../../src/waku/Waku.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4D,KAAK,SAAS,EAAa,MAAM,WAAW,CAAC;AAGhH,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAMvD,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAC3D,OAAO,CAAC,QAAQ,CAA0B;IAE1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;IACvD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,WAAW,CAAgB;gBAEvB,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE,SAAS;YAOxE,IAAI;YAMJ,mBAAmB;IAajC,OAAO,CAAC,iBAAiB;YAcX,gBAAgB;IAK9B,OAAO,CAAC,iBAAiB,CAWvB;IAEW,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAS3B,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAanC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solarpunkltd/swarm-chat-js",
3
- "version": "6.0.5",
3
+ "version": "6.0.7",
4
4
  "description": "JS library for chatting on Swarm",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -34,9 +34,7 @@
34
34
  "cafe-utility": "^27.12.1",
35
35
  "protobufjs": "^7.5.4",
36
36
  "uuid": "^11.0.3",
37
- "zod": "^3.24.1",
38
- "@waku/sdk": "^0.0.35",
39
- "protobufjs": "^7.5.4"
37
+ "zod": "^3.24.1"
40
38
  },
41
39
  "devDependencies": {
42
40
  "@types/node": "^22.7.5",