1class NotificationService { 2 projectId; 3 projectKey; 4 userId; 5 channelId; 6 socket; 7 baseUrl = "https://notification-service.softfusion.dev"; 8 baseSocketUrl = "https://notification-service.softfusion.dev"; 9 allowedConfigVars = ["projectId", "projectKey", "userId", "channelId", "userAuth"]; 10 connected = false; 11 queuedActions = []; 12 subscribedChannels = []; 13 resubscribe = false; 14 userAuth; 15 16 constructor(configObj) { 17 Object.keys(configObj).forEach(key => { 18 if (this.allowedConfigVars.includes(key)) { 19 this[key] = configObj[key]; 20 } else { 21 console.warn(`${key} is not a valid config var`); 22 } 23 }); 24 25 if (!("channelId" in configObj)) { 26 this.channelId = configObj.userId; 27 } 28 } 29 30 async connect() { 31 // Fetch project settings 32 const projectSettingsRes = await fetch(`${this.baseUrl}/project/settings/${this.projectId}`); 33 const projectSettings = await projectSettingsRes.json(); 34 35 if (!projectSettings) { 36 throw new Error("Failed to fetch project meta"); 37 } 38 39 // Run developer's auth endpoint 40 const runtimeParams = this.userAuth?.params || {}; 41 const userAuthParamString = new URLSearchParams({ 42 ...projectSettings.userAuthConfig.params, 43 ...runtimeParams, 44 userId: this.userId 45 }).toString(); 46 47 const userAuthRes = await fetch(`${projectSettings.userAuthConfig.url}${this.userAuth.endpoint}/?${userAuthParamString}`, { 48 credentials: this.userAuth.credentials, 49 headers: this.userAuth.headers ? this.userAuth.headers : {}, 50 }); 51 const userAuth = await userAuthRes.json(); 52 53 if (userAuth.status !== "success") { 54 throw new Error("User verification failed. userId has failed verification"); 55 } 56 57 // Init socket 58 this.socket = io(this.baseSocketUrl, { 59 auth: { 60 token: this.projectKey, 61 userAuthToken: userAuth.token 62 }, 63 query: { 64 userId: this.userId, 65 channelId: this.channelId, 66 // channelId: `${this.projectId}-${this.channelId}`, 67 } 68 }); 69 70 this.socket.on("connect", () => { 71 this.connected = true; 72 73 // If reconnecting, requeue subscriptions 74 if (this.resubscribe && this.subscribedChannels.length > 0) { 75 this.queueAction("subscribe", this.subscribedChannels); 76 } 77 78 this.replayQueuedActions(); 79 }); 80 81 this.socket.on("connect_error", (err) => { 82 console.error("Connection failed:", err); 83 this.resubscribe = true; 84 }); 85 86 this.socket.on("error", (err) => { 87 console.error("Error:", err); 88 this.resubscribe = true; 89 }); 90 91 this.socket.on("subscribe_error", (err) => { 92 console.log("Error: ", err); 93 }) 94 95 // Promise interface 96 return new Promise((resolve, reject) => { 97 this.socket.once("connect", resolve); 98 this.socket.once("connect_error", reject); 99 }); 100 } 101 102 // ---------- Queue System ---------- 103 queueAction(type, payload) { 104 const exists = this.queuedActions.find( 105 a => a.type === type && JSON.stringify(a.payload) === JSON.stringify(payload) 106 ); 107 if (!exists) { 108 this.queuedActions.push({ type, payload }); 109 } 110 } 111 112 replayQueuedActions() { 113 this.queuedActions.forEach(action => { 114 switch (action.type) { 115 case "subscribe": 116 this._subscribeNow(action.payload); 117 break; 118 case "sendDirect": 119 this._sendDirectNow(action.payload); 120 break; 121 case "sendGroup": 122 this._sendGroupNow(action.payload); 123 break; 124 case "addListener": 125 this._addListenerNow(action.payload); 126 break; 127 default: 128 console.warn("Unknown action type:", action.type); 129 } 130 }); 131 132 this.queuedActions = []; 133 } 134 135 // ---------- Public API ---------- 136 subscribe(channels) { 137 if (!Array.isArray(channels)) channels = [channels]; 138 139 if (!this.connected) { 140 this.queueAction("subscribe", channels); 141 } else { 142 this._subscribeNow(channels); 143 } 144 145 // Track subscriptions 146 this.subscribedChannels.push(...channels); 147 this.subscribedChannels = [...new Set(this.subscribedChannels)]; 148 } 149 150 addNotificationListener(listenerCallback) { 151 const payload = { callback: listenerCallback }; 152 153 if (!this.connected) { 154 this.queueAction("addListener", payload); 155 } else { 156 this._addListenerNow(payload); 157 } 158 } 159 160 sendDirectNotification(channel, message) { 161 const payload = { channel, message }; 162 163 if (!this.connected) { 164 this.queueAction("sendDirect", payload); 165 } else { 166 this._sendDirectNow(payload); 167 } 168 } 169 170 sendGroupNotification(message) { 171 const payload = { message }; 172 173 if (!this.connected) { 174 this.queueAction("sendGroup", payload); 175 } else { 176 this._sendGroupNow(payload); 177 } 178 } 179 180 fetchNotifications(channelId) { 181 return fetch(`${this.baseUrl}/notifications/${this.projectId}?channelId=${channelId}`); 182 } 183 184 // ---------- Private Immediate Methods ---------- 185 _subscribeNow(channels) {
186 channels.forEach(channel => { 187 this.socket.emit("join-channel", { channelId: channel }); 188 }); 189 } 190 191 _addListenerNow(payload) { 192 this.socket.on("receive-message", (data) => payload.callback(data)); 193 } 194 195 _sendDirectNow(payload) { 196 this.socket.emit("send-direct-message", { 197 message: payload.message, 198 recepientChannelId: payload.channel 199 }); 200 } 201 202 _sendGroupNow(payload) { 203 this.socket.emit("send-group-message", { message: payload.message, channelId: this.channelId, senderUserId: this.userId }); 204 } 205}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.