Compare commits

...

15 Commits

Author SHA1 Message Date
9ac91fd166 v6.0.1
Some checks failed
Default (tags) / security (push) Failing after 20s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-04 00:03:23 +00:00
b4e26d6d6a fix(web_inject): Use TypedSocket status API in web_inject and bump dependencies 2025-12-04 00:03:23 +00:00
1885eb78e5 v6.0.0
Some checks failed
Default (tags) / security (push) Failing after 22s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-03 09:41:56 +00:00
8b4c5918e9 BREAKING CHANGE(servertools.Server.addTypedSocket): Deprecate Server.addTypedSocket and upgrade typedsocket to v4; make addTypedSocket a no-op and log a deprecation warning. Bump tsbundle devDependency. 2025-12-03 09:41:56 +00:00
c6792396df v5.0.0
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-02 21:10:25 +00:00
fc6829f607 BREAKING CHANGE(devtools): Switch /reloadcheck endpoint from GET to POST in DevToolsController 2025-12-02 21:10:25 +00:00
424b742f84 v4.1.1
Some checks failed
Default (tags) / security (push) Failing after 16s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-02 21:03:57 +00:00
c25daba1c1 fix(classes.typedserver): Instantiate and register DevToolsController only when injectReload is enabled; compile ControllerRegistry routes after registration 2025-12-02 21:03:57 +00:00
dce2e926e4 v4.1.0
Some checks failed
Default (tags) / security (push) Failing after 18s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-02 20:47:11 +00:00
27c96949a1 feat(TypedServer): Integrate SmartServe controller routing; add built-in routes controller and refactor TypedServer to use controllers and FileServer 2025-12-02 20:47:11 +00:00
c17d6dac35 feat: Refactor TypedServer to use SmartServe and introduce new request handlers
- Removed legacy servertools and Express dependencies in favor of SmartServe.
- Introduced DevToolsHandler and TypedRequestHandler for handling specific routes.
- Added support for custom route registration with regex parsing.
- Implemented sitemap and feed handling with dedicated helper classes.
- Enhanced HTML response handling with reload script injection.
- Updated UtilityServiceServer and UtilityWebsiteServer to utilize new TypedServer API.
- Removed deprecated compression options and Express-based route handling.
- Added comprehensive request handling for various endpoints including robots.txt, manifest.json, and sitemap.
- Improved error handling and response formatting across the server.
2025-12-02 20:26:34 +00:00
1c0c88a7cb v4.0.0
Some checks failed
Default (tags) / security (push) Failing after 18s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-12-02 09:16:42 +00:00
8557c769fa BREAKING CHANGE(typedserver): Migrate to new push.rocks packages and async smartfs API; replace smartchok with smartwatch; update deps and service worker handling 2025-12-02 09:16:42 +00:00
bce84c6838 v3.0.80
Some checks failed
Default (tags) / security (push) Failing after 23s
Default (tags) / test (push) Failing after 13s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-11-19 21:12:16 +00:00
ba08fcdebc fix(dependencies): Bump dependencies and devDependencies to updated versions 2025-11-19 21:12:16 +00:00
23 changed files with 4687 additions and 4016 deletions

View File

@@ -1,5 +1,77 @@
# Changelog
## 2025-12-04 - 6.0.1 - fix(web_inject)
Use TypedSocket status API in web_inject and bump dependencies
- ts_web_inject: switch from typedsocket.addTag + eventSubject to await typedsocket.setTag + statusSubject; update logging and handle 'reconnecting' status as backend connection loss
- Await setTag call to ensure tag is applied before relying on socket state
- Bump dependencies: @api.global/typedrequest -> ^3.1.11, @api.global/typedsocket -> ^4.1.0, @push.rocks/smartserve -> ^1.1.2
## 2025-12-03 - 6.0.0 - BREAKING CHANGE(servertools.Server.addTypedSocket)
Deprecate Server.addTypedSocket and upgrade typedsocket to v4; make addTypedSocket a no-op and log a deprecation warning. Bump tsbundle devDependency.
- Upgrade dependency @api.global/typedsocket to ^4.0.0. TypedSocket v4 no longer supports attaching to an existing Express server.
- Deprecate servertools.Server.addTypedSocket(): the method is now a no-op and emits a console.warn directing users to use TypedServer with SmartServe integration for WebSocket support.
- Bump devDependency @git.zone/tsbundle to ^2.6.3.
- Breaking change: any consumer code that relied on addTypedSocket to attach a WebSocket server to an existing Express instance will need to migrate to the new SmartServe/TypedServer integration.
## 2025-12-02 - 5.0.0 - BREAKING CHANGE(devtools)
Switch /reloadcheck endpoint from GET to POST in DevToolsController
- Updated ts/controllers/controller.devtools.ts: decorator changed from @plugins.smartserve.Get('/reloadcheck') to @plugins.smartserve.Post('/reloadcheck').
- Clients that previously performed GET requests against /reloadcheck must be updated to use POST. This is a breaking API change.
- Bump major version to reflect the change in the public HTTP API.
## 2025-12-02 - 4.1.1 - fix(classes.typedserver)
Instantiate and register DevToolsController only when injectReload is enabled; compile ControllerRegistry routes after registration
- DevToolsController is now created and registered only if options.injectReload is true to avoid unnecessary/invalid registrations when live reload is disabled.
- ControllerRegistry.compileRoutes() is invoked after registering controllers to precompile decorated routes for faster route matching.
## 2025-12-02 - 4.1.0 - feat(TypedServer)
Integrate SmartServe controller routing; add built-in routes controller and refactor TypedServer to use controllers and FileServer
- Add BuiltInRoutesController exposing /robots.txt, /manifest.json, /sitemap, /sitemap-news, /feed and /appversion
- Refactor TypedRequestHandler into a SmartServe-decorated TypedRequestController and register it with ControllerRegistry
- Refactor TypedServer to use SmartServe: register controller instances, use ControllerRegistry matching, and delegate WebSocket integration to SmartServe
- Introduce FileServer-based static serving with HTML reload script injection and improved default root handling
- Expand supported HTTP methods to include HEAD and OPTIONS
- Remove legacy FeedHelper and consolidate sitemap/feed handling into controllers and helpers
- Enhance servertools legacy Express utilities: improved HandlerProxy, HandlerStatic, Compressor with caching and preferred compression support
- Service worker subsystem improvements: CacheManager, NetworkManager, UpdateManager and backend enhancements for robust caching, revalidation and client reloads
- Web-inject LitElement properties switched from private fields to accessor syntax (typedserver_web.infoscreen)
## 2025-12-02 - 4.0.0 - BREAKING CHANGE(typedserver)
Migrate to new push.rocks packages and async smartfs API; replace smartchok with smartwatch; update deps and service worker handling
- Major dependency updates: @push.rocks packages upgraded (smartfile -> v13, smartfs added v1.2.0, smartenv v6, smartrequest v5, smartwatch v5, webrequest v4), Express and dev-tooling bumped.
- Replace sync smartfile APIs with async SmartFs instance (plugins.fsInstance). All file reads now use async smartfs calls.
- smartfile v13 migration: removed sync fs helpers; added plugins.fsInstance (SmartFs + Node provider).
- Renamed file watcher usage: Smartchok -> Smartwatch and property names updated (smartwatchInstance).
- WebRequest class rename handled: WebRequest -> WebrequestClient (webrequest v4).
- Service worker bundle is lazy-loaded (avoid startup sync file reads) and added routes for bundle and source map.
- Service worker & SW-related improvements: more robust caching, enhanced cache headers, offline handling, revalidation, and update forcing logic.
- createServeDirHash now uses smartfs.directory().recursive().treeHash() and truncates hash to 12 chars; fallback hash logic retained.
- HandlerStatic, HandlerProxy and route handling hardened for Express 5 wildcard params (array/various shapes supported).
- Various runtime improvements: safer start/stop handling, improved error logging, and non-blocking initialization of optional features (file watcher, TypedSocket).
- Updated tooling/dev deps: @git.zone/tsbuild/tsbundle/tstest and @types/node updated.
## 2025-11-19 - 3.0.80 - fix(dependencies)
Bump dependencies and devDependencies to updated versions
- Upgrade @push.rocks/smartfeed: ^1.0.11 → ^1.4.0
- Upgrade @push.rocks/smartfile: ^11.2.5 → ^11.2.7
- Upgrade @push.rocks/smartjson: ^5.0.20 → ^5.2.0
- Upgrade @push.rocks/smartlog: ^3.1.8 → ^3.1.10
- Upgrade @push.rocks/smartsitemap: ^2.0.3 → ^2.0.4
- Upgrade @push.rocks/taskbuffer: ^3.1.7 → ^3.4.0
- Upgrade @tsclass/tsclass: ^9.2.0 → ^9.3.0
- Upgrade @types/express: ^5.0.3 → ^5.0.5
- Dev: Upgrade @git.zone/tsbuild: ^2.6.4 → ^3.1.0
- Dev: Upgrade @git.zone/tsbundle: ^2.5.1 → ^2.5.2
- Dev: Upgrade @git.zone/tsrun: ^1.3.3 → ^2.0.0
- Dev: Upgrade @git.zone/tstest: ^2.3.4 → ^2.8.2
## 2025-09-03 - 3.0.79 - fix(servertools)
Normalize Express wildcard parameter notation to /{*splat} across server routes and handlers; add local Claude settings

View File

@@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "3.0.79",
"version": "6.0.1",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {
@@ -58,19 +58,19 @@
],
"homepage": "https://code.foss.global/api.global/typedserver",
"dependencies": {
"@api.global/typedrequest": "^3.1.10",
"@api.global/typedrequest": "^3.1.11",
"@api.global/typedrequest-interfaces": "^3.0.19",
"@api.global/typedsocket": "^3.0.1",
"@cloudflare/workers-types": "^4.20250816.0",
"@api.global/typedsocket": "^4.1.0",
"@cloudflare/workers-types": "^4.20251202.0",
"@design.estate/dees-comms": "^1.0.27",
"@push.rocks/lik": "^6.2.2",
"@push.rocks/smartchok": "^1.1.1",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartenv": "^5.0.13",
"@push.rocks/smartfeed": "^1.0.11",
"@push.rocks/smartfile": "^11.2.5",
"@push.rocks/smartjson": "^5.0.20",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartenv": "^6.0.0",
"@push.rocks/smartfeed": "^1.4.0",
"@push.rocks/smartfile": "^13.1.0",
"@push.rocks/smartfs": "^1.2.0",
"@push.rocks/smartjson": "^5.2.0",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartlog-destination-devtools": "^1.0.12",
"@push.rocks/smartlog-interfaces": "^3.0.2",
"@push.rocks/smartmanifest": "^2.0.2",
@@ -80,28 +80,30 @@
"@push.rocks/smartopen": "^2.0.0",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"@push.rocks/smartrequest": "^4.2.1",
"@push.rocks/smartrequest": "^5.0.1",
"@push.rocks/smartrx": "^3.0.10",
"@push.rocks/smartsitemap": "^2.0.3",
"@push.rocks/smartserve": "^1.1.2",
"@push.rocks/smartsitemap": "^2.0.4",
"@push.rocks/smartstream": "^3.2.5",
"@push.rocks/smarttime": "^4.1.1",
"@push.rocks/taskbuffer": "^3.1.7",
"@push.rocks/webrequest": "^3.0.37",
"@push.rocks/smartwatch": "^5.0.0",
"@push.rocks/taskbuffer": "^3.4.0",
"@push.rocks/webrequest": "^4.0.1",
"@push.rocks/webstore": "^2.0.20",
"@tsclass/tsclass": "^9.2.0",
"@types/express": "^5.0.3",
"body-parser": "^2.2.0",
"@tsclass/tsclass": "^9.3.0",
"@types/express": "^5.0.6",
"body-parser": "^2.2.1",
"cors": "^2.8.5",
"express": "^5.1.0",
"express": "^5.2.1",
"express-force-ssl": "^0.3.2",
"lit": "^3.3.1"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.4",
"@types/node": "^22.14.0"
"@git.zone/tsbuild": "^3.1.2",
"@git.zone/tsbundle": "^2.6.3",
"@git.zone/tsrun": "^2.0.0",
"@git.zone/tstest": "^3.1.3",
"@types/node": "^24.10.1"
},
"private": false,
"browserslist": [

7041
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1,41 @@
# Project Hints - @api.global/typedserver
## Recent Changes (December 2025)
### Dependency Updates
- `@push.rocks/smartchok` replaced with `@push.rocks/smartwatch` (renamed package, same API)
- `@push.rocks/smartfile` upgraded from v11 to v13 (major API change - `fs` module removed)
- `@push.rocks/smartfs` added for filesystem operations (v1.2.0+)
- `@push.rocks/smartenv` upgraded to v6.0.0
- `@push.rocks/smartrequest` upgraded to v5.0.1
- `@push.rocks/webrequest` upgraded to v4.0.1 (`WebRequest` renamed to `WebrequestClient`)
- Express upgraded to v5.2.1
- All `@git.zone/*` dev dependencies updated to latest
### Code Migration Notes
#### smartfile v13 Migration
- Old: `plugins.smartfile.fs.toStringSync(path)` / `plugins.smartfile.fs.toBufferSync(path)`
- New: Use `plugins.fsInstance` (SmartFs instance with Node provider)
- String: `await plugins.fsInstance.file(path).encoding('utf8').read() as string`
- Buffer: `await plugins.fsInstance.file(path).read() as Buffer`
#### smartfs treeHash
- Old: `plugins.smartfile.fs.fileTreeToHash(dir, pattern)`
- New: `await plugins.fsInstance.directory(dir).recursive().treeHash()`
#### smartwatch (formerly smartchok)
- Class renamed: `Smartchok``Smartwatch`
- API remains the same: `new Smartwatch([paths])`, `.start()`, `.stop()`, `.getObservableFor(event)`
#### webrequest v4
- Class renamed: `WebRequest``WebrequestClient`
### Architecture
- `plugins.fsInstance` is a pre-configured SmartFs instance with SmartFsProviderNode
- All file operations should be async using smartfs
- Sync file operations have been removed
### Express 5 Notes
- Wildcard routes use `/{*splat}` notation
- `req.params.splat` can be an array, use `Array.isArray()` check

405
readme.md
View File

@@ -1,249 +1,278 @@
# @api.global/typedserver
A TypeScript-based framework for serving static files with advanced features including live reloading, compression, and type-safe API requests. Part of the @api.global ecosystem, it integrates seamlessly with @api.global/typedrequest for type-safe HTTP requests and @api.global/typedsocket for WebSocket communication.
A powerful TypeScript-first web server framework featuring static file serving, live reload, compression, and seamless type-safe API integration. Part of the `@api.global` ecosystem, it provides a modern foundation for building full-stack TypeScript applications with first-class support for typed HTTP requests and WebSocket communication.
## Features
## Issue Reporting and Security
- **Type-Safe API Ecosystem**:
- HTTP Requests via @api.global/typedrequest
- WebSocket Support via @api.global/typedsocket
- Full TypeScript support across all endpoints
- **Service Worker Integration**: Advanced caching and offline capabilities
- **Edge Worker Support**: Optimized edge computing capabilities
- **Live Reload**: Automatic browser refresh on file changes
- **Compression**: Built-in support for response compression
- **CORS Management**: Flexible cross-origin resource sharing
- **TypeScript First**: Built with and for TypeScript
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
## Components
## ✨ Features
### Core Server (`ts/`)
- Static file serving with Express
- Type-safe request handling
- Live reload functionality
- Compression middleware
- 🔒 **Type-Safe API Ecosystem** - Full TypeScript support with `@api.global/typedrequest` and `@api.global/typedsocket`
-**Live Reload** - Automatic browser refresh on file changes during development
- 🗜️ **Compression** - Built-in support for gzip, deflate, and brotli compression
- 🌐 **CORS Management** - Flexible cross-origin resource sharing configuration
- 🔧 **Service Worker Integration** - Advanced caching and offline capabilities
- ☁️ **Edge Worker Support** - Cloudflare Workers compatible edge computing
- 📡 **WebSocket Support** - Real-time bidirectional communication via TypedSocket
- 🗺️ **Sitemap & Feeds** - Automatic sitemap and RSS feed generation
- 🤖 **Robots.txt** - Built-in robots.txt handling
### Service Worker (`ts_web_serviceworker/`)
- `CacheManager`: Advanced caching strategies
- `NetworkManager`: Request/response handling
- `UpdateManager`: Cache invalidation and updates
- `ServiceWorker`: Core service worker implementation
### Edge Worker (`ts_edgeworker/`)
- Edge computing capabilities
- Request/response transformation
- Edge caching strategies
### Web Inject (`ts_web_inject/`)
- Live reload script injection
- Runtime dependency management
- Dynamic module loading
## Installation
## 📦 Installation
```bash
# Using pnpm (recommended)
pnpm add @api.global/typedserver
# Using npm
npm install @api.global/typedserver
```
## Quick Start
## 🚀 Quick Start
### Basic Static File Server
```typescript
import { TypedServer } from '@api.global/typedserver';
const server = new TypedServer({
port: 3000,
serveDir: './public',
watch: true,
compression: true
cors: true,
watch: true, // Enable file watching
injectReload: true, // Inject live reload script
});
server.start();
await server.start();
console.log('Server running on port 3000');
```
## Type-Safe API Integration
### Server with All Options
### HTTP Requests with TypedRequest
```typescript
import { TypedRequest } from '@api.global/typedrequest';
import { TypedServer } from '@api.global/typedserver';
// Define your request/response interface
interface IUserRequest {
const server = new TypedServer({
port: 8080,
serveDir: './dist',
cors: true,
watch: true,
injectReload: true,
enableCompression: true,
preferredCompressionMethod: 'brotli',
forceSsl: false,
sitemap: true,
feed: true,
robots: true,
domain: 'example.com',
appVersion: 'v1.0.0',
manifest: {
name: 'My App',
short_name: 'myapp',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#000000',
},
});
await server.start();
```
## 🔌 Type-Safe API Integration
### Adding TypedRequest Handlers
```typescript
import { TypedServer, servertools } from '@api.global/typedserver';
import * as typedrequest from '@api.global/typedrequest';
// Define your typed request interface
interface IGetUser {
method: 'getUser';
request: { userId: string };
response: { username: string; email: string; };
response: { name: string; email: string };
}
// Create and use a typed request
const getUserRequest = new TypedRequest<IUserRequest>('/api/users', 'getUser');
const user = await getUserRequest.fire({ userId: '123' });
```
const server = new TypedServer({ serveDir: './public', cors: true });
### WebSocket Communication
```typescript
import { TypedSocket } from '@api.global/typedsocket';
// Create a typed router
const typedRouter = new typedrequest.TypedRouter();
// Server setup
const typedRouter = new TypedRouter();
const server = await TypedSocket.createServer(typedRouter);
// Client connection
const client = await TypedSocket.createClient(typedRouter, 'ws://localhost:3000');
// Type-safe real-time messaging
interface IChatMessage {
method: 'sendMessage';
request: { text: string };
response: { id: string; timestamp: number; };
}
```
## Advanced Usage
### Service Worker Setup
```typescript
import { ServiceWorker } from '@api.global/typedserver/web_serviceworker';
const sw = new ServiceWorker({
cacheStrategy: 'network-first',
offlineSupport: true
});
sw.register();
```
### Edge Worker Configuration
```typescript
import { EdgeWorker } from '@api.global/typedserver/edgeworker';
const edge = new EdgeWorker({
transforms: ['compress', 'minify'],
caching: true
});
```
## Configuration
### Server Options
```typescript
interface IServerOptions {
port?: number;
host?: string;
serveDir: string;
watch?: boolean;
compression?: boolean;
cors?: boolean | CorsOptions;
cache?: CacheOptions;
}
```
### Cache Strategies
```typescript
type CacheStrategy =
| 'network-first'
| 'cache-first'
| 'stale-while-revalidate';
```
## API Reference
See [API Documentation](https://api.global/docs/typedserver) for detailed API reference.
## Contributing
1. Fork the repository
2. Create your feature branch
3. Commit your changes
4. Push to the branch
5. Create a Pull Request
## License
MIT License - see LICENSE for details.
Task Venture Capital GmbH © 2024
```typescript
// Define a request type
interface MyCustomRequest {
userId: string;
}
// Define a response type
interface MyCustomResponse {
userName: string;
}
```
Next, set up a route to handle requests using these types:
```typescript
import { TypedRouter, TypedHandler } from '@api.global/typedrequest';
// Instantiate a TypedRouter
const typedRouter = new TypedRouter();
// Register a route with request and response types
typedRouter.addTypedHandler<MyCustomRequest, MyCustomResponse>(
new TypedHandler('getUser', async (requestData) => {
// Implement your logic here. For example, fetch user data from a database.
const userData = { userName: 'John Doe' }; // Dummy implementation
return { response: userData };
// Add a typed handler
typedRouter.addTypedHandler<IGetUser>(
new typedrequest.TypedHandler('getUser', async (data) => {
// Your logic here
return { name: 'John Doe', email: 'john@example.com' };
})
);
// Bind the typed router to the server
typedServer.useTypedRouter(typedRouter);
// Attach the router to the server
server.server.addRoute('/api', new servertools.HandlerTypedRouter(typedRouter));
// Now, the route is set up to handle requests with type checking
await server.start();
```
This example shows defining types for requests and responses, creating a `TypedRouter`, and adding a route with typed handling. This feature brings the benefits of TypeScript's static type checking to server-side logic, improving the development experience.
### Enabling SSL/TLS
To enable SSL/TLS, configure the `TypedServer` with the SSL options, including the paths to your SSL certificate and key files:
### WebSocket Communication with TypedSocket
```typescript
const serverOptions = {
port: 443,
serveDir: 'public',
watch: true,
injectReload: true,
import { TypedServer } from '@api.global/typedserver';
import * as typedrequest from '@api.global/typedrequest';
import * as typedsocket from '@api.global/typedsocket';
const server = new TypedServer({ serveDir: './public', cors: true });
const typedRouter = new typedrequest.TypedRouter();
await server.start();
// Create WebSocket server attached to the HTTP server
const socketServer = await typedsocket.TypedSocket.createServer(
typedRouter,
server.server
);
// Handle real-time events
interface IChatMessage {
method: 'sendMessage';
request: { text: string; room: string };
response: { messageId: string; timestamp: number };
}
typedRouter.addTypedHandler<IChatMessage>(
new typedrequest.TypedHandler('sendMessage', async (data) => {
return { messageId: crypto.randomUUID(), timestamp: Date.now() };
})
);
```
## 🛠️ Server Tools
### Custom Route Handlers
```typescript
import { servertools } from '@api.global/typedserver';
const server = new servertools.Server({
cors: true,
privateKey: fs.readFileSync('path/to/ssl/private.key'),
publicKey: fs.readFileSync('path/to/ssl/certificate.crt')
domain: 'example.com',
});
// Add a custom route with handler
server.addRoute('/api/hello', new servertools.Handler('GET', async (req, res) => {
res.json({ message: 'Hello, World!' });
}));
// Serve static files from a directory
server.addRoute('/{*splat}', new servertools.HandlerStatic('./public', {
serveIndexHtmlDefault: true,
enableCompression: true,
}));
await server.start(3000);
```
### Proxy Handler
```typescript
import { servertools } from '@api.global/typedserver';
const server = new servertools.Server({ cors: true });
// Proxy requests to another server
server.addRoute('/proxy/{*splat}', new servertools.HandlerProxy({
target: 'https://api.example.com',
}));
await server.start(3000);
```
## ☁️ Edge Worker (Cloudflare Workers)
```typescript
import { EdgeWorker, DomainRouter } from '@api.global/typedserver/edgeworker';
const router = new DomainRouter();
router.addDomainInstruction({
domainPattern: '*.example.com',
originUrl: 'https://origin.example.com',
cacheConfig: { maxAge: 3600 },
});
const worker = new EdgeWorker(router);
// In your Cloudflare Worker entry point
export default {
fetch: (request: Request, env: any, ctx: any) => worker.handleRequest(request, env, ctx),
};
const typedServer = new TypedServer(serverOptions);
startServer().catch(console.error);
```
Replace `'path/to/ssl/private.key'` and `'path/to/ssl/certificate.crt'` with the actual paths to your SSL key and certificate files. This setup ensures that your server communicates over HTTPS, encrypting the data transmitted between the client and the server.
## 🔧 Service Worker Client
### Conclusion
```typescript
import { ServiceWorkerClient } from '@api.global/typedserver/web_serviceworker_client';
`@api.global/typedserver` offers a streamlined way to set up a web server with TypeScript, featuring static file serving, live reloading, typed request/response handling, and SSL support. This guide covers the basic usage, but `TypedServer` is highly configurable, catering to various hosting and development needs.
const swClient = new ServiceWorkerClient();
// Register and manage service worker
await swClient.register('/serviceworker.bundle.js');
// Listen for updates
swClient.onUpdate(() => {
console.log('New version available!');
});
```
For a deeper dive into the API and more advanced configurations, refer to the official documentation and type definitions included in the package.
## 📋 Configuration Reference
### IServerOptions
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `serveDir` | `string` | - | Directory to serve static files from |
| `port` | `number \| string` | `3000` | Port to listen on |
| `cors` | `boolean` | `true` | Enable CORS headers |
| `watch` | `boolean` | `false` | Watch files for changes |
| `injectReload` | `boolean` | `false` | Inject live reload script into HTML |
| `enableCompression` | `boolean` | `false` | Enable response compression |
| `preferredCompressionMethod` | `'gzip' \| 'deflate' \| 'brotli'` | - | Preferred compression algorithm |
| `forceSsl` | `boolean` | `false` | Redirect HTTP to HTTPS |
| `sitemap` | `boolean` | `false` | Generate sitemap at `/sitemap` |
| `feed` | `boolean` | `false` | Generate RSS feed at `/feed` |
| `robots` | `boolean` | `false` | Serve robots.txt |
| `domain` | `string` | - | Domain name for sitemap/feeds |
| `appVersion` | `string` | - | Application version string |
| `manifest` | `object` | - | Web App Manifest configuration |
| `publicKey` | `string` | - | SSL certificate |
| `privateKey` | `string` | - | SSL private key |
## 🏗️ Architecture
```
@api.global/typedserver
├── /backend - Main server exports (TypedServer, servertools)
├── /edgeworker - Cloudflare Workers edge computing
├── /web_inject - Live reload script injection
├── /web_serviceworker - Service Worker implementation
└── /web_serviceworker_client - Service Worker client utilities
```
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
For any legal inquiries or further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@api.global/typedserver',
version: '3.0.79',
version: '6.0.1',
description: 'A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.'
}

View File

@@ -1,8 +1,9 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import * as interfaces from '../dist_ts_interfaces/index.js';
import * as servertools from './servertools/index.js';
import { type TCompressionMethod } from './servertools/classes.compressor.js';
import { DevToolsController } from './controllers/controller.devtools.js';
import { TypedRequestController } from './controllers/controller.typedrequest.js';
import { BuiltInRoutesController } from './controllers/controller.builtin.js';
export interface IServerOptions {
/**
@@ -15,16 +16,6 @@ export interface IServerOptions {
*/
injectReload?: boolean;
/**
* enable compression
*/
enableCompression?: boolean;
/**
* choose a preferred compression method
*/
preferredCompressionMethod?: TCompressionMethod;
/**
* watch the serve directory?
*/
@@ -34,7 +25,6 @@ export interface IServerOptions {
/**
* a default answer given in case there is no other handler.
* @returns
*/
defaultAnswer?: () => Promise<string>;
@@ -42,13 +32,14 @@ export interface IServerOptions {
* will try to reroute traffic to an ssl connection using headers
*/
forceSsl?: boolean;
/**
* allows serving manifests
*/
manifest?: plugins.smartmanifest.ISmartManifestConstructorOptions;
/**
* the port to listen on
* can be overwritten when actually starting the server
*/
port?: number | string;
publicKey?: string;
@@ -57,6 +48,7 @@ export interface IServerOptions {
feed?: boolean;
robots?: boolean;
domain?: string;
/**
* convey information about the app being served
*/
@@ -66,22 +58,48 @@ export interface IServerOptions {
blockWaybackMachine?: boolean;
}
export class TypedServer {
// static
// nothing here yet
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | 'ALL';
export interface IRouteHandler {
(request: Request): Promise<Response | null>;
}
export interface IRegisteredRoute {
pattern: string;
regex: RegExp;
paramNames: string[];
method: THttpMethod;
handler: IRouteHandler;
}
export class TypedServer {
// instance
public options: IServerOptions;
public server: servertools.Server;
public smartchokInstance: plugins.smartchok.Smartchok;
public smartServe: plugins.smartserve.SmartServe;
public smartwatchInstance: plugins.smartwatch.Smartwatch;
public serveDirHashSubject = new plugins.smartrx.rxjs.ReplaySubject<string>(1);
public serveHash: string = '000000';
public typedsocket: plugins.typedsocket.TypedSocket;
public typedrouter = new plugins.typedrequest.TypedRouter();
// Sitemap helper
private sitemapHelper: SitemapHelper;
private smartmanifestInstance: plugins.smartmanifest.SmartManifest;
// Decorated controllers
private devToolsController: DevToolsController;
private typedRequestController: TypedRequestController;
private builtInRoutesController: BuiltInRoutesController;
// File server for static files
private fileServer: plugins.smartserve.FileServer;
// Custom route handlers (for addRoute API)
private customRoutes: IRegisteredRoute[] = [];
public lastReload: number = Date.now();
public ended = false;
constructor(optionsArg: IServerOptions) {
const standardOptions: IServerOptions = {
port: 3000,
@@ -94,43 +112,64 @@ export class TypedServer {
...standardOptions,
...optionsArg,
};
}
this.server = new servertools.Server(this.options);
// add routes to the smartexpress instance
this.server.addRoute(
'/typedserver/:request',
new servertools.Handler('ALL', async (req, res) => {
switch (req.params.request) {
case 'devtools':
res.setHeader('Content-Type', 'text/javascript');
res.status(200);
res.write(plugins.smartfile.fs.toStringSync(paths.injectBundlePath));
res.end();
break;
case 'reloadcheck':
console.log('got request for reloadcheck');
res.setHeader('Content-Type', 'text/plain');
res.status(200);
if (this.ended) {
res.write('end');
res.end();
return;
}
res.write(this.lastReload.toString());
res.end();
break;
default:
res.status(404);
res.write('Unknown request type');
res.end();
break;
}
/**
* Access sitemap URLs (for adding/replacing)
*/
public get sitemap() {
return this.sitemapHelper;
}
/**
* Add a custom route handler
* Supports Express-style path patterns like '/path/:param' and '/path/*splat'
* @param path - The route path pattern
* @param method - HTTP method (GET, POST, PUT, DELETE, PATCH, ALL)
* @param handler - Async function that receives Request and returns Response or null
*/
public addRoute(path: string, method: THttpMethod, handler: IRouteHandler): void {
// Convert Express-style path to regex
const paramNames: string[] = [];
let regexPattern = path
// Handle named parameters :param
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '([^/]+)';
})
);
this.server.addRoute(
'/typedrequest',
new servertools.HandlerTypedRouter(this.typedrouter)
);
// Handle wildcard *splat (matches everything including slashes)
.replace(/\*([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, paramName) => {
paramNames.push(paramName);
return '(.*)';
});
// Ensure exact match
regexPattern = `^${regexPattern}$`;
this.customRoutes.push({
pattern: path,
regex: new RegExp(regexPattern),
paramNames,
method,
handler,
});
}
/**
* Parse route parameters from a path using a registered route
*/
private parseRouteParams(
route: IRegisteredRoute,
pathname: string
): Record<string, string> | null {
const match = pathname.match(route.regex);
if (!match) return null;
const params: Record<string, string> = {};
route.paramNames.forEach((name, index) => {
params[name] = match[index + 1];
});
return params;
}
/**
@@ -144,77 +183,118 @@ export class TypedServer {
);
}
if (this.options.serveDir) {
this.server.addRoute(
'/{*splat}',
new servertools.HandlerStatic(this.options.serveDir, {
responseModifier: async (responseArg) => {
if (plugins.path.parse(responseArg.path).ext === '.html') {
let fileString = responseArg.responseContent.toString();
const fileStringArray = fileString.split('<head>');
if (this.options.injectReload && fileStringArray.length === 2) {
fileStringArray[0] = `${fileStringArray[0]}<head>
<!-- injected by @apiglobal/typedserver start -->
<script async defer type="module" src="/typedserver/devtools"></script>
<script>
globalThis.typedserver = {
lastReload: ${this.lastReload},
versionInfo: ${JSON.stringify({}, null, 2)},
}
</script>
<!-- injected by @apiglobal/typedserver stop -->
`;
fileString = fileStringArray.join('');
console.log('injected typedserver script.');
responseArg.responseContent = Buffer.from(fileString);
} else if (this.options.injectReload) {
console.log('Could not insert typedserver script - no <head> tag found');
}
}
const headers = responseArg.headers;
headers.appHash = this.serveHash;
headers['Cache-Control'] = 'no-cache, no-store, must-revalidate';
headers['Pragma'] = 'no-cache';
headers['Expires'] = '0';
return {
headers,
path: responseArg.path,
responseContent: responseArg.responseContent,
travelData: responseArg.travelData,
};
},
serveIndexHtmlDefault: true,
enableCompression: this.options.enableCompression,
preferredCompressionMethod: this.options.preferredCompressionMethod,
})
);
const port =
typeof this.options.port === 'string'
? parseInt(this.options.port, 10)
: this.options.port || 3000;
// Initialize optional helpers
if (this.options.sitemap) {
this.sitemapHelper = new SitemapHelper(this.options.domain);
}
if (this.options.manifest) {
this.smartmanifestInstance = new plugins.smartmanifest.SmartManifest(this.options.manifest);
}
// Initialize file server for static files
if (this.options.serveDir) {
this.fileServer = new plugins.smartserve.FileServer({
root: this.options.serveDir,
index: ['index.html'],
etag: true,
});
}
// Initialize decorated controllers
if (this.options.injectReload) {
this.devToolsController = new DevToolsController({
getLastReload: () => this.lastReload,
getEnded: () => this.ended,
});
}
this.typedRequestController = new TypedRequestController(this.typedrouter);
this.builtInRoutesController = new BuiltInRoutesController({
domain: this.options.domain,
robots: this.options.robots,
manifest: this.smartmanifestInstance,
sitemap: this.options.sitemap,
feed: this.options.feed,
appVersion: this.options.appVersion,
feedMetadata: this.options.feedMetadata,
articleGetterFunction: this.options.articleGetterFunction,
blockWaybackMachine: this.options.blockWaybackMachine,
getSitemapUrls: () => this.sitemapHelper?.urls || [],
});
// Register controllers with SmartServe's ControllerRegistry
if (this.options.injectReload) {
plugins.smartserve.ControllerRegistry.registerInstance(this.devToolsController);
}
plugins.smartserve.ControllerRegistry.registerInstance(this.typedRequestController);
plugins.smartserve.ControllerRegistry.registerInstance(this.builtInRoutesController);
// Compile routes for fast matching
plugins.smartserve.ControllerRegistry.compileRoutes();
// Build SmartServe options
const smartServeOptions: plugins.smartserve.ISmartServeOptions = {
port,
hostname: '0.0.0.0',
tls:
this.options.privateKey && this.options.publicKey
? {
key: this.options.privateKey,
cert: this.options.publicKey,
}
: undefined,
websocket: {
typedRouter: this.typedrouter,
onConnectionOpen: (peer) => {
peer.tags.add('typedserver_frontend');
console.log(`WebSocket connected: ${peer.id}`);
},
onConnectionClose: (peer) => {
console.log(`WebSocket disconnected: ${peer.id}`);
},
},
};
this.smartServe = new plugins.smartserve.SmartServe(smartServeOptions);
// Set up custom request handler that integrates with ControllerRegistry
this.smartServe.setHandler(async (request: Request): Promise<Response> => {
return this.handleRequest(request);
});
// Setup file watching
if (this.options.watch && this.options.serveDir) {
try {
this.smartchokInstance = new plugins.smartchok.Smartchok([this.options.serveDir]);
await this.smartchokInstance.start();
(await this.smartchokInstance.getObservableFor('change')).subscribe(async () => {
this.smartwatchInstance = new plugins.smartwatch.Smartwatch([this.options.serveDir]);
await this.smartwatchInstance.start();
(await this.smartwatchInstance.getObservableFor('change')).subscribe(async () => {
await this.createServeDirHash();
this.reload();
});
await this.createServeDirHash();
} catch (error) {
console.error('Failed to initialize file watching:', error);
// Continue without file watching rather than crashing
}
}
// lets start the server
await this.server.start();
// Start the server
await this.smartServe.start();
console.log(`TypedServer listening on port ${port}`);
// Setup TypedSocket using SmartServe integration
try {
this.typedsocket = await plugins.typedsocket.TypedSocket.createServer(
this.typedrouter,
this.server
this.typedsocket = plugins.typedsocket.TypedSocket.fromSmartServe(
this.smartServe,
this.typedrouter
);
// lets setup typedrouter
// Setup typedrouter handlers
this.typedrouter.addTypedHandler<interfaces.IReq_GetLatestServerChangeTime>(
new plugins.typedrequest.TypedHandler('getLatestServerChangeTime', async () => {
return {
@@ -224,10 +304,188 @@ export class TypedServer {
);
} catch (error) {
console.error('Failed to initialize TypedSocket:', error);
// Continue without WebSocket support rather than crashing
}
}
/**
* Create an IRequestContext from a Request
*/
private async createContext(
request: Request,
params: Record<string, string>
): Promise<plugins.smartserve.IRequestContext> {
const url = new URL(request.url);
const method = request.method.toUpperCase() as THttpMethod;
// Parse query params
const query: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
query[key] = value;
});
// Parse body
let body: unknown = undefined;
const contentType = request.headers.get('content-type');
if (contentType?.includes('application/json')) {
try {
body = await request.clone().json();
} catch {
body = {};
}
}
return {
request,
body,
params,
query,
headers: request.headers,
path: url.pathname,
method,
url,
runtime: 'node' as const,
state: {},
};
}
/**
* Main request handler - routes to appropriate sub-handlers
*/
private async handleRequest(request: Request): Promise<Response> {
const url = new URL(request.url);
const path = url.pathname;
const method = request.method.toUpperCase() as THttpMethod;
// First, try to match via ControllerRegistry (decorated routes)
const match = plugins.smartserve.ControllerRegistry.matchRoute(path, method);
if (match) {
try {
const context = await this.createContext(request, match.params);
const result = await match.route.handler(context);
// Handle Response or convert to Response
if (result instanceof Response) {
return result;
}
return new Response(JSON.stringify(result), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
if (error instanceof plugins.smartserve.RouteNotFoundError) {
// Route explicitly threw "not found", continue to other handlers
} else {
console.error('Controller error:', error);
return new Response('Internal Server Error', { status: 500 });
}
}
}
// Custom routes (registered via addRoute)
for (const route of this.customRoutes) {
if (route.method === 'ALL' || route.method === method) {
const params = this.parseRouteParams(route, path);
if (params !== null) {
(request as any).params = params;
const response = await route.handler(request);
if (response) return response;
}
}
}
// HTML injection for reload (if enabled)
if (this.options.injectReload && this.options.serveDir) {
const response = await this.handleHtmlWithInjection(request);
if (response) return response;
}
// Try static file serving
if (this.fileServer && (method === 'GET' || method === 'HEAD')) {
try {
const staticResponse = await this.fileServer.serve(request);
if (staticResponse) {
return staticResponse;
}
} catch (error) {
// Fall through to 404
}
}
// Default answer for root
if (path === '/' && method === 'GET' && this.options.defaultAnswer) {
const html = await this.options.defaultAnswer();
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html' },
});
}
// Not found
return new Response('Not Found', { status: 404 });
}
/**
* Handle HTML files with reload script injection
*/
private async handleHtmlWithInjection(request: Request): Promise<Response | null> {
const url = new URL(request.url);
const requestPath = url.pathname;
// Check if this is a request for an HTML file or root
if (requestPath === '/' || requestPath.endsWith('.html') || !requestPath.includes('.')) {
try {
let filePath = requestPath === '/' ? 'index.html' : requestPath.slice(1);
if (!filePath.endsWith('.html') && !filePath.includes('.')) {
filePath = plugins.path.join(filePath, 'index.html');
}
const fullPath = plugins.path.join(this.options.serveDir, filePath);
// Security check
if (!fullPath.startsWith(this.options.serveDir)) {
return new Response('Forbidden', { status: 403 });
}
let fileContent = (await plugins.fsInstance
.file(fullPath)
.encoding('utf8')
.read()) as string;
// Inject reload script
if (fileContent.includes('<head>')) {
const injection = `<head>
<!-- injected by @apiglobal/typedserver start -->
<script async defer type="module" src="/typedserver/devtools"></script>
<script>
globalThis.typedserver = {
lastReload: ${this.lastReload},
versionInfo: ${JSON.stringify({}, null, 2)},
}
</script>
<!-- injected by @apiglobal/typedserver stop -->
`;
fileContent = fileContent.replace('<head>', injection);
console.log('injected typedserver script.');
}
return new Response(fileContent, {
status: 200,
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-cache, no-store, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
appHash: this.serveHash,
},
});
} catch (error) {
// Fall through to default handling
}
}
return null;
}
/**
* reloads the page
*/
@@ -237,14 +495,17 @@ export class TypedServer {
console.warn('TypedSocket not initialized, skipping client notifications');
return;
}
try {
const connections = await this.typedsocket.findAllTargetConnectionsByTag('typedserver_frontend');
const connections = await this.typedsocket.findAllTargetConnectionsByTag(
'typedserver_frontend'
);
for (const connection of connections) {
const pushTime = this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
'pushLatestServerChangeTime',
connection
);
const pushTime =
this.typedsocket.createTypedRequest<interfaces.IReq_PushLatestServerChangeTime>(
'pushLatestServerChangeTime',
connection
);
pushTime.fire({
time: this.lastReload,
});
@@ -259,7 +520,7 @@ export class TypedServer {
*/
public async stop(): Promise<void> {
this.ended = true;
const stopWithErrorHandling = async (
stopFn: () => Promise<unknown>,
componentName: string
@@ -270,24 +531,24 @@ export class TypedServer {
console.error(`Error stopping ${componentName}:`, err);
}
};
const tasks: Promise<void>[] = [];
// Stop server
if (this.server) {
tasks.push(stopWithErrorHandling(() => this.server.stop(), 'server'));
// Stop SmartServe
if (this.smartServe) {
tasks.push(stopWithErrorHandling(() => this.smartServe.stop(), 'SmartServe'));
}
// Stop TypedSocket
// Stop TypedSocket (in SmartServe mode, this is a no-op but good for cleanup)
if (this.typedsocket) {
tasks.push(stopWithErrorHandling(() => this.typedsocket.stop(), 'TypedSocket'));
}
// Stop file watcher
if (this.smartchokInstance) {
tasks.push(stopWithErrorHandling(() => this.smartchokInstance.stop(), 'file watcher'));
if (this.smartwatchInstance) {
tasks.push(stopWithErrorHandling(() => this.smartwatchInstance.stop(), 'file watcher'));
}
await Promise.all(tasks);
}
@@ -296,17 +557,57 @@ export class TypedServer {
*/
public async createServeDirHash() {
try {
const serveDirHash = await plugins.smartfile.fs.fileTreeToHash(this.options.serveDir, '**/*');
this.serveHash = serveDirHash;
console.log('Current ServeDir hash: ' + serveDirHash);
this.serveDirHashSubject.next(serveDirHash);
const serveDirHash = await plugins.fsInstance
.directory(this.options.serveDir)
.recursive()
.treeHash();
this.serveHash = serveDirHash.slice(0, 12);
console.log('Current ServeDir hash: ' + this.serveHash);
this.serveDirHashSubject.next(this.serveHash);
} catch (error) {
console.error('Failed to create serve directory hash:', error);
// Use a timestamp-based hash as fallback
const fallbackHash = Date.now().toString(16).slice(-6);
this.serveHash = fallbackHash;
console.log('Using fallback hash: ' + fallbackHash);
this.serveDirHashSubject.next(fallbackHash);
}
}
}
}
// ============================================================================
// Helper Classes
// ============================================================================
/**
* Sitemap helper class
*/
class SitemapHelper {
private smartSitemap = new plugins.smartsitemap.SmartSitemap();
public urls: plugins.smartsitemap.IUrlInfo[] = [];
constructor(domain?: string) {
if (domain) {
this.urls.push({
url: `https://${domain}/`,
timestamp: Date.now(),
frequency: 'daily',
});
}
}
async createSitemap(): Promise<string> {
return this.smartSitemap.createSitemapFromUrlInfoArray(this.urls);
}
async createSitemapNews(articles: plugins.tsclass.content.IArticle[]): Promise<string> {
return this.smartSitemap.createSitemapNewsFromArticleArray(articles);
}
replaceUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
this.urls = urlsArg;
}
addUrls(urlsArg: plugins.smartsitemap.IUrlInfo[]) {
this.urls = this.urls.concat(urlsArg);
}
}

View File

@@ -0,0 +1,125 @@
import * as plugins from '../plugins.js';
/**
* Built-in routes controller for TypedServer
* Handles robots.txt, manifest.json, sitemap, feed, appversion
*/
@plugins.smartserve.Route('')
export class BuiltInRoutesController {
private options: {
domain?: string;
robots?: boolean;
manifest?: plugins.smartmanifest.SmartManifest;
sitemap?: boolean;
feed?: boolean;
appVersion?: string;
feedMetadata?: plugins.smartfeed.IFeedOptions;
articleGetterFunction?: () => Promise<plugins.tsclass.content.IArticle[]>;
blockWaybackMachine?: boolean;
getSitemapUrls: () => plugins.smartsitemap.IUrlInfo[];
};
constructor(options: typeof BuiltInRoutesController.prototype.options) {
this.options = options;
}
@plugins.smartserve.Get('/robots.txt')
async getRobots(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.robots || !this.options.domain) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
const robotsContent = [
'User-agent: *',
'Allow: /',
`Sitemap: https://${this.options.domain}/sitemap`,
];
if (this.options.blockWaybackMachine) {
robotsContent.push('', 'User-agent: ia_archiver', 'Disallow: /');
}
return new Response(robotsContent.join('\n'), {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
}
@plugins.smartserve.Get('/manifest.json')
async getManifest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.manifest) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
return new Response(this.options.manifest.jsonString(), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
@plugins.smartserve.Get('/sitemap')
async getSitemap(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.sitemap || !this.options.domain) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
const smartsitemap = new plugins.smartsitemap.SmartSitemap();
const urls = this.options.getSitemapUrls();
const sitemapXml = await smartsitemap.createSitemapFromUrlInfoArray(urls);
return new Response(sitemapXml, {
status: 200,
headers: { 'Content-Type': 'application/xml' },
});
}
@plugins.smartserve.Get('/sitemap-news')
async getSitemapNews(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.sitemap || !this.options.domain || !this.options.articleGetterFunction) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
const smartsitemap = new plugins.smartsitemap.SmartSitemap();
const articles = await this.options.articleGetterFunction();
const sitemapNewsXml = await smartsitemap.createSitemapNewsFromArticleArray(articles);
return new Response(sitemapNewsXml, {
status: 200,
headers: { 'Content-Type': 'application/xml' },
});
}
@plugins.smartserve.Get('/feed')
async getFeed(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.feed || !this.options.feedMetadata) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
const smartfeed = new plugins.smartfeed.Smartfeed();
const articles = this.options.articleGetterFunction
? await this.options.articleGetterFunction()
: [];
const feedXml = await smartfeed.createFeedFromArticleArray(
this.options.feedMetadata,
articles
);
return new Response(feedXml, {
status: 200,
headers: { 'Content-Type': 'application/atom+xml' },
});
}
@plugins.smartserve.Get('/appversion')
async getAppVersion(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
if (!this.options.appVersion) {
throw new plugins.smartserve.RouteNotFoundError(ctx.path, ctx.method);
}
return new Response(this.options.appVersion, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
}
}

View File

@@ -0,0 +1,53 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
/**
* DevTools controller for TypedServer
* Handles /typedserver/devtools and /typedserver/reloadcheck endpoints
*/
@plugins.smartserve.Route('/typedserver')
export class DevToolsController {
private getLastReload: () => number;
private getEnded: () => boolean;
constructor(options: { getLastReload: () => number; getEnded: () => boolean }) {
this.getLastReload = options.getLastReload;
this.getEnded = options.getEnded;
}
@plugins.smartserve.Get('/devtools')
async getDevtools(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
const devtoolsContent = (await plugins.fsInstance
.file(paths.injectBundlePath)
.encoding('utf8')
.read()) as string;
return new Response(devtoolsContent, {
status: 200,
headers: {
'Content-Type': 'text/javascript',
},
});
}
@plugins.smartserve.Post('/reloadcheck')
async reloadCheck(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
console.log('got request for reloadcheck');
if (this.getEnded()) {
return new Response('end', {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
}
return new Response(this.getLastReload().toString(), {
status: 200,
headers: {
'Content-Type': 'text/plain',
},
});
}
}

View File

@@ -0,0 +1,34 @@
import * as plugins from '../plugins.js';
/**
* TypedRequest controller for type-safe RPC endpoint
*/
@plugins.smartserve.Route('/typedrequest')
export class TypedRequestController {
private typedRouter: plugins.typedrequest.TypedRouter;
constructor(typedRouter: plugins.typedrequest.TypedRouter) {
this.typedRouter = typedRouter;
}
@plugins.smartserve.Post('/')
async handleTypedRequest(ctx: plugins.smartserve.IRequestContext): Promise<Response> {
try {
const response = await this.typedRouter.routeAndAddResponse(ctx.body as plugins.typedrequestInterfaces.ITypedRequest);
return new Response(plugins.smartjson.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json',
},
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid request' }), {
status: 400,
headers: {
'Content-Type': 'application/json',
},
});
}
}
}

3
ts/controllers/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './controller.devtools.js';
export * from './controller.typedrequest.js';
export * from './controller.builtin.js';

View File

@@ -5,10 +5,10 @@ import * as servertools from './servertools/index.js';
export { servertools };
export * from './classes.typedserver.js';
// Type helpers
export type Request = plugins.express.Request;
export type Response = plugins.express.Response;
// Type helpers - using native Web API Request/Response types
// Native Request and Response are available in Node.js 18+ and all modern browsers
// Legacy Express types are available via servertools for backward compatibility
// lets export utilityservers
import * as utilityservers from './utilityservers/index.js';

View File

@@ -3,7 +3,6 @@ import * as http from 'http';
import * as https from 'https';
import * as net from 'net';
import * as path from 'path';
import * as stream from 'stream';
import * as zlib from 'zlib';
export { http, https, net, path, zlib };
@@ -22,10 +21,11 @@ export { typedrequest, typedrequestInterfaces, typedsocket };
// @pushrocks scope
import * as lik from '@push.rocks/lik';
import * as smartchok from '@push.rocks/smartchok';
import * as smartwatch from '@push.rocks/smartwatch';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartfeed from '@push.rocks/smartfeed';
import * as smartfile from '@push.rocks/smartfile';
import * as smartfs from '@push.rocks/smartfs';
import * as smartjson from '@push.rocks/smartjson';
import * as smartmanifest from '@push.rocks/smartmanifest';
import * as smartmime from '@push.rocks/smartmime';
@@ -40,10 +40,11 @@ import * as smarttime from '@push.rocks/smarttime';
export {
lik,
smartchok,
smartwatch,
smartdelay,
smartfeed,
smartfile,
smartfs,
smartjson,
smartmanifest,
smartmime,
@@ -57,11 +58,19 @@ export {
smartrx,
};
// express
// Create a ready-to-use smartfs instance with Node.js provider
export const fsInstance = new smartfs.SmartFs(new smartfs.SmartFsProviderNode());
// @push.rocks/smartserve
import * as smartserve from '@push.rocks/smartserve';
export { smartserve };
// Legacy Express dependencies - kept for backward compatibility with deprecated servertools
// These will be removed in the next major version
import express from 'express';
import bodyParser from 'body-parser';
import cors from 'cors';
import express from 'express';
// @ts-ignore
import expressForceSsl from 'express-force-ssl';
export { bodyParser, cors, express, expressForceSsl };
export { express, bodyParser, cors, expressForceSsl };

View File

@@ -92,7 +92,7 @@ export class HandlerStatic extends Handler {
// lets actually care about serving, if security checks pass
let fileBuffer: Buffer;
try {
fileBuffer = plugins.smartfile.fs.toBufferSync(joinedPath);
fileBuffer = await plugins.fsInstance.file(joinedPath).read() as Buffer;
usedPath = joinedPath;
} catch (err) {
// try serving index.html instead
@@ -101,7 +101,7 @@ export class HandlerStatic extends Handler {
console.log(`serving default path ${defaultPath} instead of ${joinedPath}`);
try {
parsedPath = plugins.path.parse(defaultPath);
fileBuffer = plugins.smartfile.fs.toBufferSync(defaultPath);
fileBuffer = await plugins.fsInstance.file(defaultPath).read() as Buffer;
usedPath = defaultPath;
} catch (err) {
res.writeHead(500);

View File

@@ -53,10 +53,17 @@ export class Server {
this.addRoute('/typedrequest', new HandlerTypedRouter(typedrouter));
}
/**
* @deprecated This method is deprecated. Use TypedServer with SmartServe integration instead.
* TypedSocket v4 no longer supports attaching to an existing Express server.
*/
public addTypedSocket(typedrouter: plugins.typedrequest.TypedRouter): void {
this.executeAfterStartFunctions.push(async () => {
plugins.typedsocket.TypedSocket.createServer(typedrouter, this);
});
console.warn(
'[DEPRECATED] servertools.Server.addTypedSocket() is deprecated and has no effect. ' +
'Use TypedServer with SmartServe integration for WebSocket support.'
);
// TypedSocket v4 creates its own server, which would conflict with Express.
// This method is now a no-op for backward compatibility.
}
public addRoute(routeStringArg: string, handlerArg?: Handler) {

View File

@@ -1,12 +1,22 @@
export * from './classes.server.js';
export * from './classes.route.js';
export * from './classes.handler.js';
export * from './classes.handlerstatic.js';
export * from './classes.handlerproxy.js';
export * from './classes.handlertypedrouter.js';
// Core utilities that don't depend on Express
export * from './classes.compressor.js';
import * as serviceworker from './tools.serviceworker.js';
export {
serviceworker,
}
// Legacy Express-based classes - deprecated, will be removed in next major version
// These are kept for backward compatibility but should not be used for new code
// Use SmartServe decorator-based controllers instead
/** @deprecated Use SmartServe directly */
export * from './classes.server.js';
/** @deprecated Use SmartServe @Route decorator */
export * from './classes.route.js';
/** @deprecated Use SmartServe @Get/@Post decorators */
export * from './classes.handler.js';
/** @deprecated Use SmartServe static file serving */
export * from './classes.handlerstatic.js';
/** @deprecated Use SmartServe custom handler */
export * from './classes.handlerproxy.js';
/** @deprecated Use SmartServe TypedRouter integration */
export * from './classes.handlertypedrouter.js';
// Service worker utilities - uses legacy patterns, will be migrated
import * as serviceworker from './tools.serviceworker.js';
export { serviceworker };

View File

@@ -1,60 +1,92 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as interfaces from '../../dist_ts_interfaces/index.js'
import { Handler } from './classes.handler.js';
import * as interfaces from '../../dist_ts_interfaces/index.js';
import type { TypedServer } from '../classes.typedserver.js';
import { HandlerTypedRouter } from './classes.handlertypedrouter.js';
const swBundleJs: string = plugins.smartfile.fs.toStringSync(
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js')
);
const swBundleJsMap: string = plugins.smartfile.fs.toStringSync(
plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map')
);
// Lazy-loaded service worker bundle content
let swBundleJs: string | null = null;
let swBundleJsMap: string | null = null;
const loadServiceWorkerBundle = async (): Promise<void> => {
if (swBundleJs === null) {
swBundleJs = (await plugins.fsInstance
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js'))
.encoding('utf8')
.read()) as string;
}
if (swBundleJsMap === null) {
swBundleJsMap = (await plugins.fsInstance
.file(plugins.path.join(paths.serviceworkerBundleDir, './serviceworker.bundle.js.map'))
.encoding('utf8')
.read()) as string;
}
};
let swVersionInfo: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
null;
const serviceworkerHandler = new Handler(
'GET',
async (req, res) => {
if (req.path === '/serviceworker.bundle.js') {
res.status(200);
res.set('Content-Type', 'text/javascript');
res.write(swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`);
} else if (req.path === '/serviceworker.bundle.js.map') {
res.status(200);
res.set('Content-Type', 'application/json');
res.write(swBundleJsMap);
}
res.end();
}
);
export const addServiceWorkerRoute = (
typedserverInstance: TypedServer,
swDataFunc: () => interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response']
) => {
// lets the version info as unique string;
// Set the version info
swVersionInfo = swDataFunc();
// the basic stuff
typedserverInstance.server.addRoute('/serviceworker/*splat', serviceworkerHandler);
// Service worker bundle handler
typedserverInstance.addRoute('/serviceworker/*splat', 'GET', async (request: Request) => {
await loadServiceWorkerBundle();
const url = new URL(request.url);
const path = url.pathname;
// the typed stuff
const typedrouter = new plugins.typedrequest.TypedRouter();
if (path === '/serviceworker/serviceworker.bundle.js' || path === '/serviceworker.bundle.js') {
return new Response(
swBundleJs + '\n' + `/** appSemVer: ${swVersionInfo?.appSemVer || 'not set'} */`,
{
status: 200,
headers: { 'Content-Type': 'text/javascript' },
}
);
} else if (
path === '/serviceworker/serviceworker.bundle.js.map' ||
path === '/serviceworker.bundle.js.map'
) {
return new Response(swBundleJsMap, {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
'serviceworker_versionInfo',
async (req) => {
const versionInfoResponse = swDataFunc();
return versionInfoResponse;
}
)
);
return null;
});
typedserverInstance.server.addRoute(
'/sw-typedrequest',
new HandlerTypedRouter(typedrouter)
);
// Typed request handler for service worker
typedserverInstance.addRoute('/sw-typedrequest', 'POST', async (request: Request) => {
try {
const body = await request.json();
// Create a local typed router for service worker requests
const typedrouter = new plugins.typedrequest.TypedRouter();
typedrouter.addTypedHandler(
new plugins.typedrequest.TypedHandler<interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo>(
'serviceworker_versionInfo',
async () => {
return swDataFunc();
}
)
);
const response = await typedrouter.routeAndAddResponse(body);
return new Response(plugins.smartjson.stringify(response), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response(JSON.stringify({ error: 'Invalid request' }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
});
}
});
};

View File

@@ -1,9 +1,7 @@
import { TypedServer } from '../classes.typedserver.js';
import * as servertools from '../servertools/index.js';
import * as plugins from '../plugins.js';
export interface ILoleServiceServerConstructorOptions {
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
serviceName: string;
serviceVersion: string;
serviceDomain: string;
@@ -20,12 +18,12 @@ export class UtilityServiceServer {
}
public async start() {
console.log('starting lole-serviceserver...')
console.log('starting lole-serviceserver...');
this.typedServer = new TypedServer({
cors: true,
domain: this.options.serviceDomain,
forceSsl: false,
port: this.options.port || 3000,
port: this.options.port || 3000,
robots: true,
defaultAnswer: async () => {
const InfoHtml = (await import('../infohtml/index.js')).InfoHtml;
@@ -37,9 +35,9 @@ export class UtilityServiceServer {
},
});
// lets add any custom routes
// Add any custom routes
if (this.options.addCustomRoutes) {
await this.options.addCustomRoutes(this.typedServer.server);
await this.options.addCustomRoutes(this.typedServer);
}
await this.typedServer.start();

View File

@@ -1,11 +1,10 @@
import * as interfaces from '../../dist_ts_interfaces/index.js';
import { type IServerOptions, TypedServer } from '../classes.typedserver.js';
import type { Request, Response } from '../index.js';
import * as plugins from '../plugins.js';
import * as servertools from '../servertools/index.js';
export interface IUtilityWebsiteServerConstructorOptions {
addCustomRoutes?: (serverArg: servertools.Server) => Promise<any>;
addCustomRoutes?: (typedserver: TypedServer) => Promise<any>;
appSemVer?: string;
domain: string;
serveDir: string;
@@ -16,7 +15,6 @@ export interface IUtilityWebsiteServerConstructorOptions {
* the utility website server implements a best practice server for websites
* It supports:
* * live reload
* * compression
* * serviceworker
* * pwa manifest
*/
@@ -30,7 +28,7 @@ export class UtilityWebsiteServer {
}
/**
*
* Start the website server
*/
public async start(portArg = 3000) {
this.typedserver = new TypedServer({
@@ -38,8 +36,6 @@ export class UtilityWebsiteServer {
injectReload: true,
watch: true,
serveDir: this.options.serveDir,
enableCompression: true,
preferredCompressionMethod: 'gzip',
domain: this.options.domain,
forceSsl: false,
manifest: {
@@ -58,33 +54,32 @@ export class UtilityWebsiteServer {
sitemap: true,
});
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] =
{
appHash: 'xxxxxx',
appSemVer: this.options.appSemVer || 'x.x.x',
};
let lswData: interfaces.serviceworker.IRequest_Serviceworker_Backend_VersionInfo['response'] = {
appHash: 'xxxxxx',
appSemVer: this.options.appSemVer || 'x.x.x',
};
// -> /lsw* - anything regarding serviceworker
servertools.serviceworker.addServiceWorkerRoute(this.typedserver, () => {
return lswData;
});
// lets add ads.txt
this.typedserver.server.addRoute(
'/ads.txt',
new servertools.Handler('GET', async (req, res) => {
res.type('txt/plain');
const adsTxt =
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
res.write(adsTxt);
res.end();
})
);
// ads.txt handler
this.typedserver.addRoute('/ads.txt', 'GET', async () => {
const adsTxt =
['google.com, pub-4104137977476459, DIRECT, f08c47fec0942fa0'].join('\n') + '\n';
return new Response(adsTxt, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
});
});
this.typedserver.server.addRoute(
// Asset broker manifest handler
this.typedserver.addRoute(
'/assetbroker/manifest/:manifestAsset',
new servertools.Handler('GET', async (req, res) => {
let manifestAssetName = req.params.manifestAsset;
'GET',
async (request: Request) => {
let manifestAssetName = (request as any).params?.manifestAsset;
if (manifestAssetName === 'favicon.png') {
manifestAssetName = `favicon_${this.options.domain
.replace('.', '')
@@ -95,19 +90,19 @@ export class UtilityWebsiteServer {
const smartRequest = plugins.smartrequest.SmartRequest.create();
const response = await smartRequest.url(fullOriginAssetUrl).get();
const arrayBuffer = await response.arrayBuffer();
const dataBuffer: Buffer = Buffer.from(arrayBuffer);
res.type('.png');
res.write(dataBuffer);
res.end();
})
return new Response(arrayBuffer, {
status: 200,
headers: { 'Content-Type': 'image/png' },
});
}
);
// lets add any custom routes
// Add any custom routes
if (this.options.addCustomRoutes) {
await this.options.addCustomRoutes(this.typedserver.server);
await this.options.addCustomRoutes(this.typedserver);
}
// -> /* - serve the files
// Subscribe to serve directory hash changes
this.typedserver.serveDirHashSubject.subscribe((appHash: string) => {
lswData = {
appHash,
@@ -115,11 +110,11 @@ export class UtilityWebsiteServer {
};
});
// lets setup the typedrouter chain
// Setup the typedrouter chain
this.typedserver.typedrouter.addTypedRouter(this.typedrouter);
// lets start everything
console.log('routes are all set. Startin up now!');
// Start everything
console.log('routes are all set. Starting up now!');
await this.typedserver.start();
console.log('typedserver started!');
}
@@ -127,14 +122,4 @@ export class UtilityWebsiteServer {
public async stop() {
await this.typedserver.stop();
}
/**
* allows you to hanlde requests from other server instances without the need to listen for yourself
* note smartexpress allows you start the instance wuith passing >>false<< as second parameter to .start();
* @param req
* @param res
*/
public async handleRequest(req: Request, res: Response) {
await this.typedserver.server.handleReqRes(req, res);
}
}

View File

@@ -102,17 +102,13 @@ export class ReloadChecker {
this.typedrouter,
plugins.typedsocket.TypedSocket.useWindowLocationOriginUrl()
);
this.typedsocket.addTag('typedserver_frontend', {});
this.typedsocket.eventSubject.subscribe(async (eventArg) => {
console.log(`typedsocket event subscription: ${eventArg}`);
if (
eventArg === 'disconnected' ||
eventArg === 'disconnecting' ||
eventArg === 'timedOut'
) {
await this.typedsocket.setTag('typedserver_frontend', {});
this.typedsocket.statusSubject.subscribe(async (statusArg) => {
console.log(`typedsocket status: ${statusArg}`);
if (statusArg === 'disconnected' || statusArg === 'reconnecting') {
this.backendConnectionLost = true;
this.infoscreen.setText(`typedsocket ${eventArg}!`);
} else if (eventArg === 'connected' && this.backendConnectionLost) {
this.infoscreen.setText(`typedsocket ${statusArg}!`);
} else if (statusArg === 'connected' && this.backendConnectionLost) {
this.backendConnectionLost = false;
this.infoscreen.setSuccess('typedsocket connected!');
// lets check if a reload is necessary

View File

@@ -14,10 +14,10 @@ export class TypedserverInfoscreen extends LitElement {
//INSTANCE
@property()
private text = 'Hello';
accessor text = 'Hello';
@property()
private success = false;
accessor success = false;
public static styles = [
css`

View File

@@ -4,7 +4,7 @@ import { logger } from './logging.js';
export class NetworkManager {
public serviceWorkerRef: ServiceWorker;
public webRequest: plugins.webrequest.WebRequest;
public webRequest: plugins.webrequest.WebrequestClient;
private isOffline: boolean = false;
private lastOnlineCheck: number = 0;
private readonly ONLINE_CHECK_INTERVAL = 30000; // 30 seconds
@@ -13,7 +13,7 @@ export class NetworkManager {
constructor(serviceWorkerRefArg: ServiceWorker) {
this.serviceWorkerRef = serviceWorkerRefArg;
this.webRequest = new plugins.webrequest.WebRequest();
this.webRequest = new plugins.webrequest.WebrequestClient();
// Listen for connection changes
this.getConnection()?.addEventListener('change', () => {

View File

@@ -1,7 +1,5 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",