Skip to content

Commit

Permalink
* Fixed that idling on the homepage would cause a "websocket disconne…
Browse files Browse the repository at this point in the history
…cted" notification to show up. (caused by the websocket having no subscriptions active; now, a "link preserver" subscription is created -- also moved the "request page refresh" functionality from the "ping" endpoint to this new one)

* Renamed "env_ENV" in dockerfiles to "ENVIRONMENT". (less confusing, and standardizes with env-var-name chosen for Tiltfile)
  • Loading branch information
Venryx committed Mar 12, 2024
1 parent d2f8413 commit aeca0df
Show file tree
Hide file tree
Showing 17 changed files with 124 additions and 159 deletions.
10 changes: 5 additions & 5 deletions Packages/app-server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
# ----------
# see: ./Tilt/Main.star (or source: Packages/deploy/@RustBase/Dockerfile)
FROM $RUST_BASE_URL as cargo-build
ARG env_ENV
ARG ENVIRONMENT
ARG debug_vs_release
ARG debug_vs_release_flag
ARG cargo_path
Expand All @@ -34,8 +34,8 @@ RUN echo "fn main() { println!(\"If this println executes, the build broke.\");

# initial arg processing
WORKDIR "/dm_repo"
ENV ENV=$env_ENV
RUN echo "Env:$ENV DebugVSRelease:$debug_vs_release CargoPath:$cargo_path"
ENV ENVIRONMENT=$ENVIRONMENT
RUN echo "Env:$ENVIRONMENT DebugVSRelease:$debug_vs_release CargoPath:$cargo_path"

# now build everything
WORKDIR /dm_repo/Packages/app-server
Expand Down Expand Up @@ -64,11 +64,11 @@ RUN mkdir -p ./kgetOutput_buildTime && (cp cargo-timing.html ./kgetOutput_buildT
# use debian v12 (bookworm), because that is what our linker (mold) was built on [mold only has releases for debian v12+], which makes the produced binary require it as well
#FROM debian:bookworm-slim@sha256:5007b106fd828d768975b21cfdcecb51a8eeea9aab815a9e4a169acde464fb89
FROM debian:bookworm-20221114-slim
ARG env_ENV
ARG ENVIRONMENT
# ----------

WORKDIR /dm_repo/Packages/app-server
ENV ENV=$env_ENV
ENV ENVIRONMENT=$ENVIRONMENT

# temp (for ssl connections; will look for cleaner way soon)
#RUN apt-get update && apt-get install -y ca-certificates
Expand Down
58 changes: 44 additions & 14 deletions Packages/app-server/src/db/_general.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,31 +85,61 @@ pub struct GenericMutation_Result {
// subscriptions
// ==========

struct Ping_Result {
pong: String,
refreshPage: bool,
#[derive(InputObject, Serialize, Deserialize)]
pub struct LinkPreserverInput {
pub updateInterval: u64,
}
#[Object]
impl Ping_Result {
async fn pong(&self) -> &str { &self.pong }
async fn refreshPage(&self) -> &bool { &self.refreshPage }

#[derive(SimpleObject)]
struct LinkPreserverResult {
alive: bool,
// probably move effects like this (unrelated to link-preserving) into a separate subscription eventually
pageRefreshRequested: bool,
}

#[derive(SimpleObject)]
struct PingResult {
pong: String,
}

#[derive(Default)] pub struct SubscriptionShard_General;
#[Subscription] impl SubscriptionShard_General {
/// This endpoint serves two purposes:
/// * Keeps cloudflare from terminating the websocket for inactivity, in cases where >100s pass without data changing or the user navigating anywhere.
/// * Keeps the frontend from closing the websocket, in cases where the client is not watching any data. (eg. on homepage when not signed-in)
async fn linkPreserver(&self, _ctx: &async_graphql::Context<'_>, input: LinkPreserverInput) -> impl Stream<Item = Result<LinkPreserverResult, SubError>> {
let base_stream = async_stream::stream! {
let LinkPreserverInput { updateInterval } = input;
if (updateInterval < 10000) { Err(SubError::new(format!("Update-interval cannot be lower than 10000ms.")))?; }

let mut refresh_requested_last_iteration = Path::new("./refreshPageForAllUsers_enabled").exists();
loop {
// create the listed file in the app-server pod (eg. using Lens), if you've made an update that you need all clients to refresh for
let refresh_requested_new = Path::new("./refreshPageForAllUsers_enabled").exists();
let refresh_just_requested = refresh_requested_new && !refresh_requested_last_iteration;
let result = LinkPreserverResult {
alive: true,
pageRefreshRequested: refresh_just_requested,
};
refresh_requested_last_iteration = refresh_requested_new;

yield Ok(result);
rust_shared::tokio::time::sleep(Duration::from_millis(updateInterval)).await;
}
};
base_stream
}

// for testing (eg. in gql-playground) [temporarily also used by frontend as a websocket keep-alive -- inferior to above since doesn't work in the no-data-watched case]
#[graphql(name = "_ping")]
async fn _ping(&self, _ctx: &async_graphql::Context<'_>) -> impl Stream<Item = Ping_Result> {
async fn _ping(&self, _ctx: &async_graphql::Context<'_>) -> impl Stream<Item = PingResult> {
let pong = "pong".to_owned();
// create the listed file in the app-server pod (eg. using Lens), if you've made an update that you need all clients to refresh for
let refreshPage = Path::new("./refreshPageForAllUsers_enabled").exists();

stream::once(async move { Ping_Result {
stream::once(async move { PingResult {
pong,
refreshPage,
} })
}

// meant only for debugging, so hide from gql api introspection
// for debugging only, so hide from gql api introspection
#[graphql(visible = false)]
async fn checkUser<'a>(&self, ctx: &'a async_graphql::Context<'a>) -> impl Stream<Item = Result<CheckUserResult, SubError>> + 'a {
let base_stream = async_stream::stream! {
Expand Down
4 changes: 2 additions & 2 deletions Packages/app-server/src/gql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,8 @@ pub type RootSchema = wrap_agql_schema_type!{
const GRAPHQL_PATH: &str = "/app-server/graphql";

async fn graphiql() -> impl IntoResponse {
// use the DEV/PROD value from the "ENV" env-var, to determine what the app-server's URL is (maybe temp)
let app_server_host = if env::var("ENV").unwrap_or("DEV".to_owned()) == "DEV" { "localhost:5110" } else { "app-server.debatemap.app" };
// use the DEV/PROD value from the "ENVIRONMENT" env-var, to determine what the app-server's URL is (maybe temp)
let app_server_host = if env::var("ENVIRONMENT").unwrap_or("DEV".to_owned()) == "DEV" { "localhost:5110" } else { "app-server.debatemap.app" };
response::Html(graphiql_source(GRAPHQL_PATH, Some(&format!("wss://{app_server_host}{GRAPHQL_PATH}"))))
}
async fn graphql_playground() -> impl IntoResponse {
Expand Down
35 changes: 35 additions & 0 deletions Packages/client/Source/UI/@Root/LinkPreserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {BaseComponent} from "react-vextensions";
import {gql, useSubscription} from "web-vcore/nm/@apollo/client";

export const LINK_PRESERVER_SUBSCRIPTION = gql`
subscription($input: LinkPreserverInput!) {
linkPreserver(input: $input) {
alive
pageRefreshRequested
}
}
`;
type LinkPreserverResult = {alive: boolean, pageRefreshRequested: boolean};

export class LinkPreserver extends BaseComponent<{}, {}> {
render() {
let {} = this.props;

// Choose 45s as our update-interval; this avoids Cloudflare's "100 seconds of dormancy" timeout. (https://community.cloudflare.com/t/cloudflare-websocket-timeout/5865)
// (we use a <60s interval, so that it will reliably hit each 60s timer-interval that Chrome 88+ allows for hidden pages: https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling)
const updateInterval = 45000;

const {data, loading} = useSubscription(LINK_PRESERVER_SUBSCRIPTION, {
variables: {input: {updateInterval}},
onSubscriptionData: info=>{
const {alive, pageRefreshRequested} = info.subscriptionData.data.linkPreserver as LinkPreserverResult;
if (pageRefreshRequested) {
console.log("Refreshing page due to server request.");
window.location.reload();
}
},
});

return null;
}
}
12 changes: 4 additions & 8 deletions Packages/client/Source/UI/@Shared/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import React, {useCallback} from "react";
import {RootState, store} from "Store";
import {liveSkin} from "Utils/Styles/SkinManager.js";
import {zIndexes} from "Utils/UI/ZIndexes.js";
import {rootPageDefaultChilds} from "Utils/URL/URLs.js";
import {HasAdminPermissions, Me, MeID} from "dm_common";
import React, {useCallback} from "react";
import {Link, NavBarPanelButton, NotificationsUI, Observer} from "web-vcore";
import {E} from "web-vcore/nm/js-vextensions.js";
import {runInAction} from "web-vcore/nm/mobx.js";
import {GetDocs} from "web-vcore/nm/mobx-graphlink.js";
import {Div} from "web-vcore/nm/react-vcomponents.js";
import {BaseComponent, BaseComponentPlus} from "web-vcore/nm/react-vextensions.js";
import {graph} from "Utils/LibIntegrations/MobXGraphlink.js";
import {HasAdminPermissions, Me, MeID} from "dm_common";
import {liveSkin} from "Utils/Styles/SkinManager.js";
import {DebugPanel} from "./NavBar/DebugPanel.js";
import {GuidePanel} from "./NavBar/GuidePanel.js";
import {ReputationPanel} from "./NavBar/ReputationPanel.js";
import {DebugPanel} from "./NavBar/DebugPanel.js";
import {SearchPanel} from "./NavBar/SearchPanel.js";
import {StreamPanel} from "./NavBar/StreamPanel.js";
import {UserPanel} from "./NavBar/UserPanel.js";
Expand Down
2 changes: 2 additions & 0 deletions Packages/client/Source/UI/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {FeedbackUI} from "./Feedback.js";
import {ForumUI} from "./Forum.js";
import {SocialUI} from "./Social.js";
import {apolloClient} from "../Utils/LibIntegrations/Apollo.js";
import {LinkPreserver} from "./@Root/LinkPreserver";

ColorPickerBox.Init(ReactColor, chroma);

Expand Down Expand Up @@ -213,6 +214,7 @@ class RootUI extends BaseComponentPlus({} as {}, {}) {
<RootStyles/>
<ErrorBoundary>
<AddressBarWrapper/>
<LinkPreserver/>
<OverlayUI/>
</ErrorBoundary>
{ShowHeader &&
Expand Down
101 changes: 0 additions & 101 deletions Packages/client/Source/Utils/LibIntegrations/Apollo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,8 @@ import {GetUserInfoJWTString, SendUserJWTToMGL} from "Utils/AutoRuns/UserInfoChe
import {RunInAction} from "web-vcore";
import {ApolloClient, ApolloError, ApolloLink, DefaultOptions, FetchResult, from, gql, HttpLink, NormalizedCacheObject, split} from "web-vcore/nm/@apollo/client.js";
import {getMainDefinition, GraphQLWsLink, onError} from "web-vcore/nm/@apollo/client_deep.js";
import {Timer} from "web-vcore/nm/js-vextensions";
import {VoidCache} from "./Apollo/VoidCache.js";

/*export function GetWebServerURL(subpath: string) {
Assert(subpath.startsWith("/"));
/*if (location.host == "localhost:5100") return subpath;
if (location.host == "localhost:31005") return subpath; // because of tilt-proxy, this usually isn't needed, but keeping for raw access
return `https://debatemap.app/${subpath.slice(1)}`;*#/
return subpath;
}
export function GetAppServerURL(subpath: string): string {
Assert(subpath.startsWith("/"));
// temp
/*if (location.host == "debates.app" || DB == "prod") return `https://app-server.debates.app/${subpath.slice(1)}`;
if (location.host == "localhost:5100" || location.host == "localhost:5101") return `http://localhost:5110/${subpath.slice(1)}`;
//if (location.host == "localhost:31005") return `http://localhost:31006/${subpath.slice(1)}`; // because of tilt-proxy, this usually isn't needed, but keeping for raw access
// if we're in remote k8s, but accessing it from the raw cluster-url, just change the port
//if (location.host.endsWith(":31005")) return `${location.protocol}//${location.host.replace(":31005", ":31006")}/${subpath.slice(1)}`;
return `https://app-server.debatemap.app/${subpath.slice(1)}`;*#/
if (DB == "dev") return `http://localhost:5110/${subpath.slice(1)}`;
if (DB == "prod") {
// maybe temp: for graphql/websocket to OVH host directly, use unencrypted http/ws rather than https/wss (since the server hasn't yet been set up with TLS itself)
/*if (window.location.host.endsWith(".ovh.us") && subpath == "/graphql") {
return `http://app-server.${window.location.host}/${subpath.slice(1)}`;
}*#/
//return `https://app-server.debates.app/${subpath.slice(1)}`;
//return `https://app-server.${window.location.host}/${subpath.slice(1)}`;
return `${window.location.protocol}//app-server.${window.location.host}/${subpath.slice(1)}`;
}
Assert(false, `Invalid database specified:${DB}`);
}*/

export function GetWebServerURL(subpath: string, preferredServerOrigin?: string) {
return GetServerURL("web-server", subpath, preferredServerOrigin ?? window.location.origin);
}
Expand Down Expand Up @@ -72,17 +36,6 @@ let link: ApolloLink;
let link_withErrorHandling: ApolloLink;
export let apolloClient: ApolloClient<NormalizedCacheObject>;

/*function Test1() {
const websocket = new WebSocket(GRAPHQL_URL.replace(/^http/, "ws"));
websocket.onopen = ()=>{
console.log("connection opened");
//websocket.send(username.value);
};
websocket.onclose = ()=>console.log("connection closed");
websocket.onmessage = e=>console.log(`received message: ${e.data}`);
document.onclick = e=>websocket.send(`Hi:${Date.now()}`);
}*/

export function InitApollo() {
httpLink = new HttpLink({
uri: GRAPHQL_URL,
Expand Down Expand Up @@ -138,12 +91,6 @@ export function InitApollo() {
});
wsLink = new GraphQLWsLink(wsClient);

// every 45s, send a "keepalive message" through the WS; this avoids Cloudflare's "100 seconds of dormancy" timeout (https://community.cloudflare.com/t/cloudflare-websocket-timeout/5865)
// (we use a <60s interval, so that it will reliably hit each 60s timer-interval that Chrome 88+ allows for hidden pages: https://developer.chrome.com/blog/timer-throttling-in-chrome-88/#intensive-throttling)
const keepAliveTimer = new Timer(45000, ()=>{
SendPingOverWebSocket();
}).Start();

// using the ability to split links, you can send data to each link depending on what kind of operation is being sent
link = split(
// split based on operation type
Expand Down Expand Up @@ -190,26 +137,7 @@ export function InitApollo() {
]);
apolloClient = new ApolloClient({
//credentials: "include", // allows cookies to be sent with "graphql" calls (eg. for passing passportjs session-token with mutation/command calls) // this way doesn't work, I think because we send a custom "link"
//link,
link: link_withErrorHandling,
/*cache: new InMemoryCache({
//dataIdFromObject: a=>a.nodeId as string ?? null,
dataIdFromObject: a=>a.id as string ?? null,
typePolicies: {
Query: {
fields: {
...GetTypePolicyFieldsMappingSingleDocQueriesToCache(),
},
},
// temp fix for: https://github.com/apollographql/apollo-client/issues/8677#issuecomment-925661998
Map: {
fields: {
featured(rawVal: boolean, {args}) { return rawVal ?? null; },
note(rawVal: string, {args}) { return rawVal ?? null; },
},
},
},
}),*/
// replace InMemoryCache with VoidCache, because even a "not used" InMemoryCache has significant overhead, for checking for cache matches and such (>1s over ~25s map-load)
cache: new VoidCache(),
// default to not using the cache (it does nothing for subscriptions, and often does *opposite* of what we want for queries [eg. search]; and even when wanted, it's better to explicitly set it)
Expand All @@ -231,35 +159,6 @@ export function InitApollo() {
SendUserJWTToMGL();
}

export async function SendPingOverWebSocket() {
const fetchResult_subscription = apolloClient.subscribe({
query: gql`
subscription {
_ping {
pong
refreshPage
}
}
`,
variables: {},
});
const fetchResult = await new Promise<FetchResult<any>>(resolve=>{
const subscription = fetchResult_subscription.subscribe(data=>{
subscription.unsubscribe(); // unsubscribe as soon as first (and only) result is received
resolve(data);
});
});
//console.log("Got response to ping:", fetchResult);

const {pong, refreshPage} = fetchResult.data._ping;
if (refreshPage) {
console.log("Refreshing page due to server request.");
window.location.reload();
}

return fetchResult;
}

// todo: ensure that this request gets sent before any others, on the websocket connection (else those ones will fail)
export async function AttachUserJWTToWebSocketConnection() {
// associate user-info jwt to websocket-connection, by calling the `signInAttach` endpoint
Expand Down
6 changes: 3 additions & 3 deletions Packages/deploy/@JSBase/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
FROM node:16-alpine

# image-internal args
ARG env_ENV
ARG ENVIRONMENT
# ----------

# initial arg processing
ENV ENV=$env_ENV
RUN echo "Env:$ENV"
ENV ENVIRONMENT=$ENVIRONMENT
RUN echo "Env:$ENVIRONMENT"

WORKDIR "/dm_repo"

Expand Down
6 changes: 3 additions & 3 deletions Packages/deploy/@RustBase/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@
FROM instrumentisto/rust:nightly-bullseye-2024-03-05

# commented; these args aren't used in this dockerfile anyway, so leave them out, to allow complete skipping of this dockerfile (execution can be fast anyway when most steps cached, but this way keeps the logs leaner)
#ARG env_ENV
#ARG ENVIRONMENT
#ARG debug_vs_release
#ARG debug_vs_release_flag
# ----------

# initial arg processing
#ENV ENV=$env_ENV
#RUN echo "Env:$ENV DebugVSRelease:$debug_vs_release"
#ENV ENVIRONMENT=$ENVIRONMENT
#RUN echo "Env:$ENVIRONMENT DebugVSRelease:$debug_vs_release"

# generic env-var for code to know if its running as part of a Dockerfile
ENV IN_DOCKER=1
Expand Down
Loading

0 comments on commit aeca0df

Please sign in to comment.