Skip to content

Commit 0ba72d8

Browse files
authored
feat(js,react): disconnect usage limited users (#356)
* feat(js,react): disconnect usage limited users * fix: websocket test in firefox
1 parent dff76b5 commit 0ba72d8

File tree

6 files changed

+135
-54
lines changed

6 files changed

+135
-54
lines changed

workspaces/e2e/tests/websocket.spec.ts

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,49 +13,94 @@ const getBlock = (): Block => ({
1313
propertyMeta: [],
1414
});
1515

16-
let ws: WebSocketRoute | null = null;
17-
test.beforeEach(async ({ page }) => {
18-
await page.routeWebSocket(
19-
(url) => url.pathname === "/ws/sdk/block-updates",
20-
(_ws) => {
21-
ws = _ws;
22-
},
23-
);
24-
});
25-
2616
const run = (packageName: string) => {
27-
test(`${packageName} - should display block that is received through websocket`, async ({
28-
page,
29-
}) => {
30-
const block = getBlock();
31-
await page.route("**/v2/sdk/blocks", (route) => {
32-
route.fulfill({ json: { blocks: [] } });
17+
test.describe(`mocked websocket`, () => {
18+
let ws: WebSocketRoute | null = null;
19+
test.beforeEach(async ({ page }) => {
20+
await page.routeWebSocket(
21+
(url) => url.pathname === "/ws/sdk/block-updates",
22+
(_ws) => {
23+
ws = _ws;
24+
},
25+
);
26+
});
27+
28+
test(`${packageName} - should display block that is received through websocket`, async ({
29+
page,
30+
}) => {
31+
const block = getBlock();
32+
await page.route("**/v2/sdk/blocks", (route) => {
33+
route.fulfill({ json: { blocks: [] } });
34+
});
35+
await page.goto(`/${packageName}.html`);
36+
await expect(page.locator("h1")).toBeVisible();
37+
await expect(page.getByText("Hello world", { exact: true })).toBeHidden();
38+
const payload: BlockUpdatesPayload = {
39+
exitedBlockIds: [],
40+
updatedBlocks: [block],
41+
};
42+
await (ws as unknown as WebSocketRoute).send(JSON.stringify(payload));
43+
await expect(page.getByText("Hello world", { exact: true })).toBeVisible();
44+
});
45+
test(`${packageName} - should exit block that is received through websocket`, async ({
46+
page,
47+
}) => {
48+
const block = getBlock();
49+
await page.route("**/v2/sdk/blocks", (route) => {
50+
route.fulfill({ json: { blocks: [block] } });
51+
});
52+
await page.goto(`/${packageName}.html`);
53+
await expect(page.getByText("Hello world", { exact: true })).toBeVisible();
54+
const payload: BlockUpdatesPayload = {
55+
exitedBlockIds: [block.id],
56+
updatedBlocks: [],
57+
};
58+
await (ws as unknown as WebSocketRoute).send(JSON.stringify(payload));
59+
await expect(page.getByText("Hello world", { exact: true })).toBeHidden();
3360
});
34-
await page.goto(`/${packageName}.html`);
35-
await expect(page.locator("h1")).toBeVisible();
36-
await expect(page.getByText("Hello world", { exact: true })).toBeHidden();
37-
const payload: BlockUpdatesPayload = {
38-
exitedBlockIds: [],
39-
updatedBlocks: [block],
40-
};
41-
await (ws as unknown as WebSocketRoute).send(JSON.stringify(payload));
42-
await expect(page.getByText("Hello world", { exact: true })).toBeVisible();
4361
});
44-
test(`${packageName} - should exit block that is received through websocket`, async ({
45-
page,
46-
}) => {
47-
const block = getBlock();
48-
await page.route("**/v2/sdk/blocks", (route) => {
49-
route.fulfill({ json: { blocks: [block] } });
62+
63+
test.describe(`real websocket`, () => {
64+
test(`${packageName} - should disconnect from websocket if the user is usage limited`, async ({
65+
page,
66+
}) => {
67+
await page.route("**/v2/sdk/blocks", (route) => {
68+
route.fulfill({ json: { blocks: [], meta: { usage_limited: true } } });
69+
});
70+
const wsPromise = page.waitForEvent("websocket");
71+
await page.goto(`/${packageName}.html`);
72+
const websocket = await wsPromise;
73+
74+
let wsWasClosed = false;
75+
websocket.on("close", () => {
76+
wsWasClosed = true;
77+
});
78+
await page.waitForRequest((req) => {
79+
return req.url() === "https://api.flows-cloud.com/v2/sdk/blocks";
80+
});
81+
82+
await expect(() => expect(wsWasClosed).toBe(true)).toPass();
83+
});
84+
test(`${packageName} - should keep websocket connection alive`, async ({ page }) => {
85+
await page.route("**/v2/sdk/blocks", (route) => {
86+
route.fulfill({ json: { blocks: [] } });
87+
});
88+
const wsPromise = page.waitForEvent("websocket");
89+
await page.goto(`/${packageName}.html`);
90+
const websocket = await wsPromise;
91+
92+
let wsWasClosed = false;
93+
websocket.on("close", () => {
94+
wsWasClosed = true;
95+
});
96+
await page.waitForRequest((req) => {
97+
return req.url() === "https://api.flows-cloud.com/v2/sdk/blocks";
98+
});
99+
100+
await new Promise((res) => setTimeout(res, 500));
101+
102+
await expect(() => expect(wsWasClosed).toBe(false)).toPass();
50103
});
51-
await page.goto(`/${packageName}.html`);
52-
await expect(page.getByText("Hello world", { exact: true })).toBeVisible();
53-
const payload: BlockUpdatesPayload = {
54-
exitedBlockIds: [block.id],
55-
updatedBlocks: [],
56-
};
57-
await (ws as unknown as WebSocketRoute).send(JSON.stringify(payload));
58-
await expect(page.getByText("Hello world", { exact: true })).toBeHidden();
59104
});
60105
};
61106

workspaces/js/src/lib/blocks.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { type BlockUpdatesPayload, getApi, log, type UserProperties } from "@flows/shared";
22
import { blocks } from "../store";
3-
import { websocket } from "./websocket";
3+
import { type Disconnect, websocket } from "./websocket";
44
import { packageAndVersion } from "./constants";
55

66
interface Props {
@@ -11,6 +11,8 @@ interface Props {
1111
userProperties?: UserProperties;
1212
}
1313

14+
let disconnect: Disconnect | null = null;
15+
1416
export const connectToWebsocketAndFetchBlocks = (props: Props): void => {
1517
const { environment, organizationId, userId, apiUrl } = props;
1618
const params = { environment, organizationId, userId };
@@ -24,6 +26,8 @@ export const connectToWebsocketAndFetchBlocks = (props: Props): void => {
2426
.getBlocks({ ...params, userProperties: props.userProperties })
2527
.then((res) => {
2628
blocks.value = res.blocks;
29+
// Disconnect if the user is usage limited
30+
if (res.meta?.usage_limited) disconnect?.();
2731
})
2832
.catch((err: unknown) => {
2933
log.error("Failed to load blocks", err);
@@ -41,5 +45,9 @@ export const connectToWebsocketAndFetchBlocks = (props: Props): void => {
4145
];
4246
};
4347

44-
websocket({ url: wsUrl, onMessage, onOpen: fetchBlocks });
48+
// Disconnect previous connection if it exists
49+
disconnect?.();
50+
51+
const websocketResult = websocket({ url: wsUrl, onMessage, onOpen: fetchBlocks });
52+
disconnect = websocketResult.disconnect;
4553
};

workspaces/js/src/lib/websocket.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,37 @@ interface Props {
44
onOpen: () => void;
55
}
66

7+
export type Disconnect = () => void;
8+
79
// TODO: implement better automatic reconnection
8-
export const websocket = (props: Props): void => {
9-
const connect = (): void => {
10+
export const websocket = (props: Props): { disconnect: Disconnect } => {
11+
const connect = (): { disconnect: Disconnect } => {
1012
const socket = new WebSocket(props.url);
1113

12-
socket.addEventListener("message", props.onMessage);
13-
socket.addEventListener("open", props.onOpen);
14-
socket.addEventListener("close", () => {
14+
const handleClose = (): void => {
1515
setTimeout(() => {
1616
connect();
1717
}, 2000);
18-
});
18+
};
19+
20+
socket.addEventListener("message", props.onMessage);
21+
socket.addEventListener("open", props.onOpen);
22+
socket.addEventListener("close", handleClose);
23+
24+
const disconnect = (): void => {
25+
socket.removeEventListener("message", props.onMessage);
26+
socket.removeEventListener("open", props.onOpen);
27+
socket.removeEventListener("close", handleClose);
28+
29+
if (socket.readyState === WebSocket.CONNECTING) {
30+
socket.addEventListener("open", () => {
31+
socket.close();
32+
});
33+
} else socket.close();
34+
};
35+
36+
return { disconnect };
1937
};
2038

21-
connect();
39+
return connect();
2240
};

workspaces/react/src/hooks/use-blocks.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const useBlocks = ({
3333
userProperties,
3434
}: Props): Return => {
3535
const [blocks, setBlocks] = useState<Block[]>([]);
36+
const [usageLimited, setUsageLimited] = useState(false);
3637

3738
const params = useMemo(
3839
() => ({ environment, organizationId, userId }),
@@ -49,16 +50,18 @@ export const useBlocks = ({
4950
.getBlocks({ ...params, userProperties: userPropertiesRef.current })
5051
.then((res) => {
5152
setBlocks(res.blocks);
53+
if (res.meta?.usage_limited) setUsageLimited(true);
5254
})
5355
.catch((err: unknown) => {
5456
log.error("Failed to load blocks", err);
5557
});
5658
}, [apiUrl, params]);
5759

58-
const url = useMemo(() => {
60+
const websocketUrl = useMemo(() => {
61+
if (usageLimited) return;
5962
const baseUrl = apiUrl.replace("https://", "wss://").replace("http://", "ws://");
6063
return `${baseUrl}/ws/sdk/block-updates?${new URLSearchParams(params).toString()}`;
61-
}, [apiUrl, params]);
64+
}, [apiUrl, params, usageLimited]);
6265

6366
const onMessage = useCallback((event: MessageEvent<unknown>) => {
6467
// TODO: add debug logging
@@ -73,7 +76,7 @@ export const useBlocks = ({
7376
...data.updatedBlocks,
7477
]);
7578
}, []);
76-
useWebsocket({ url, onMessage, onOpen: fetchBlocks });
79+
useWebsocket({ url: websocketUrl, onMessage, onOpen: fetchBlocks });
7780

7881
// Log error about slottable blocks without slotId
7982
useEffect(() => {

workspaces/react/src/hooks/use-websocket.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback, useEffect, useRef, useState } from "react";
22

33
interface Props {
4-
url: string;
4+
url?: string;
55
onMessage: (event: MessageEvent<unknown>) => void;
66
onOpen?: () => void;
77
}
@@ -24,12 +24,14 @@ export const useWebsocket = ({ url, onMessage, onOpen }: Props): void => {
2424
onMessageRef.current(event);
2525
}, []);
2626

27-
const connect = useCallback(() => {
27+
const connect = useCallback((): (() => void) | undefined => {
2828
if (cleanupRef.current) {
2929
cleanupRef.current();
3030
cleanupRef.current = undefined;
3131
}
3232

33+
if (!url) return;
34+
3335
const socket = new WebSocket(url);
3436
setWs(socket);
3537

@@ -66,7 +68,7 @@ export const useWebsocket = ({ url, onMessage, onOpen }: Props): void => {
6668
useEffect(() => {
6769
const cleanup = connect();
6870
return () => {
69-
cleanup();
71+
cleanup?.();
7072
};
7173
}, [connect]);
7274

workspaces/shared/src/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,13 @@ interface GetBlocksRequest {
2828
userProperties?: Record<string, unknown>;
2929
}
3030

31+
interface BlockResponseMeta {
32+
usage_limited?: boolean;
33+
}
34+
3135
interface BlocksResponse {
3236
blocks: Block[];
37+
meta?: BlockResponseMeta;
3338
}
3439

3540
export interface EventRequest {

0 commit comments

Comments
 (0)