Compare commits

...

14 Commits

5 changed files with 313 additions and 119 deletions

View File

@ -1,5 +1,78 @@
# Changelog
## 2025-02-04 - 3.0.65 - fix(readme)
Update documentation with advanced usage and examples
- Added section on advanced usage including service worker and edge worker setup
- Detailed integration examples for type-safe API requests and WebSocket communication
- Expanded configuration options and cache strategies
## 2025-02-04 - 3.0.64 - fix(serviceworker)
Improve cache handling and response header management in service worker.
- Addressed issue preventing caching of certain responses due to missing CORS headers.
- Added 'Vary: Origin' header to ensure proper response handling.
- Included 'Access-Control-Expose-Headers' for better CORS support.
## 2025-02-04 - 3.0.63 - fix(core)
Refactored caching strategy for service worker to improve compatibility and performance.
- Removed hard and soft caching distinctions.
- Simplified cache setup process.
- Improved browser caching control headers.
## 2025-02-04 - 3.0.62 - fix(Service Worker)
Refactor and clean up the cache logic in the Service Worker to improve maintainability and handle Safari-specific cache behavior.
- Refactored logic for determining cached domains, enhancing the readability and maintainability of the code.
- Improved handling of CORS settings in caching requests, notably bypassing caching for soft cached domains in Safari to avoid CORS issues.
- Enhanced error response creation for failed resource fetching, maintaining clarity on why and how certain resources were not fetched or cached.
- Revised the structure of the caching logic to ensure consistent behavior across all supported browsers.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.61 - fix(ServiceWorkerCacheManager)
Fixed caching mechanism to better support Safari's handling of soft-cached domains.
- Added logic to differentiate between hard and soft cached domains.
- Implemented special handling for soft cached domains on Safari by bypassing caching.
- Ensured appropriate CORS headers are present in cached responses.
- Improved error handling with informative 500 error responses.
- Optimized caching logic to prevent redundant caching and potential issues with locked streams on Safari.
## 2025-02-04 - 3.0.60 - fix(cachemanager)
Improve cache management and error handling
- Updated comments for clarity and consistency.
- Enhanced error handling in `fetch` event listener.
- Optimized cache key management and cleanup process.
- Ensured CORS headers are set for cached responses.
- Improved logging for caching operations.
## 2025-02-03 - 3.0.59 - fix(serviceworker)
Fixed CORS and Cache Control handling for Service Worker
- Improved handling of CORS settings for external requests.
- Preserved important headers while excluding caching headers.
- Ensured the presence of CORS headers in cached responses.
- Adjusted Cache-Control headers to prevent browser caching but allow service worker caching.
## 2025-02-03 - 3.0.58 - fix(network-manager)
Refined network management logic for better offline handling.

View File

@ -1,6 +1,6 @@
{
"name": "@api.global/typedserver",
"version": "3.0.58",
"version": "3.0.65",
"description": "A TypeScript-based project for easy serving of static files with support for live reloading, compression, and typed requests.",
"type": "module",
"exports": {

182
readme.md
View File

@ -1,52 +1,168 @@
```markdown
# @api.global/typedserver
Easy serving of static files
## Install
To install @api.global/typedserver, run the following command in your terminal:
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.
## Features
- **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
## Components
### Core Server (`ts/`)
- Static file serving with Express
- Type-safe request handling
- Live reload functionality
- Compression middleware
### 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
```bash
npm install @api.global/typedserver --save
npm install @api.global/typedserver
```
This will add `@api.global/typedserver` to your project's dependencies.
## Usage
`@api.global/typedserver` is designed to make serving static files and handling web requests in a TypeScript environment easy and efficient. It leverages Express under the hood, providing a powerful API for web server creation with additional utilities for live reloading, typed requests/responses, and more, embracing TypeScript's static typing advantages.
### Setting up a Basic Web Server
The following example demonstrates how to set up a basic web server serving files from a directory.
## Quick Start
```typescript
import { TypedServer } from '@api.global/typedserver';
const serverOptions = {
port: 8080, // Port to listen on
serveDir: 'public', // Directory to serve static files from
watch: true, // Enable live reloading of changes
injectReload: true, // Inject live reload script into served HTML files
cors: true // Enable CORS
};
const server = new TypedServer({
port: 3000,
serveDir: './public',
watch: true,
compression: true
});
const typedServer = new TypedServer(serverOptions);
async function startServer() {
await typedServer.start();
console.log(`Server is running on http://localhost:${serverOptions.port}`);
}
startServer().catch(console.error);
server.start();
```
In the example above, `TypedServer` is instantiated with an `IServerOptions` object specifying options like the port to listen on (`8080`), the directory containing static files to serve (`public`), and live reload features. Calling `start()` on the `typedServer` instance initiates the server.
## Type-Safe API Integration
### Using Typed Requests and Responses
### HTTP Requests with TypedRequest
```typescript
import { TypedRequest } from '@api.global/typedrequest';
`TypedServer` supports typed requests and responses, making API development more robust and maintainable. Define your request and response types, and use them to type-check incoming requests and their responses.
// Define your request/response interface
interface IUserRequest {
method: 'getUser';
request: { userId: string };
response: { username: string; email: string; };
}
First, define the types:
// Create and use a typed request
const getUserRequest = new TypedRequest<IUserRequest>('/api/users', 'getUser');
const user = await getUserRequest.fire({ userId: '123' });
```
### WebSocket Communication
```typescript
import { TypedSocket } from '@api.global/typedsocket';
// 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

View File

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

View File

@ -16,25 +16,29 @@ export class CacheManager {
}
private _setupCache = () => {
const createMatchRequest = (requestArg: Request) => {
// lets create a matchRequest
const createMatchRequest = (requestArg: Request): Request => {
// Create a matchRequest based on whether the request is internal or external.
let matchRequest: Request;
if (requestArg.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin)) {
// internal request
// Internal request; use the original.
matchRequest = requestArg;
} else {
// External request; create a new Request with appropriate CORS settings.
matchRequest = new Request(requestArg.url, {
...requestArg.clone(),
mode: 'cors'
method: requestArg.method,
headers: requestArg.headers,
mode: 'cors',
credentials: 'same-origin',
redirect: 'follow'
});
}
return matchRequest;
};
/**
* creates a 500 response
* Creates a 500 error response.
*/
const create500Response = async (requestArg: Request, responseArg: Response) => {
const create500Response = async (requestArg: Request, responseArg: Response): Promise<Response> => {
return new Response(
`
<html>
@ -50,16 +54,15 @@ export class CacheManager {
</style>
</head>
<body>
<div class="note">
<strong>serviceworker running, but status 500</strong><br>
</div>
serviceworker is unable to fetch this request<br>
Here is some info about the request/response pair:<br>
<br>
requestUrl: ${requestArg.url}<br>
responseType: ${responseArg.type}<br>
responseBody: ${await responseArg.clone().text()}<br>
<div class="note">
<strong>serviceworker running, but status 500</strong><br>
</div>
serviceworker is unable to fetch this request<br>
Here is some info about the request/response pair:<br>
<br>
requestUrl: ${requestArg.url}<br>
responseType: ${responseArg.type}<br>
responseBody: ${await responseArg.clone().text()}<br>
</body>
</html>
`,
@ -71,43 +74,41 @@ export class CacheManager {
}
);
};
// A list of local resources we always want to be cached.
// Listen for fetch events.
this.losslessServiceWorkerRef.serviceWindowRef.addEventListener('fetch', async (fetchEventArg: any) => {
// Lets block scopes we don't want to be passing through the serviceworker
// Block specific scopes.
const originalRequest: Request = fetchEventArg.request;
const parsedUrl = new URL(originalRequest.url);
if (
parsedUrl.hostname.includes('paddle.com')
|| parsedUrl.hostname.includes('paypal.com')
|| parsedUrl.hostname.includes('reception.lossless.one')
|| parsedUrl.pathname.startsWith('/socket.io')
|| originalRequest.url.startsWith('https://umami.')
parsedUrl.hostname.includes('paddle.com') ||
parsedUrl.hostname.includes('paypal.com') ||
parsedUrl.hostname.includes('reception.lossless.one') ||
parsedUrl.pathname.startsWith('/socket.io') ||
originalRequest.url.startsWith('https://umami.')
) {
logger.log('note',`serviceworker not active for ${parsedUrl.toString()}`);
logger.log('note', `serviceworker not active for ${parsedUrl.toString()}`);
return;
}
// lets continue for the rest
// Create a deferred response.
const done = plugins.smartpromise.defer<Response>();
fetchEventArg.respondWith(done.promise);
if (
(originalRequest.method === 'GET' &&
(originalRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin) &&
!originalRequest.url.includes('/api/') &&
!originalRequest.url.includes('smartserve/reloadcheck'))) ||
(originalRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin) &&
!originalRequest.url.includes('/api/') &&
!originalRequest.url.includes('smartserve/reloadcheck'))) ||
originalRequest.url.includes('https://assetbroker.') ||
originalRequest.url.includes('https://unpkg.com') ||
originalRequest.url.includes('https://fonts.googleapis.com') ||
originalRequest.url.includes('https://fonts.gstatic.com')
) {
// lets see if things need to be updated
// not waiting here
// Check for updates asynchronously.
this.losslessServiceWorkerRef.updateManager.checkUpdate(this);
// this code block is executed for local requests
// Try to serve from cache.
const matchRequest = createMatchRequest(originalRequest);
const cachedResponse = await caches.match(matchRequest);
if (cachedResponse) {
@ -115,61 +116,73 @@ export class CacheManager {
done.resolve(cachedResponse);
return;
}
// in case there is no cached response
// In case there is no cached response, fetch from the network.
logger.log('info', `NOTYETCACHED: trying to cache ${matchRequest.url}`);
const newResponse: Response = await fetch(matchRequest).catch(async err => {
return await create500Response(matchRequest, new Response(err.message));
});
// fill cache
// Put a copy of the response in the runtime cache.
if (newResponse.status > 299 || newResponse.type === 'opaque') {
// If the response status is an error or the response is opaque, do not cache it.
if (newResponse.status > 299 || newResponse.type === 'opaque' || (newResponse.headers.get('access-control-allow-origin') === null && !matchRequest.url.startsWith(this.losslessServiceWorkerRef.serviceWindowRef.location.origin))) {
logger.log(
'error',
`NOTCACHED: can't cache response for ${matchRequest.url} due to status ${
newResponse.status
} and type ${newResponse.type}`
`NOTCACHED: not caching response for ${matchRequest.url} due to status ${newResponse.status} and type ${newResponse.type}`
);
done.resolve(await create500Response(matchRequest, newResponse));
// Simply return the network response without caching.
done.resolve(newResponse);
return;
} else {
// Cache the response.
const cache = await caches.open(this.usedCacheNames.runtimeCacheName);
const responseToPutToCache = newResponse.clone();
const headers = new Headers();
responseToPutToCache.headers.forEach((value, key) => {
if (
value !== 'Cache-Control'
&& value !== 'cache-control'
&& value !== 'Expires'
&& value !== 'expires'
&& value !== 'Pragma'
&& value !== 'pragma'
) {
// Preserve all headers except caching-related ones.
if (![
'Cache-Control',
'cache-control',
'Expires',
'expires',
'Pragma',
'pragma'
].includes(key)) {
headers.set(key, value);
}
});
// Prevent browser caching while allowing service worker caching
// Ensure CORS headers are present.
if (!headers.has('Access-Control-Allow-Origin')) {
headers.set('Access-Control-Allow-Origin', '*');
}
headers.set('Vary', 'Origin');
if (!headers.has('Access-Control-Expose-Headers')) {
headers.set('Access-Control-Expose-Headers', '*')
}
if (!headers.has('Access-Control-Allow-Methods')) {
headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
}
if (!headers.has('Access-Control-Allow-Headers')) {
headers.set('Access-Control-Allow-Headers', 'Content-Type');
}
// Prevent browser caching while allowing service worker caching.
headers.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
headers.set('Pragma', 'no-cache');
headers.set('Expires', '0');
headers.set('Surrogate-Control', 'no-store');
await cache.put(matchRequest, new Response(responseToPutToCache.body, {
...responseToPutToCache,
headers
}));
logger.log(
'ok',
`NOWCACHED: cached response for ${matchRequest.url} for subsequent requests!`
);
logger.log('ok', `NOWCACHED: cached response for ${matchRequest.url} for subsequent requests!`);
done.resolve(newResponse);
}
} else {
// this code block is executed for remote requests
// For remote requests not intended for caching, fetch directly from the origin.
logger.log(
'ok',
`NOTCACHED: not caching any responses for ${
originalRequest.url
}. Fetching from origin now...`
`NOTCACHED: not caching any responses for ${originalRequest.url}. Fetching from origin now...`
);
done.resolve(
await fetch(originalRequest).catch(async err => {
@ -181,39 +194,31 @@ export class CacheManager {
}
/**
* update caches
* @param reasonArg
*/
/**
* cleans all caches
* should only be run when running a new service worker
* @param reasonArg
* Cleans all caches.
* Should only be run when a new service worker is activated.
*/
public cleanCaches = async (reasonArg = 'no reason given') => {
logger.log('info', `MAJOR CACHEEVENT: cleaning caches now! Reason: ${reasonArg}`);
const cacheNames = await caches.keys();
const deletePromises = cacheNames.map(cacheToDelete => {
const deletePromise = caches.delete(cacheToDelete);
deletePromise.then(() => {
logger.log('ok', `Deleted cache ${cacheToDelete}`);
});
return deletePromise;
const deletePromise = caches.delete(cacheToDelete);
deletePromise.then(() => {
logger.log('ok', `Deleted cache ${cacheToDelete}`);
});
return deletePromise;
});
await Promise.all(deletePromises);
}
/**
* revalidate cache
* Revalidates the runtime cache.
*/
public async revalidateCache() {
const runtimeCache = await caches.open(this.usedCacheNames.runtimeCacheName);
const cacheKeys = await runtimeCache.keys();
for (const requestArg of cacheKeys) {
const cachedResponse = runtimeCache.match(requestArg);
// lets get a new response for comparison
// Fetch a new response for comparison.
const clonedRequest = requestArg.clone();
const response = await plugins.smartpromise.timeoutWrap(fetch(clonedRequest), 5000); // Increased timeout for better mobile compatibility
if (response && response.status >= 200 && response.status < 300) {
@ -222,4 +227,4 @@ export class CacheManager {
}
}
}
}
}