Skip to content

Commit

Permalink
Fixed some nits in the nextjs example
Browse files Browse the repository at this point in the history
  • Loading branch information
slimandslam committed Dec 19, 2024
1 parent 209f2c4 commit dcd03d4
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 150 deletions.
37 changes: 0 additions & 37 deletions examples/schwab-dashboard-nextjs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,40 +50,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
120 changes: 62 additions & 58 deletions examples/schwab-dashboard-nextjs/app/api/market-stream/route.js
Original file line number Diff line number Diff line change
@@ -1,90 +1,94 @@
// route.js -- receives the websocket feed from Schwab and sends it
// via SSE to the frontend
// Author: Jason Levitt

import { StreamingApiClient } from "schwab-client-js";

let streamclient = null;
let activeClients = 0;
let initialized = false;

export async function GET() {
const encoder = new TextEncoder();
let closed = false;
let heartbeat = null;
let controllerClosed = false; // Track if the controller is closed

const stream = new ReadableStream({
async start(controller) {
console.log("Client connected to SSE");
const initializeResources = async () => {
if (initialized) return;
console.log("Initializing WebSocket resources...");

const cleanup = () => {
if (streamclient) streamclient.streamClose(); // Close WebSocket
clearInterval(heartbeat);
closed = true;
console.log("Stream closed.");
controller.close();
};
streamclient = new StreamingApiClient();

try {
streamclient = new StreamingApiClient();
streamclient.streamListen("open", () => {
console.log("WebSocket connection opened.");
});

streamclient.streamListen("open", () => {
console.log("WebSocket connection opened.");
});
streamclient.streamListen("close", (code, reason) => {
console.log(`WebSocket closed: Code=${code}, Reason=${reason}`);
});

streamclient.streamListen("close", (code, reason) => {
console.log(`WebSocket closed: Code=${code}, Reason=${reason}`);
});
streamclient.streamListen("error", (error) => {
console.error("WebSocket error:", error);
});

streamclient.streamListen("error", (error) => {
console.error("WebSocket error:", error);
});
try {
await streamclient.streamInit();
await streamclient.streamSchwabLogin();

await streamclient.streamInit();
await streamclient.streamSchwabLogin();
const params = { keys: "NVDA", fields: "0,1,2,8,9,10,11,12,18" };
await streamclient.streamSchwabRequest(
"ADD",
"LEVELONE_EQUITIES",
params,
);

// Subscribe to LEVELONE_EQUITIES and CHART_EQUITY
let params = { keys: "NVDA", fields: "0,1,2,8,9,10,11,12,18" };
await streamclient.streamSchwabRequest(
"ADD",
"LEVELONE_EQUITIES",
params,
);
initialized = true;
console.log("WebSocket resources initialized.");
} catch (err) {
console.error("Error initializing WebSocket resources:", err);
}
};

// Send WebSocket messages to SSE clients
streamclient.streamListen("message", (message) => {
if (closed) return;
if (message.includes('"data"')) {
console.log("Forwarding data:", message);
try {
controller.enqueue(encoder.encode(`data: ${message}\n\n`));
} catch (err) {
console.error("Failed to send message:", err);
cleanup();
}
}
});
} catch (err) {
console.error("Error initializing WebSocket client:", err);
cleanup();
const stream = new ReadableStream({
async start(controller) {
console.log("Client connected to SSE");
activeClients++;
controllerClosed = false; // Reset the flag for new connections

if (!initialized) {
await initializeResources();
}

// Heartbeat to keep the SSE alive
const heartbeat = setInterval(() => {
if (closed) return;
heartbeat = setInterval(() => {
if (controllerClosed) return; // Skip if the controller is closed
try {
controller.enqueue(
encoder.encode(
`data: ${JSON.stringify({ heartbeat: "keep-alive" })}\n\n`,
`data: ${JSON.stringify({ heartbeat: "alive" })}\n\n`,
),
);
} catch (err) {
console.error("Failed to send heartbeat:", err);
cleanup();
}
}, 10000);

streamclient.streamListen("message", (message) => {
if (controllerClosed) return; // Skip if the controller is closed
try {
controller.enqueue(encoder.encode(`data: ${message}\n\n`));
console.log("Sending data: ", message);
} catch (err) {
console.error("Failed to send message:", err);
}
});
},
cancel() {
console.log("Client disconnected.");
closed = true;
if (streamclient) streamclient.streamClose();
activeClients--;
controllerClosed = true; // Mark the controller as closed
if (heartbeat) clearInterval(heartbeat); // Clear the heartbeat interval
if (activeClients === 0) {
console.log(
"No active clients. Keeping WebSocket and resources alive for future connections.",
);
// Do not clean up WebSocket resources; wait for next client
}
},
});

Expand Down
110 changes: 67 additions & 43 deletions examples/schwab-dashboard-nextjs/components/StockDashboard.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
// StockDashboard - Four charts that are populated by an SSE connection
// Author: Jason Levitt

"use client";

import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import {
LineChart,
Line,
Expand All @@ -22,50 +19,77 @@ const StockDashboard = () => {
const [lastHighPrice, setLastHighPrice] = useState(null);
const [lastLowPrice, setLastLowPrice] = useState(null);
const [closePrice, setClosePrice] = useState(null);
const [error, setError] = useState(null);
const eventSourceRef = useRef(null); // Persist the SSE connection across re-renders

useEffect(() => {
const eventSource = new EventSource("/api/market-stream");

eventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
console.log("Incoming SSE Data:", parsedData); // Log the incoming data

if (parsedData?.data?.[0]?.content) {
parsedData.data[0].content.forEach((entry) => {
const timestamp = new Date(
parsedData.data[0].timestamp,
).toLocaleTimeString();

// Persist high/low prices and close price if they are undefined
const highPrice = entry[10] ?? lastHighPrice;
const lowPrice = entry[11] ?? lastLowPrice;

if (entry[10] !== undefined) setLastHighPrice(entry[10]);
if (entry[11] !== undefined) setLastLowPrice(entry[11]);
if (entry[12] !== undefined) setClosePrice(entry[12]);

setData((prevData) => [
...prevData.slice(-100), // Keep only the last 100 points to limit memory usage
{
time: timestamp,
bidPrice: entry[1],
askPrice: entry[2],
lastSize: entry[9],
highPrice,
lowPrice,
netChange: entry[18],
},
]);
});
if (!eventSourceRef.current) {
console.log("Establishing SSE connection...");
const eventSource = new EventSource("/api/market-stream");
eventSourceRef.current = eventSource;

eventSource.onopen = () => {
console.log("SSE connection established.");
setError(null); // Clear any existing error state
};

eventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
console.log("Incoming SSE Data:", parsedData);

if (parsedData?.data?.[0]?.content) {
parsedData.data[0].content.forEach((entry) => {
const timestamp = new Date(
parsedData.data[0].timestamp,
).toLocaleTimeString();

const highPrice = entry[10] ?? lastHighPrice;
const lowPrice = entry[11] ?? lastLowPrice;

if (entry[10] !== undefined) setLastHighPrice(entry[10]);
if (entry[11] !== undefined) setLastLowPrice(entry[11]);
if (entry[12] !== undefined) setClosePrice(entry[12]);

setData((prevData) => [
...prevData.slice(-100), // Keep only the last 100 points
{
time: timestamp,
bidPrice: entry[1],
askPrice: entry[2],
lastSize: entry[9],
highPrice,
lowPrice,
netChange: entry[18],
},
]);
});
}
} catch (error) {
console.error("Error processing SSE data:", error);
}
} catch (error) {
console.error("Error processing SSE data:", error);
};

eventSource.onerror = () => {
console.error("SSE connection error. Reconnecting...");
setError("Connection lost. Reconnecting...");
eventSource.close();

// Reconnect after a delay
setTimeout(() => {
eventSourceRef.current = null; // Clear the ref for reconnection
}, 5000);
};
}

return () => {
if (eventSourceRef.current) {
console.log("Cleaning up SSE connection...");
eventSourceRef.current.close();
eventSourceRef.current = null;
}
};

return () => eventSource.close();
}, [lastHighPrice, lastLowPrice]);
}, [lastHighPrice, lastLowPrice]); // Dependencies ensure prices are preserved across re-renders

return (
<div className="grid grid-cols-2 gap-4 p-4">
Expand Down
20 changes: 9 additions & 11 deletions examples/schwab-dashboard-nextjs/package.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,33 @@
{
"name": "schwab-dashboard-nextjs",
"Author": "Jason Levitt",
"description": "A NextJS stock dashboard Using recharts, Tailwind, and the schwab-client-js library",
"version": "1.0.0",
"name": "nextjs-dashboard",
"version": "1.0.1",
"private": true,
"license": "MIT",
"type": "module",
"scripts": {
"format": "prettier --write .",
"dev": "next dev & open http://localhost:3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"dev": "concurrently --kill-others-on-fail \"next dev\" \"wait-on http://localhost:3000 && opener http://localhost:3000\""
},
"dependencies": {
"next": "15.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"recharts": "^2.15.0",
"schwab-client-js": "^1.0.6",
"schwab-client-js": "^1.0.0",
"typescript": "^5.7.2"
},
"devDependencies": {
"eslint": "^9",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.17.0",
"@next/eslint-plugin-next": "^15.1.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^9",
"eslint-config-next": "^15.1.1",
"eslint-plugin-react": "^7.37.2",
"open-cli": "^8.0.0",
"concurrently": "^9.1.0",
"opener": "^1.5.2",
"wait-on": "^8.0.1",
"postcss": "^8",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.1"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "schwab-client-js",
"author": "Jason Levitt",
"description": "Javascript client for the Schwab API",
"version": "1.0.7",
"version": "1.0.8",
"homepage": "https://github.com/slimandslam/schwab-client-js#readme",
"main": "./dist/schwab-client-js.js",
"bugs": {
Expand Down

0 comments on commit dcd03d4

Please sign in to comment.