Skip to content

Commit 2e11e6a

Browse files
authored
Merge pull request #86 from customerio/feature/INAPP-13259
Adds realtime through SSE
2 parents 0e569e0 + 25491a0 commit 2e11e6a

File tree

7 files changed

+140
-15
lines changed

7 files changed

+140
-15
lines changed

src/gist.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import EventEmitter from "./utilities/event-emitter";
22
import { log } from "./utilities/log";
3-
import { startQueueListener, checkMessageQueue } from "./managers/queue-manager";
3+
import { startQueueListener, checkMessageQueue, stopSSEListener } from "./managers/queue-manager";
44
import { setUserToken, clearUserToken, useGuestSession } from "./managers/user-manager";
55
import { showMessage, embedMessage, hideMessage, removePersistentMessage, fetchMessageByInstanceId, logBroadcastDismissedLocally } from "./managers/message-manager";
66
import { setUserLocale } from "./managers/locale-manager";
@@ -63,6 +63,7 @@ export default class {
6363
if (this.config.useAnonymousSession) {
6464
useGuestSession();
6565
}
66+
stopSSEListener();
6667
await startQueueListener();
6768
}
6869

src/managers/message-manager.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@ function resetEmbedState(message) {
9898
}
9999

100100
async function resetOverlayState(hideFirst, message) {
101-
removeMessageByInstanceId(message.instanceId);
102-
Gist.overlayInstanceId = null;
103101
if (hideFirst) {
104102
await hideOverlayComponent();
105103
} else {
@@ -110,6 +108,9 @@ async function resetOverlayState(hideFirst, message) {
110108
window.removeEventListener('message', handleGistEvents);
111109
window.removeEventListener('touchstart', handleTouchStartEvents);
112110
}
111+
112+
removeMessageByInstanceId(message.instanceId);
113+
Gist.overlayInstanceId = null;
113114
}
114115

115116
function loadMessageComponent(message, elementId = null) {

src/managers/queue-manager.js

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import Gist from '../gist';
22
import { log } from "../utilities/log";
3-
import { getUserToken } from "./user-manager";
4-
import { getUserQueue, userQueueNextPullCheckLocalStoreName } from "../services/queue-service";
3+
import { getUserToken, isAnonymousUser } from "./user-manager";
4+
import { getUserQueue, getQueueSSEEndpoint, userQueueNextPullCheckLocalStoreName } from "../services/queue-service";
55
import { showMessage, embedMessage } from "./message-manager";
66
import { resolveMessageProperties } from "./gist-properties-manager";
77
import { getKeyFromLocalStore } from '../utilities/local-storage';
88
import { updateBroadcastsLocalStore, getEligibleBroadcasts, isShowAlwaysBroadcast } from './message-broadcast-manager';
99
import { updateQueueLocalStore, getMessagesFromLocalStore, isMessageLoading, setMessageLoading } from './message-user-queue-manager';
10+
import { settings } from '../services/settings';
1011

1112
var sleep = time => new Promise(resolve => setTimeout(resolve, time))
1213
var poll = (promiseFn, time) => promiseFn().then(sleep(time).then(() => poll(promiseFn, time)));
1314
var pollingSetup = false;
15+
let sseSource = null;
1416

1517
export async function startQueueListener() {
1618
if (!pollingSetup) {
@@ -22,7 +24,7 @@ export async function startQueueListener() {
2224
log("User token not setup, queue not started.");
2325
}
2426
} else {
25-
checkMessageQueue();
27+
await checkMessageQueue();
2628
}
2729
}
2830

@@ -42,7 +44,7 @@ export async function checkMessageQueue() {
4244
async function handleMessage(message) {
4345
var messageProperties = resolveMessageProperties(message);
4446
if (messageProperties.hasRouteRule) {
45-
var currentUrl = Gist.currentRoute
47+
var currentUrl = Gist.currentRoute;
4648
if (currentUrl == null) {
4749
currentUrl = new URL(window.location.href).pathname;
4850
}
@@ -75,6 +77,23 @@ async function handleMessage(message) {
7577
}
7678

7779
export async function pullMessagesFromQueue() {
80+
// If SSE connection is already active, just check the local queue
81+
if (settings.hasActiveSSEConnection()) {
82+
await checkMessageQueue();
83+
return;
84+
}
85+
86+
// If SSE is enabled and user is not anonymous, set up SSE listener
87+
if (settings.useSSE() && !isAnonymousUser()) {
88+
await setupSSEQueueListener();
89+
return;
90+
}
91+
92+
// Fall back to polling
93+
await checkQueueThroughPolling();
94+
}
95+
96+
async function checkQueueThroughPolling() {
7897
if (getUserToken()) {
7998
if (Gist.isDocumentVisible) {
8099
// We're using the TTL as a way to determine if we should check the queue, so if the key is not there, we check the queue.
@@ -87,8 +106,7 @@ export async function pullMessagesFromQueue() {
87106
responseData = response.data;
88107
updateQueueLocalStore(responseData);
89108
updateBroadcastsLocalStore(responseData);
90-
}
91-
else if (response.status === 304) {
109+
} else if (response.status === 304) {
92110
log("304 response, using local store.");
93111
}
94112
await checkMessageQueue();
@@ -99,9 +117,66 @@ export async function pullMessagesFromQueue() {
99117
log(`Next queue pull scheduled for later.`);
100118
}
101119
} else {
102-
log(`Document not visible, skipping queue check.`);
120+
log(`Document not visible, skipping queue check.`);
103121
}
104122
} else {
105123
log(`User token reset, skipping queue check.`);
106124
}
107-
}
125+
}
126+
127+
async function setupSSEQueueListener() {
128+
const sseURL = getQueueSSEEndpoint();
129+
if (sseURL === null) {
130+
log("SSE endpoint not available, falling back to polling.");
131+
await checkQueueThroughPolling();
132+
return;
133+
}
134+
log(`Starting SSE queue listener on ${sseURL}`);
135+
sseSource = new EventSource(sseURL);
136+
settings.setActiveSSEConnection();
137+
138+
sseSource.addEventListener("connected", async (event) => {
139+
log("SSE connection received:", event);
140+
settings.setActiveSSEConnection();
141+
settings.setUseSSEFlag(true);
142+
});
143+
144+
sseSource.addEventListener("messages", async (event) => {
145+
try {
146+
var messages = JSON.parse(event.data);
147+
log("SSE message received:", messages);
148+
await updateQueueLocalStore(messages);
149+
await updateBroadcastsLocalStore(messages);
150+
await checkMessageQueue();
151+
} catch (e) {
152+
log("Failed to parse SSE message", e);
153+
stopSSEListener();
154+
}
155+
});
156+
157+
sseSource.addEventListener("error", async (event) => {
158+
log("SSE error received:", event);
159+
stopSSEListener();
160+
});
161+
162+
sseSource.addEventListener("heartbeat", async (event) => {
163+
log("SSE heartbeat received:", event);
164+
settings.setActiveSSEConnection();
165+
settings.setUseSSEFlag(true);
166+
});
167+
}
168+
169+
export function stopSSEListener() {
170+
// No active SSE connection to stop
171+
if (!sseSource) {
172+
return;
173+
}
174+
175+
// Close the connection and clean up
176+
log("Stopping SSE queue listener...");
177+
sseSource.close();
178+
sseSource = null;
179+
180+
// Update settings to reflect disconnected state
181+
settings.setUseSSEFlag(false);
182+
}

src/managers/user-manager.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export function useGuestSession() {
4747
}
4848
}
4949

50+
export function isAnonymousUser() {
51+
return isUsingGuestUserToken();
52+
}
53+
5054
export async function getHashedUserToken() {
5155
var userToken = getUserToken();
5256
if (userToken === null) {
@@ -55,6 +59,14 @@ export async function getHashedUserToken() {
5559
return await hashString(userToken);
5660
}
5761

62+
export function getEncodedUserToken() {
63+
var userToken = getUserToken();
64+
if (userToken === null) {
65+
return null;
66+
}
67+
return btoa(userToken);
68+
}
69+
5870
export function clearUserToken() {
5971
clearKeyFromLocalStore(userTokenLocalStoreName);
6072
log(`Cleared user token`);

src/services/network.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Gist from '../gist';
22
import { settings } from './settings';
3-
import { getUserToken } from "../managers/user-manager";
3+
import { getEncodedUserToken } from "../managers/user-manager";
44

55
export function UserNetworkInstance() {
66
const baseURL =
@@ -13,9 +13,9 @@ export function UserNetworkInstance() {
1313
'X-CIO-Client-Platform': 'web',
1414
};
1515

16-
const userToken = getUserToken();
16+
const userToken = getEncodedUserToken();
1717
if (userToken != null) {
18-
defaultHeaders['X-Gist-Encoded-User-Token'] = btoa(userToken);
18+
defaultHeaders['X-Gist-Encoded-User-Token'] = userToken;
1919
}
2020

2121
async function request(path, options = {}) {

src/services/queue-service.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import Gist from '../gist';
12
import { UserNetworkInstance } from './network';
23
import { getKeyFromLocalStore, setKeyToLocalStore } from '../utilities/local-storage';
34
import { log } from "../utilities/log";
4-
import { isUsingGuestUserToken } from '../managers/user-manager';
5+
import { isUsingGuestUserToken, getEncodedUserToken } from '../managers/user-manager';
56
import { getUserLocale } from '../managers/locale-manager';
67
import { settings } from './settings';
78
import { v4 as uuidv4 } from 'uuid';
@@ -39,6 +40,7 @@ export async function getUserQueue() {
3940
checkInProgress = false;
4041
scheduleNextQueuePull(response);
4142
setQueueAPIVersion(response);
43+
setQueueUseSSE(response);
4244
}
4345

4446
return response;
@@ -53,6 +55,11 @@ function setQueueAPIVersion(response) {
5355
}
5456
}
5557

58+
function setQueueUseSSE(response) {
59+
const useSSE = response?.headers?.["x-cio-use-sse"]?.toLowerCase() === "true";
60+
settings.setUseSSEFlag(useSSE);
61+
}
62+
5663
function getSessionId() {
5764
var sessionId = getKeyFromLocalStore(sessionIdLocalStoreName);
5865
if (!sessionId) {
@@ -73,3 +80,12 @@ function scheduleNextQueuePull(response) {
7380
var expiryDate = new Date(new Date().getTime() + currentPollingDelayInSeconds * 1000);
7481
setKeyToLocalStore(userQueueNextPullCheckLocalStoreName, currentPollingDelayInSeconds, expiryDate);
7582
}
83+
84+
export function getQueueSSEEndpoint() {
85+
var encodedUserToken = getEncodedUserToken();
86+
if (encodedUserToken === null) {
87+
log("No user token available for SSE endpoint.");
88+
return null;
89+
}
90+
return settings.GIST_QUEUE_REALTIME_API_ENDPOINT[Gist.config.env] + `/api/v3/sse?userToken=${encodedUserToken}&siteId=${Gist.config.siteId}&sessionId=${getSessionId()}`;
91+
}

src/services/settings.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { getKeyFromLocalStore, setKeyToLocalStore } from '../utilities/local-storage';
22
import { log } from '../utilities/log';
33
const userQueueVersionLocalStoreName = "gist.web.userQueueVersion";
4+
const userQueueUseSSELocalStoreName = "gist.web.userQueueUseSSE";
5+
const userQueueActiveSSEConnectionLocalStoreName = "gist.web.activeSSEConnection";
46

57
export const settings = {
68
RENDERER_HOST: "https://code.gist.build",
@@ -19,6 +21,11 @@ export const settings = {
1921
"dev": "https://consumer.cloud.dev.gist.build",
2022
"local": "http://api.local.gist.build:86"
2123
},
24+
GIST_QUEUE_REALTIME_API_ENDPOINT: {
25+
"prod": "https://realtime.cloud.gist.build",
26+
"dev": "https://realtime.cloud.dev.gist.build",
27+
"local": "http://api.local.gist.build:3000"
28+
},
2229
GIST_VIEW_ENDPOINT: {
2330
"prod": "https://renderer.gist.build/3.0",
2431
"dev": "https://renderer.gist.build/3.0",
@@ -31,5 +38,18 @@ export const settings = {
3138
// The Queue API version TTL is renewed with every poll request and extended by 30 minutes.
3239
setKeyToLocalStore(userQueueVersionLocalStoreName, version, new Date(new Date().getTime() + 1800000));
3340
log(`Set user queue version to "${version}"`);
41+
},
42+
useSSE: function() {
43+
return getKeyFromLocalStore(userQueueUseSSELocalStoreName) ?? false;
44+
},
45+
setUseSSEFlag: function(useSSE) {
46+
setKeyToLocalStore(userQueueUseSSELocalStoreName, useSSE, new Date(new Date().getTime() + 60000));
47+
log(`Set user uses SSE to "${useSSE}"`);
48+
},
49+
setActiveSSEConnection: function() {
50+
setKeyToLocalStore(userQueueActiveSSEConnectionLocalStoreName, true, new Date(new Date().getTime() + 31000));
51+
},
52+
hasActiveSSEConnection: function() {
53+
return getKeyFromLocalStore(userQueueActiveSSEConnectionLocalStoreName) ?? false;
3454
}
3555
}

0 commit comments

Comments
 (0)