Add README and initial project structure for WebSocket chat application

* Created README.md for project overview and setup instructions.
* Updated App component to use selector for todo items.
* Enhanced TodoItemComponent styling and structure.
* Introduced new Redux selectors for better state management.
* Added initial configuration files for RequireJS and Bun.
* Established project structure for WebSocket chat application with server and frontend components.
* Included necessary dependencies and configurations for TypeScript and Vite.
This commit is contained in:
2025-09-30 03:19:52 -04:00
parent 0cc0ce13e7
commit 0345f3d2d0
71 changed files with 996 additions and 1065 deletions

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# Playground
A repository to play around in

View File

@@ -1,8 +1,10 @@
import {useAppSelector} from './store'; import {selectTodoItems} from "./store/slices";
import {TodoItem} from './entities/TodoItem';
import {TodoItemComponent} from './components/TodoItem/TodoItem'; import {TodoItemComponent} from './components/TodoItem/TodoItem';
import { useAppSelector } from "./store";
export default function App() { export default function App() {
const items = useAppSelector(state => state.todo.items); const items = useAppSelector(selectTodoItems);
return ( return (
<div style={{width: '100vw', height: '100vh'}}> <div style={{width: '100vw', height: '100vh'}}>

View File

@@ -6,7 +6,14 @@ export interface ITodoItemProps {
} }
export const TodoItemComponent = ({todo: {id, title, notes, createdAt, updatedAt, dueAt, isArchived, isDone}}: ITodoItemProps): JSX.Element => { export const TodoItemComponent = ({todo: {id, title, notes, createdAt, updatedAt, dueAt, isArchived, isDone}}: ITodoItemProps): JSX.Element => {
return <div key={id} style={{border: '2px solid black', display: 'flex', width: '800px', height: '400px', flexDirection: 'column', margin: 'auto'}}> return <div key={id} style={{
border: '2px solid black',
display: 'flex',
width: '800px',
height: '250px',
flexDirection: 'column',
margin: 'auto'
}}>
<header >{title}</header> <header >{title}</header>
<div> <div>
<textarea defaultValue={notes ?? ''}></textarea> <textarea defaultValue={notes ?? ''}></textarea>

View File

@@ -1,5 +1,5 @@
import type {Nullable} from '../../types/definedTypes'; import type {Nullable} from '../../types/definedTypes';
import {createSlice} from '@reduxjs/toolkit'; import {createSelector, createSlice} from '@reduxjs/toolkit';
import {TodoItem} from '../../entities/TodoItem'; import {TodoItem} from '../../entities/TodoItem';
interface ITodoSliceState { interface ITodoSliceState {
@@ -20,6 +20,7 @@ const initialTodoState: ITodoSliceState = {
error: null error: null
}; };
export const TodoSlice = createSlice({ export const TodoSlice = createSlice({
name: 'todo', name: 'todo',
initialState: initialTodoState, initialState: initialTodoState,
@@ -61,5 +62,8 @@ export const TodoSlice = createSlice({
//} //}
}); });
const selectTodos = (state: {todo: ITodoSliceState}) => state.todo;
export const selectTodoItems = createSelector([selectTodos], todo => todo.items);
export const {appendTodoItem} = TodoSlice.actions; export const {appendTodoItem} = TodoSlice.actions;
export const todoSliceStateReducer = TodoSlice.reducer; export const todoSliceStateReducer = TodoSlice.reducer;

View File

@@ -0,0 +1 @@
../../CLAUDE.md

34
RequireJs/require-js-node/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -0,0 +1,111 @@
---
description: Use Bun instead of Node.js, npm, pnpm, or vite.
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
alwaysApply: false
---
Default to using Bun instead of Node.js.
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
- Use `bun test` instead of `jest` or `vitest`
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
- Bun automatically loads .env, so don't use dotenv.
## APIs
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
- `Bun.redis` for Redis. Don't use `ioredis`.
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
- `WebSocket` is built-in. Don't use `ws`.
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
- Bun.$`ls` instead of execa.
## Testing
Use `bun test` to run tests.
```ts#index.test.ts
import { test, expect } from "bun:test";
test("hello world", () => {
expect(1).toBe(1);
});
```
## Frontend
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
Server:
```ts#index.ts
import index from "./index.html"
Bun.serve({
routes: {
"/": index,
"/api/users/:id": {
GET: (req) => {
return new Response(JSON.stringify({ id: req.params.id }));
},
},
},
// optional websocket support
websocket: {
open: (ws) => {
ws.send("Hello, world!");
},
message: (ws, message) => {
ws.send(message);
},
close: (ws) => {
// handle close
}
},
development: {
hmr: true,
console: true,
}
})
```
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
```html#index.html
<html>
<body>
<h1>Hello, world!</h1>
<script type="module" src="./frontend.tsx"></script>
</body>
</html>
```
With the following `frontend.tsx`:
```tsx#frontend.tsx
import React from "react";
// import .css files directly and it works
import './index.css';
import { createRoot } from "react-dom/client";
const root = createRoot(document.body);
export default function Frontend() {
return <h1>Hello, world!</h1>;
}
root.render(<Frontend />);
```
Then, run index.ts
```sh
bun --hot ./index.ts
```
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.

View File

@@ -0,0 +1,15 @@
# require-js-node
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run src/index.ts
```
This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.

View File

@@ -0,0 +1,37 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "require-js-node",
"dependencies": {
"requirejs": "2.3.7",
},
"devDependencies": {
"@types/bun": "latest",
"@types/requirejs": "2.1.37",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="],
"@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="],
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
"@types/requirejs": ["@types/requirejs@2.1.37", "", {}, "sha512-jmFgr3mwN2NSmtRP6IpZ2nfRS7ufSXuDYQ6YyPFArN8x5dARQcD/DXzT0J6NYbvquVT4pg9K9HWdi6e6DZR9iQ=="],
"bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"requirejs": ["requirejs@2.3.7", "", { "bin": { "r.js": "bin/r.js", "r_js": "bin/r.js" } }, "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "require-js-node",
"module": "src/index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest",
"@types/requirejs": "2.1.37"
},
"peerDependencies": {
"typescript": "^5"
},
"dependencies": {
"requirejs": "2.3.7"
}
}

View File

@@ -0,0 +1,7 @@
// requireJs
import {requirejs} from 'requirejs';
requirejs.config({
})

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "es6",
"module": "AMD",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": ["src"],
"exclude": [],
}

View File

@@ -0,0 +1,5 @@
{
"name": "@techniker-me/websocket-chat",
"version": "0.0.0",
"workspaces": ["server", "frontend"]
}

View File

@@ -0,0 +1,12 @@
{
"arrowParens": "avoid",
"bracketSameLine": true,
"bracketSpacing": false,
"printWidth": 160,
"semi": true,
"singleAttributePerLine": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "none",
"useTabs": false
}

View File

@@ -0,0 +1,60 @@
# WebSocket Chat Monorepo
A real-time WebSocket chat application built with TypeScript, Express, and Vite.
## Project Structure
```
├── apps/
│ ├── server/ # Express WebSocket server
│ └── frontend/ # React/Vite frontend
├── packages/
│ └── shared-types/ # Shared TypeScript types
└── package.json # Root workspace configuration
```
## Quick Start
```bash
# Install all dependencies
npm install
# Start both server and frontend in development mode
npm start
# Or run individually:
npm run dev:server # Start server only
npm run dev:frontend # Start frontend only
```
## Available Scripts
- `npm start` - Build shared types and start both server and frontend
- `npm run dev:all` - Start both server and frontend concurrently
- `npm run dev:server` - Start server only
- `npm run dev:frontend` - Start frontend only
- `npm run build` - Build all packages
- `npm run typecheck` - Type check all packages
- `npm run lint` - Lint all packages
- `npm run clean` - Clean all build outputs
## Development
The monorepo uses npm workspaces for dependency management and TypeScript project references for type checking across packages.
### Server
- Runs on `http://localhost:3000`
- WebSocket endpoint: `ws://localhost:3000/ws`
- Health check: `http://localhost:3000/ping`
### Frontend
- Runs on `http://localhost:5173` (Vite default)
- Connects to WebSocket server for real-time messaging
## Features
- Real-time WebSocket communication
- Ping/pong latency measurement
- Shared TypeScript types across packages
- Hot reload for development
- Monorepo workspace management

View File

@@ -0,0 +1,22 @@
{
"name": "@techniker-me/websocket-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"typecheck": "tsc",
"clean": "rm -rf dist",
"lint": "echo 'No linting configured for frontend'"
},
"devDependencies": {
"typescript": "5.9.2",
"vite": "7.1.7"
},
"dependencies": {
"@techniker-me/logger": "0.0.15",
"@techniker-me/tools": "2025.0.16"
}
}

View File

@@ -25,6 +25,11 @@ messagesElement.id = 'messages-container';
const messageInput = document.createElement('input'); const messageInput = document.createElement('input');
messageInput.type = 'text'; messageInput.type = 'text';
messageInput.disabled = true; messageInput.disabled = true;
messageInput.onkeydown = ({target}) => {
if (target.value === 'enter') {
sendMessage()
}
};
const actionButton = document.createElement('button'); const actionButton = document.createElement('button');
actionButton.innerText = 'Set your username!'; actionButton.innerText = 'Set your username!';
@@ -32,8 +37,8 @@ actionButton.innerText = 'Set your username!';
disposables.add(websocket.on('message', (message: any) => { disposables.add(websocket.on('message', (message: any) => {
if (message.pong) { if (message.pong) {
console.log('[WebSocket] Received message', message); console.log('[WebSocket] Received message', message);
// Use server-calculated RTT
const rtt = message.pong.rtt || 0; const rtt = message.pong.rtt || 0;
rttContainer.innerHTML = `RTT: [<span id="rtt-value">${rtt.toFixed(2)}</span>] ms`; rttContainer.innerHTML = `RTT: [<span id="rtt-value">${rtt.toFixed(2)}</span>] ms`;
} else { } else {
messagesElement.innerHTML += `<div class="message"> [${new Date(message.sentAt).toLocaleString("en-US", {timeStyle: 'short'})}]${message.message.author}: ${message.message.payload}</div>`; messagesElement.innerHTML += `<div class="message"> [${new Date(message.sentAt).toLocaleString("en-US", {timeStyle: 'short'})}]${message.message.author}: ${message.message.payload}</div>`;
@@ -50,9 +55,11 @@ chatContainer.append(actionButton);
appContainer.append(chatContainer); appContainer.append(chatContainer);
actionButton.onclick = () => { actionButton.onclick = () => {
const usernamePrompt = prompt('Enter your name:'); const usernamePrompt = prompt('Enter your name:');
if (!usernamePrompt) { if (!usernamePrompt) {
return; return;
} }
username = usernamePrompt; username = usernamePrompt;
messageInput.disabled = false; messageInput.disabled = false;
messageInput.placeholder = 'Enter a message...'; messageInput.placeholder = 'Enter a message...';
@@ -62,6 +69,7 @@ actionButton.onclick = () => {
function sendMessage() { function sendMessage() {
console.log('[actionButton] Set username'); console.log('[actionButton] Set username');
if (!messageInput.value) { if (!messageInput.value) {
return; return;
} }

View File

@@ -0,0 +1,30 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": false,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"references": [
{ "path": "../../packages/shared-types" }
]
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 680 B

After

Width:  |  Height:  |  Size: 680 B

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,15 +2,17 @@
"name": "@techniker-me/websocket-server", "name": "@techniker-me/websocket-server",
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"private": true,
"scripts": { "scripts": {
"preinstall": "npm prune",
"format": "prettier --write .", "format": "prettier --write .",
"prelint": "npm install", "prelint": "npm install",
"lint": "eslint --max-warnings 0 .", "lint": "eslint --max-warnings 0 .",
"prelint:fix": "npm run format", "prelint:fix": "npm run format",
"lint:fix": "eslint --fix .", "lint:fix": "eslint --fix .",
"dev": "tsx watch --clear-screen=false src/index.ts", "dev": "tsx watch --clear-screen=false src/index.ts",
"typecheck": "tsc" "build": "tsc",
"typecheck": "tsc",
"clean": "rm -rf dist"
}, },
"author": "Alexander Zinn", "author": "Alexander Zinn",
"license": "ISC", "license": "ISC",
@@ -44,6 +46,7 @@
"@techniker-me/tools": "2025.0.16", "@techniker-me/tools": "2025.0.16",
"body-parser": "2.2.0", "body-parser": "2.2.0",
"cors": "2.8.5", "cors": "2.8.5",
"express": "5.1.0",
"lru-cache": "11.2.2", "lru-cache": "11.2.2",
"moment": "2.30.1", "moment": "2.30.1",
"morgan": "1.10.1", "morgan": "1.10.1",

View File

@@ -1,17 +1,13 @@
{ {
"extends": "../../tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "es2022", "target": "es2022",
"lib": ["es2022"], "lib": ["es2022"],
"moduleResolution": "node", "moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"moduleDetection": "force",
"module": "Preserve", "module": "Preserve",
"resolveJsonModule": true, "resolveJsonModule": true,
"allowJs": false, "allowJs": false,
"esModuleInterop": true,
"isolatedModules": true, "isolatedModules": true,
"strict": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"noImplicitOverride": true, "noImplicitOverride": true,
@@ -20,9 +16,15 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"forceConsistentCasingInFileNames": true, "noEmit": false,
"noEmit": true "outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules"] "exclude": ["node_modules", "dist"],
"references": [
{ "path": "../../packages/shared-types" }
]
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,26 @@
{ {
"dependencies": { "name": "@techniker-me/websocket-chat",
"body-parser": "^2.2.0", "version": "0.0.0",
"compression": "^1.8.1", "private": true,
"express": "^5.1.0" "workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "npm run build --workspaces",
"build:types": "npm run build -w @techniker-me/websocket-shared-types",
"dev": "npm run dev --workspaces --if-present",
"dev:server": "npm run dev -w @techniker-me/websocket-server",
"dev:frontend": "npm run dev -w @techniker-me/websocket-frontend",
"dev:all": "concurrently \"npm run dev:server\" \"npm run dev:frontend\"",
"clean": "npm run clean --workspaces --if-present",
"lint": "npm run lint --workspaces --if-present",
"typecheck": "npm run typecheck --workspaces --if-present",
"start": "npm run build:types && npm run dev:all"
}, },
"devDependencies": { "devDependencies": {
"@types/compression": "^1.8.1", "typescript": "5.9.2",
"@types/express": "^5.0.3" "concurrently": "8.2.2"
} },
"author": "Alexander Zinn"
} }

View File

@@ -0,0 +1,17 @@
{
"name": "@techniker-me/websocket-shared-types",
"version": "0.0.0",
"type": "module",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"clean": "rm -rf dist",
"typecheck": "tsc"
},
"devDependencies": {
"typescript": "~5.8.3"
},
"dependencies": {}
}

View File

@@ -0,0 +1,41 @@
// Shared types for WebSocket communication
export type Milliseconds = number;
export type Seconds = number;
export interface PingMessage {
ping: {
sentAt: number;
};
}
export interface PongMessage {
pong: {
sentAt: number;
rtt: number;
};
}
export interface ChatMessage {
message: {
author: string;
recipient: string;
payload: string;
};
}
export type WebSocketMessage = PingMessage | PongMessage | ChatMessage;
export enum WebSocketConnectionStatus {
Connecting = 'Connecting',
Open = 'Open',
Closed = 'Closed',
Error = 'Error'
}
export interface ExtendedWebSocket {
id: string;
remoteAddress: string;
isOpen(): boolean;
isClosed(): boolean;
}

View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"noEmit": false,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"composite": true,
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo"
},
"references": [
{ "path": "./packages/shared-types" },
{ "path": "./apps/server" },
{ "path": "./apps/frontend" }
],
"files": []
}

24
Web/bfcache/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,133 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "bfcache",
"devDependencies": {
"typescript": "~5.8.3",
"vite": "^7.1.2",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.50.2", "", { "os": "android", "cpu": "arm" }, "sha512-uLN8NAiFVIRKX9ZQha8wy6UUs06UNSZ32xj6giK/rmMXAgKahwExvK6SsmgU5/brh4w/nSgj8e0k3c1HBQpa0A=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.50.2", "", { "os": "android", "cpu": "arm64" }, "sha512-oEouqQk2/zxxj22PNcGSskya+3kV0ZKH+nQxuCCOGJ4oTXBdNTbv+f/E3c74cNLeMO1S5wVWacSws10TTSB77g=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.50.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-OZuTVTpj3CDSIxmPgGH8en/XtirV5nfljHZ3wrNwvgkT5DQLhIKAeuFSiwtbMto6oVexV0k1F1zqURPKf5rI1Q=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.50.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Wa/Wn8RFkIkr1vy1k1PB//VYhLnlnn5eaJkfTQKivirOvzu5uVd2It01ukeQstMursuz7S1bU+8WW+1UPXpa8A=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.50.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QkzxvH3kYN9J1w7D1A+yIMdI1pPekD+pWx7G5rXgnIlQ1TVYVC6hLl7SOV9pi5q9uIDF9AuIGkuzcbF7+fAhow=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.50.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dkYXB0c2XAS3a3jmyDkX4Jk0m7gWLFzq1C3qUnJJ38AyxIF5G/dyS4N9B30nvFseCfgtCEdbYFhk0ChoCGxPog=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.50.2", "", { "os": "linux", "cpu": "arm" }, "sha512-9VlPY/BN3AgbukfVHAB8zNFWB/lKEuvzRo1NKev0Po8sYFKx0i+AQlCYftgEjcL43F2h9Ui1ZSdVBc4En/sP2w=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.50.2", "", { "os": "linux", "cpu": "arm" }, "sha512-+GdKWOvsifaYNlIVf07QYan1J5F141+vGm5/Y8b9uCZnG/nxoGqgCmR24mv0koIWWuqvFYnbURRqw1lv7IBINw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.50.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-df0Eou14ojtUdLQdPFnymEQteENwSJAdLf5KCDrmZNsy1c3YaCNaJvYsEUHnrg+/DLBH612/R0xd3dD03uz2dg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.50.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-iPeouV0UIDtz8j1YFR4OJ/zf7evjauqv7jQ/EFs0ClIyL+by++hiaDAfFipjOgyz6y6xbDvJuiU4HwpVMpRFDQ=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-OL6KaNvBopLlj5fTa5D5bau4W82f+1TyTZRr2BdnfsrnQnmdxh4okMxR2DcDkJuh4KeoQZVuvHvzuD/lyLn2Kw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.50.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-I21VJl1w6z/K5OTRl6aS9DDsqezEZ/yKpbqlvfHbW0CEF5IL8ATBMuUx6/mp683rKTK8thjs/0BaNrZLXetLag=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-Hq6aQJT/qFFHrYMjS20nV+9SKrXL2lvFBENZoKfoTH2kKDOJqff5OSJr4x72ZaG/uUn+XmBnGhfr4lwMRrmqCQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.50.2", "", { "os": "linux", "cpu": "none" }, "sha512-82rBSEXRv5qtKyr0xZ/YMF531oj2AIpLZkeNYxmKNN6I2sVE9PGegN99tYDLK2fYHJITL1P2Lgb4ZXnv0PjQvw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.50.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-4Q3S3Hy7pC6uaRo9gtXUTJ+EKo9AKs3BXKc2jYypEcMQ49gDPFU2P1ariX9SEtBzE5egIX6fSUmbmGazwBVF9w=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.50.2", "", { "os": "linux", "cpu": "x64" }, "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.50.2", "", { "os": "linux", "cpu": "x64" }, "sha512-HPNJwxPL3EmhzeAnsWQCM3DcoqOz3/IC6de9rWfGR8ZCuEHETi9km66bH/wG3YH0V3nyzyFEGUZeL5PKyy4xvw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.50.2", "", { "os": "none", "cpu": "arm64" }, "sha512-nMKvq6FRHSzYfKLHZ+cChowlEkR2lj/V0jYj9JnGUVPL2/mIeFGmVM2mLaFeNa5Jev7W7TovXqXIG2d39y1KYA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.50.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-eFUvvnTYEKeTyHEijQKz81bLrUQOXKZqECeiWH6tb8eXXbZk+CXSG2aFrig2BQ/pjiVRj36zysjgILkqarS2YA=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.50.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-cBaWmXqyfRhH8zmUxK3d3sAhEWLrtMjWBRwdMMHJIXSjvjLKvv49adxiEz+FJ8AP90apSDDBx2Tyd/WylV6ikA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.50.2", "", { "os": "win32", "cpu": "x64" }, "sha512-APwKy6YUhvZaEoHyM+9xqmTpviEI+9eL7LoCH+aLcvWYHJ663qG5zx7WzWZY+a9qkg5JtzcMyJ9z0WtQBMDmgA=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"rollup": ["rollup@4.50.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.50.2", "@rollup/rollup-android-arm64": "4.50.2", "@rollup/rollup-darwin-arm64": "4.50.2", "@rollup/rollup-darwin-x64": "4.50.2", "@rollup/rollup-freebsd-arm64": "4.50.2", "@rollup/rollup-freebsd-x64": "4.50.2", "@rollup/rollup-linux-arm-gnueabihf": "4.50.2", "@rollup/rollup-linux-arm-musleabihf": "4.50.2", "@rollup/rollup-linux-arm64-gnu": "4.50.2", "@rollup/rollup-linux-arm64-musl": "4.50.2", "@rollup/rollup-linux-loong64-gnu": "4.50.2", "@rollup/rollup-linux-ppc64-gnu": "4.50.2", "@rollup/rollup-linux-riscv64-gnu": "4.50.2", "@rollup/rollup-linux-riscv64-musl": "4.50.2", "@rollup/rollup-linux-s390x-gnu": "4.50.2", "@rollup/rollup-linux-x64-gnu": "4.50.2", "@rollup/rollup-linux-x64-musl": "4.50.2", "@rollup/rollup-openharmony-arm64": "4.50.2", "@rollup/rollup-win32-arm64-msvc": "4.50.2", "@rollup/rollup-win32-ia32-msvc": "4.50.2", "@rollup/rollup-win32-x64-msvc": "4.50.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"vite": ["vite@7.1.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ=="],
}
}

View File

@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="/src/styles.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bfcache</title>
</head>
<body>
<div id="app" class="flex-column-container">
<div class="flex-row-container">
<h1>bfcache</h1>
<div>
<br/>
<br/>
<br/>
<br/>
<p>WebSocket status: <span id="websocket-status">Offline</span></p>
</div>
</div>
<div class="flex-row-container">
<div>
<h2>Messages</h2>
<ul id="messages"></ul>
</div>
</div>
<div class="flex-row-container">
<div>
<h2>Send Message</h2>
<input type="text" id="message-input" required/>
<button id="send-button">Send</button>
</div>
</div>
</div>
<dialog id="login-dialog">
<form id="login-form">
<div>
<input type="text" id="username-input" autocomplete="username" placeholder="Enter a username" required/>
</div>
<!-- <input type="password" id="password-input" autocomplete="current-password" placeholder="Password" required/> -->
<div>
<button type="submit" id="login-button">Login</button>
</div>
</form>
</dialog>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -1,5 +1,5 @@
{ {
"name": "websocket", "name": "bfcache",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
@@ -10,10 +10,6 @@
}, },
"devDependencies": { "devDependencies": {
"typescript": "~5.8.3", "typescript": "~5.8.3",
"vite": "^7.1.7" "vite": "^7.1.2"
},
"dependencies": {
"@techniker-me/logger": "0.0.15",
"@techniker-me/tools": "2025.0.16"
} }
} }

View File

@@ -0,0 +1,84 @@
const websocketStatus = document.getElementById('websocket-status') as HTMLSpanElement;
const messages = document.getElementById('messages') as HTMLUListElement;
const messageInput = document.getElementById('message-input') as HTMLInputElement;
const sendButton = document.getElementById('send-button') as HTMLButtonElement;
const loginDialog = document.getElementById('login-dialog') as HTMLDialogElement;
const usernameInput = document.getElementById('username-input') as HTMLInputElement;
const loginForm = document.getElementById('login-form') as HTMLFormElement;
let username: string = '';
let websocket: WebSocket;
window.addEventListener('beforeunload', () => {
disconnect(websocket);
});
function connect(): Promise<WebSocket> {
return new Promise((resolve, reject) => {
websocket = new WebSocket(`ws://${window.location.hostname}:4444`)
websocket.onopen = () => {
websocketStatus.textContent = 'Connected';
websocket.send(JSON.stringify({
type: 'login',
username
}));
resolve(websocket);
};
websocket.onerror = (event) => {
reject(event);
};
websocket.onclose = () => {
websocketStatus.textContent = 'Offline';
};
websocket.onclose = () => {
websocketStatus.textContent = 'Offline';
};
websocket.onerror = (event: Event) => {
websocketStatus.textContent = `Error [${event.toString()}]`;
};
websocket.onmessage = (event) => {
console.log('WebSocket message received:', event.data);
const message = document.createElement('li');
message.textContent = event.data;
messages.appendChild(message);
};
sendButton.onclick = () => sendMessage(websocket, messageInput.value);
});
}
function disconnect(websocket: WebSocket) {
websocket.close();
}
function login() {
connect();
loginDialog.close();
}
function sendMessage(websocket: WebSocket, message: string) {
if (!message) {
return;
}
console.log('Sending message:', message);
websocket.send(message);
messageInput.value = '';
}
loginForm.onsubmit = (event) => {
event.preventDefault();
loginDialog.close();
login(usernameInput.value);
};
loginDialog.showModal();

View File

@@ -0,0 +1,49 @@
body {
background-color: #f0f0f0;
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
}
h3 {
text-align: center;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.flex-column-container {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.flex-row-container {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.flex-item {
flex: 1;
}
#login-button {
margin: 10px auto;
width: 100%;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#login-button:hover {
background-color: #0056b3;
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -4,7 +4,6 @@
"useDefineForClassFields": true, "useDefineForClassFields": true,
"module": "ESNext", "module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"], "lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"skipLibCheck": true, "skipLibCheck": true,
/* Bundler mode */ /* Bundler mode */
@@ -18,7 +17,7 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"erasableSyntaxOnly": false, "erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },

View File

@@ -0,0 +1,3 @@
{
"idf.pythonInstallPath": "/opt/homebrew/bin/python3"
}

View File

@@ -1,5 +1,6 @@
import {LoggerFactory} from '@techniker-me/logger'; import {LoggerFactory} from '@techniker-me/logger';
import WebSocketServerFactory from './net/websockets/WebSocketServerFactory'; import WebSocketServerFactory from './net/websockets/WebSocketServerFactory';
import type { WebSocketHandler } from 'bun';
LoggerFactory.setLoggingLevel('All'); LoggerFactory.setLoggingLevel('All');
@@ -15,7 +16,7 @@ const server = Bun.serve({
return new Response('hi'); return new Response('hi');
}, },
websocket: webSocketRelayServer websocket: webSocketRelayServer as WebSocketHandler<undefined>
}); });
process.on('SIGINT', () => { process.on('SIGINT', () => {

View File

@@ -95,4 +95,102 @@ export default class WebSocketServer {
get perMessageDeflate(): PerMessageDeflate { get perMessageDeflate(): PerMessageDeflate {
return this._perMessageDeflate; return this._perMessageDeflate;
} }
public start(connectDelegate: (req, websocket, headers) => void, requestDelegate: (req, websocket, headers) => void, disconnectDelegate: (req, websocket, headers) => void, pongDelegate: (req, websocket, headers) => void) {
if (this._server) {
throw new Error('Can only start server once');
}
assert.assertFunction('connectDelegate', connectDelegate);
assert.assertFunction('requestDelegate', requestDelegate);
assert.assertFunction('disconnectDelegate', disconnectDelegate);
assert.assertFunction('pongDelegate', pongDelegate);
const serverOptions = _.cloneDeep(this._parameters);
log.info('Websocket server listening on port [%s] bound to [%s]', this._httpServer.address().port, _.get(serverOptions, ['path']));
serverOptions.noServer = true;
this._server = new ws.Server(serverOptions);
this._extensions = new WebsocketExtensions();
this._extensions.add(deflate);
// TODO Prevent upgrade request from being handled by this WS server if it's not within the configured path
this._server._server = this._httpServer;
this._httpServer.on('listening', this._server.emit.bind(this._server, 'listening'));
this._httpServer.on('error', this._server.emit.bind(this._server, 'error'));
this._httpServer.on('upgrade', (req, socket, head) => {
if (!_.startsWith(req.url, serverOptions.path)) {
return;
}
this._server.handleUpgrade(req, socket, head, ws => {
this._server.emit('connection', ws, req);
});
});
this._server.on('error', e => {
log.error('An error occurred on websocket', e);
});
this._server.on('connection', (connection, req) => {
let closed = false;
try {
connection.id = randomstring.generate(connectionIdLength);
connection.remoteAddress = getRemoteAddress.call(this, connection, req);
log.debug('[%s] connected from [%s] with headers [%j]', connection.id, connection.remoteAddress, req.headers);
connection.isOpen = () => connection.readyState === ws.OPEN;
connection.isClosed = () => connection.readyState === ws.CLOSED;
connection.on('error', e => {
log.error('An error occurred on websocket', e);
});
connection.on('message', message => {
try {
requestDelegate(connection, message);
} catch (e) {
log.error('Request handler failed for message [%s]', message, e);
}
});
connection.on('close', (reasonCode, description) => {
if (closed) {
log.warn('[%s] Multiple close events [%s] [%s] [%s]', connection.id, connection.remoteAddress, reasonCode, description);
return;
}
closed = true;
try {
disconnectDelegate(connection, reasonCode, description);
} catch (e) {
log.error('Disconnect handler failed', e);
}
});
connection.on('pong', message => {
try {
pongDelegate(connection, message);
} catch (e) {
log.error('Pong handler failed', e);
}
});
return connectDelegate(connection, _.get(req, ['headers']));
} catch (e) {
log.error('Accept/connect handler failed', e);
}
}
);
return this;
}
}
} }

View File

@@ -9,11 +9,13 @@ export default class WebSocketServerFactory {
const webSocketRelayServerOptions: WebSocketServerOptions = { const webSocketRelayServerOptions: WebSocketServerOptions = {
onSocketError: (client, error) => logger.error(`Error: [%o] [${error.message}]`, client), onSocketError: (client, error) => logger.error(`Error: [%o] [${error.message}]`, client),
onSocketOpen: client => { onSocketOpen: client => {
console.log('New WebSocketClient [%o]', client);
logger.debug('New WebSocketClient [%o]', client); logger.debug('New WebSocketClient [%o]', client);
clients.add(client); clients.add(client);
}, },
onSocketMessage: (fromClient, message) => { onSocketMessage: (fromClient, message) => {
console.log('Relaying message [%o]', message);
logger.debug(`Relaying message [%o]`, message); logger.debug(`Relaying message [%o]`, message);
for (const client of clients) { for (const client of clients) {
@@ -24,7 +26,10 @@ export default class WebSocketServerFactory {
client.send(message); client.send(message);
} }
}, },
onSocketClose: client => clients.delete(client), onSocketClose: client => {
console.log('Client closed [%o]', client);
clients.delete(client);
},
onSocketDrain: client => logger.debug('Client drain [%o]', client), onSocketDrain: client => logger.debug('Client drain [%o]', client),
publishToSelf: false publishToSelf: false
}; };