Initial commit: Set up monorepo with SSE client package
This commit is contained in:
commit
b4d35ac4ab
72
.github/workflows/publish.yml
vendored
Normal file
72
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Publish Package
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: 'Package directory to publish (e.g., packages/sse-client)'
|
||||
required: true
|
||||
version_bump:
|
||||
description: 'Version bump (patch, minor, major, none, or version:x.y.z)'
|
||||
required: true
|
||||
default: 'patch'
|
||||
type: 'string'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Set up PNPM
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- name: Set up SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.GITEA_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan git.generation.one >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Set up Git
|
||||
run: |
|
||||
git config --global user.name "GitHub Actions"
|
||||
git config --global user.email "actions@github.com"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Publish package
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||
run: |
|
||||
echo "@g1:registry=https://git.generation.one/api/packages/GenerationOne/npm/" > .npmrc
|
||||
echo "//git.generation.one/api/packages/GenerationOne/npm/:_authToken=${GITEA_TOKEN}" >> .npmrc
|
||||
echo "legacy-peer-deps=true" >> .npmrc
|
||||
echo "resolution-mode=highest" >> .npmrc
|
||||
|
||||
cd ${{ github.event.inputs.package }}
|
||||
|
||||
if [ "${{ github.event.inputs.version_bump }}" != "none" ]; then
|
||||
# Check if it's a specific version (format: version:x.y.z)
|
||||
if [[ "${{ github.event.inputs.version_bump }}" == version:* ]]; then
|
||||
SPECIFIC_VERSION="${{ github.event.inputs.version_bump }}"
|
||||
SPECIFIC_VERSION="${SPECIFIC_VERSION#version:}"
|
||||
echo "Setting specific version ($SPECIFIC_VERSION)..."
|
||||
npm version "$SPECIFIC_VERSION" --no-git-tag-version --allow-same-version
|
||||
else
|
||||
echo "Bumping version (${{ github.event.inputs.version_bump }})..."
|
||||
npm version ${{ github.event.inputs.version_bump }} --no-git-tag-version
|
||||
fi
|
||||
fi
|
||||
|
||||
pnpm build
|
||||
pnpm publish --no-git-checks
|
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
build/
|
||||
lib/
|
||||
coverage/
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Deploy keys
|
||||
.deploy-key
|
||||
.deploy-key.pub
|
||||
.gitea-token
|
||||
|
||||
# Editor directories and files
|
||||
.idea/
|
||||
.vscode/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
127
docs/DEPLOY_KEYS.md
Normal file
127
docs/DEPLOY_KEYS.md
Normal file
@ -0,0 +1,127 @@
|
||||
# Deploy Keys for Publishing Packages
|
||||
|
||||
This document explains how to set up and use deploy keys for publishing packages to the Generation One Gitea repository.
|
||||
|
||||
## What are Deploy Keys?
|
||||
|
||||
Deploy keys are SSH keys that grant access to a specific repository. They are more secure than personal access tokens because:
|
||||
|
||||
1. They can be limited to a single repository
|
||||
2. They can be read-only or read-write
|
||||
3. They don't grant access to your personal account
|
||||
|
||||
## Setting Up Deploy Keys
|
||||
|
||||
### 1. Generate a Deploy Key
|
||||
|
||||
Run the provided script to generate a new deploy key:
|
||||
|
||||
```bash
|
||||
# Make the script executable
|
||||
chmod +x scripts/setup-deploy-key.sh
|
||||
|
||||
# Run the script
|
||||
./scripts/setup-deploy-key.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Generate a new SSH key pair (`.deploy-key` and `.deploy-key.pub`)
|
||||
- Add the public key to the Gitea repository as a deploy key
|
||||
|
||||
### 2. Create a Gitea Token
|
||||
|
||||
1. Log in to https://git.generation.one
|
||||
2. Go to your user settings (click your profile picture)
|
||||
3. Select "Applications" or "Access Tokens"
|
||||
4. Create a new token with the "packages:write" scope
|
||||
5. Save the token to a file named `.gitea-token` in the root of the repository
|
||||
|
||||
## Creating New Packages
|
||||
|
||||
### Setting the Initial Version
|
||||
|
||||
When creating a new package, you can set the initial version in the `package.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@g1/your-package-name",
|
||||
"version": "0.2.0",
|
||||
"description": "Your package description",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Choosing a version like `0.2.0` for a new package is a good practice as it indicates:
|
||||
- The package is still in development (0.x.x)
|
||||
- It has gone through some initial development (0.2.x rather than 0.1.x)
|
||||
- It's starting with a clean minor version (x.x.0)
|
||||
|
||||
## Publishing Packages
|
||||
|
||||
### Manual Publishing
|
||||
|
||||
Use the provided script to publish a package:
|
||||
|
||||
```bash
|
||||
# Make the script executable
|
||||
chmod +x scripts/publish-package.sh
|
||||
|
||||
# Publish a package with a patch version bump
|
||||
./scripts/publish-package.sh packages/sse-client patch
|
||||
|
||||
# Publish a package with a specific version
|
||||
./scripts/publish-package.sh packages/sse-client version:0.2.0
|
||||
|
||||
# Publish a package without changing the version
|
||||
./scripts/publish-package.sh packages/sse-client none
|
||||
```
|
||||
|
||||
### Automated Publishing with GitHub Actions
|
||||
|
||||
A GitHub Actions workflow is included in this repository for automated publishing:
|
||||
|
||||
1. Go to the "Actions" tab in your GitHub repository
|
||||
2. Select the "Publish Package" workflow
|
||||
3. Click "Run workflow"
|
||||
4. Enter the package directory (e.g., `packages/sse-client`)
|
||||
5. Select the version bump type (patch, minor, major, or none)
|
||||
6. Click "Run workflow"
|
||||
|
||||
### Setting Up GitHub Actions Secrets
|
||||
|
||||
For the GitHub Actions workflow to work, you need to add the following secrets to your repository:
|
||||
|
||||
1. `GITEA_DEPLOY_KEY`: The contents of the `.deploy-key` file
|
||||
2. `GITEA_TOKEN`: The contents of the `.gitea-token` file
|
||||
|
||||
To add these secrets:
|
||||
1. Go to your repository on GitHub
|
||||
2. Click on "Settings"
|
||||
3. Click on "Secrets and variables" > "Actions"
|
||||
4. Click on "New repository secret"
|
||||
5. Add each secret with the appropriate name and value
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Never commit the private key or token to the repository**
|
||||
- The `.deploy-key`, `.deploy-key.pub`, and `.gitea-token` files are already in `.gitignore`
|
||||
- Rotate the deploy key and token periodically for better security
|
||||
- Limit the permissions of the token to only what is necessary (packages:write)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
If you encounter authentication errors when publishing:
|
||||
|
||||
1. Verify that your Gitea token has the correct permissions
|
||||
2. Check that the token is correctly set in the `.gitea-token` file
|
||||
3. Ensure the deploy key has been added to the repository with write access
|
||||
|
||||
### SSH Key Issues
|
||||
|
||||
If there are issues with the SSH key:
|
||||
|
||||
1. Verify that the key has been added to the repository
|
||||
2. Check the permissions on the `.deploy-key` file (should be `600`)
|
||||
3. Try regenerating the key with the setup script
|
8
g1-ts-common-packages.code-workspace
Normal file
8
g1-ts-common-packages.code-workspace
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "g1-ts-common-packages",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"test": "pnpm -r test",
|
||||
"clean": "pnpm -r exec -- rm -rf dist node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"glob": "^7.2.3",
|
||||
"inflight": "^1.0.6"
|
||||
}
|
||||
}
|
4
packages/sse-client/.npmrc
Normal file
4
packages/sse-client/.npmrc
Normal file
@ -0,0 +1,4 @@
|
||||
@g1:registry=https://git.generation.one/api/packages/GenerationOne/npm/
|
||||
//git.generation.one/api/packages/GenerationOne/npm/:_authToken=c44ffb795a86532ca684f7a6642219fe6a15cf85
|
||||
legacy-peer-deps=true
|
||||
resolution-mode=highest
|
21
packages/sse-client/LICENSE
Normal file
21
packages/sse-client/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
156
packages/sse-client/PUBLISHING.md
Normal file
156
packages/sse-client/PUBLISHING.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Publishing the SSE Client Package
|
||||
|
||||
This document explains how to build and publish the SSE client package to the Generation One Gitea repository.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js and pnpm installed
|
||||
- Access to the Generation One Gitea repository
|
||||
- Gitea access token with `packages:write` permission
|
||||
|
||||
## Building the Package
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
cd sse-client
|
||||
pnpm install
|
||||
```
|
||||
|
||||
2. Build the package:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
This will compile the TypeScript code and generate the distribution files in the `dist` directory.
|
||||
|
||||
## Configuration for Publishing
|
||||
|
||||
The package is already configured for publishing to the Generation One Gitea repository with the following files:
|
||||
|
||||
### package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@g1/sse-client",
|
||||
"version": "1.0.0",
|
||||
"description": "A custom SSE client that supports headers and bypasses certificate issues",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.generation.one/GenerationOne/g1-ts-common-packages.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://git.generation.one/api/packages/GenerationOne/npm/"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### .npmrc
|
||||
|
||||
```
|
||||
# Gitea registry configuration
|
||||
@g1:registry=https://git.generation.one/api/packages/GenerationOne/npm/
|
||||
//git.generation.one/api/packages/GenerationOne/npm/:_authToken=${GITEA_TOKEN}
|
||||
```
|
||||
|
||||
## Publishing to Gitea
|
||||
|
||||
### Method 1: Direct Publishing
|
||||
|
||||
1. Generate an access token in Gitea:
|
||||
- Log in to https://git.generation.one
|
||||
- Go to your user settings (click your profile picture)
|
||||
- Select "Applications" or "Access Tokens"
|
||||
- Create a new token with the "packages:write" scope
|
||||
- Copy the generated token
|
||||
|
||||
2. Set the token as an environment variable:
|
||||
|
||||
```powershell
|
||||
$env:GITEA_TOKEN = "your-token-here"
|
||||
```
|
||||
|
||||
3. Publish the package:
|
||||
|
||||
```powershell
|
||||
pnpm publish --no-git-checks
|
||||
```
|
||||
|
||||
The `--no-git-checks` flag allows publishing without committing to Git first.
|
||||
|
||||
### Method 2: Commit to Git Repository First
|
||||
|
||||
If you prefer to commit the code to the Git repository before publishing:
|
||||
|
||||
1. Initialize Git and add the remote:
|
||||
|
||||
```powershell
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit of SSE client package"
|
||||
git remote add origin https://git.generation.one/GenerationOne/g1-ts-common-packages.git
|
||||
```
|
||||
|
||||
2. Create a branch for the package:
|
||||
|
||||
```powershell
|
||||
git checkout -b feature/sse-client
|
||||
```
|
||||
|
||||
3. Push to Gitea:
|
||||
|
||||
```powershell
|
||||
git push -u origin feature/sse-client
|
||||
```
|
||||
|
||||
4. Then publish the package:
|
||||
|
||||
```powershell
|
||||
$env:GITEA_TOKEN = "your-token-here"
|
||||
pnpm publish
|
||||
```
|
||||
|
||||
## Using the Published Package
|
||||
|
||||
After publishing, you can use the package in other projects:
|
||||
|
||||
1. Add it to your dependencies:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@g1/sse-client": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Configure your project's .npmrc file:
|
||||
|
||||
```
|
||||
@g1:registry=https://git.generation.one/api/packages/GenerationOne/npm/
|
||||
//git.generation.one/api/packages/GenerationOne/npm/:_authToken=${GITEA_TOKEN}
|
||||
```
|
||||
|
||||
3. Install the package:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Updating the Package
|
||||
|
||||
To update the package:
|
||||
|
||||
1. Make your changes
|
||||
2. Update the version in package.json (following semantic versioning)
|
||||
3. Build the package: `pnpm build`
|
||||
4. Publish the new version: `pnpm publish --no-git-checks`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Authentication Error**: Make sure your GITEA_TOKEN environment variable is set correctly
|
||||
- **Version Conflict**: If the version already exists, update the version number in package.json
|
||||
- **Registry Not Found**: Verify the registry URL in .npmrc and package.json
|
176
packages/sse-client/README.md
Normal file
176
packages/sse-client/README.md
Normal file
@ -0,0 +1,176 @@
|
||||
# SSE Client
|
||||
|
||||
A custom SSE (Server-Sent Events) client that supports headers and bypasses certificate issues. This package provides a robust implementation for connecting to SSE endpoints with features like:
|
||||
|
||||
- Custom headers support
|
||||
- SSL certificate validation bypass
|
||||
- Automatic reconnection with exponential backoff
|
||||
- Connection management
|
||||
- Event handling
|
||||
- TypeScript support
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install sse-client
|
||||
# or
|
||||
yarn add sse-client
|
||||
# or
|
||||
pnpm add sse-client
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { SSEClient } from 'sse-client';
|
||||
|
||||
// Create a new SSE client
|
||||
const client = new SSEClient('https://api.example.com/events', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer token123',
|
||||
'X-Custom-Header': 'value'
|
||||
},
|
||||
debug: true // Enable debug logging
|
||||
});
|
||||
|
||||
// Listen for events
|
||||
client.on('message', (event) => {
|
||||
console.log('Received message:', event.parsedData);
|
||||
});
|
||||
|
||||
// Connect to the SSE endpoint
|
||||
client.connect();
|
||||
|
||||
// Later, when you're done
|
||||
client.close();
|
||||
```
|
||||
|
||||
## SSL Certificate Bypass
|
||||
|
||||
In Node.js environments, you can bypass SSL certificate validation:
|
||||
|
||||
```typescript
|
||||
import { SSEClient } from 'sse-client';
|
||||
|
||||
// Create a new SSE client with SSL certificate validation bypass
|
||||
const client = new SSEClient('https://api.example.com/events', {
|
||||
headers: {
|
||||
'Authorization': 'Bearer token123'
|
||||
},
|
||||
skipSSLValidation: true, // Bypass SSL certificate validation
|
||||
debug: true
|
||||
});
|
||||
|
||||
client.on('message', (event) => {
|
||||
console.log('Received message:', event.parsedData);
|
||||
});
|
||||
|
||||
client.connect();
|
||||
```
|
||||
|
||||
This is particularly useful for development environments or when dealing with self-signed certificates.
|
||||
|
||||
## Connection Management
|
||||
|
||||
The package includes a connection manager for handling multiple SSE connections:
|
||||
|
||||
```typescript
|
||||
import { getSSEConnection, closeSSEConnection, closeAllSSEConnections } from 'sse-client';
|
||||
|
||||
// Get or create a connection
|
||||
const client = getSSEConnection(
|
||||
'https://api.example.com/events/orders/123',
|
||||
'order-123', // Unique ID for this connection
|
||||
{
|
||||
headers: {
|
||||
'Authorization': 'Bearer token123'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Listen for events
|
||||
client.on('orderStateChanged', (event) => {
|
||||
console.log('Order state changed:', event.parsedData);
|
||||
});
|
||||
|
||||
// Close a specific connection
|
||||
closeSSEConnection('order-123');
|
||||
|
||||
// Close all connections
|
||||
closeAllSSEConnections();
|
||||
```
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
You can set up automatic cleanup of all SSE connections when the page is unloaded:
|
||||
|
||||
```typescript
|
||||
import { setupSSECleanup } from 'sse-client';
|
||||
|
||||
// Call this once in your application
|
||||
setupSSECleanup();
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `SSEClient` constructor accepts the following options:
|
||||
|
||||
```typescript
|
||||
interface SSEClientOptions {
|
||||
// Headers to include in the SSE request
|
||||
headers?: Record<string, string>;
|
||||
|
||||
// Whether to include credentials in the request (default: true)
|
||||
withCredentials?: boolean;
|
||||
|
||||
// Timeout for heartbeat in milliseconds (default: 21600000 - 6 hours)
|
||||
heartbeatTimeout?: number;
|
||||
|
||||
// Maximum number of retry attempts (default: 10)
|
||||
maxRetryAttempts?: number;
|
||||
|
||||
// Initial delay for retry in milliseconds (default: 1000)
|
||||
initialRetryDelay?: number;
|
||||
|
||||
// Maximum delay for retry in milliseconds (default: 30000)
|
||||
maxRetryDelay?: number;
|
||||
|
||||
// Whether to automatically reconnect on error (default: true)
|
||||
autoReconnect?: boolean;
|
||||
|
||||
// Debug mode (default: false)
|
||||
debug?: boolean;
|
||||
|
||||
// Whether to bypass SSL certificate validation (default: false)
|
||||
// Only works in Node.js environments
|
||||
skipSSLValidation?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
## Event Handling
|
||||
|
||||
The SSE client provides methods for handling events:
|
||||
|
||||
```typescript
|
||||
// Add an event listener
|
||||
client.on('eventName', (event) => {
|
||||
console.log('Event received:', event);
|
||||
});
|
||||
|
||||
// Remove a specific event listener
|
||||
client.off('eventName', listener);
|
||||
|
||||
// Remove all listeners for an event
|
||||
client.removeAllListeners('eventName');
|
||||
|
||||
// Remove all listeners for all events
|
||||
client.removeAllListeners();
|
||||
```
|
||||
|
||||
## Building and Publishing
|
||||
|
||||
For instructions on how to build and publish this package to the Generation One Gitea repository, see [PUBLISHING.md](./PUBLISHING.md).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
108
packages/sse-client/SUMMARY.md
Normal file
108
packages/sse-client/SUMMARY.md
Normal file
@ -0,0 +1,108 @@
|
||||
# SSE Client Package Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides a robust implementation of a Server-Sent Events (SSE) client with the following features:
|
||||
|
||||
- Custom headers support for authentication
|
||||
- SSL certificate validation bypass for development environments
|
||||
- Automatic reconnection with exponential backoff
|
||||
- Connection management for multiple SSE connections
|
||||
- Event handling with JSON parsing
|
||||
- TypeScript support
|
||||
|
||||
## Package Structure
|
||||
|
||||
```
|
||||
sse-client/
|
||||
├── dist/ # Compiled output
|
||||
├── src/
|
||||
│ ├── index.ts # Main entry point
|
||||
│ ├── sseClient.ts # Core SSE client implementation
|
||||
│ ├── utils/
|
||||
│ │ └── debug.ts # Debug utilities
|
||||
│ └── __tests__/
|
||||
│ └── sseClient.test.ts # Unit tests
|
||||
├── examples/
|
||||
│ ├── order-tracking.ts # Basic usage example
|
||||
│ ├── react-order-tracking.tsx # React example
|
||||
│ ├── vue-order-tracking.vue # Vue example
|
||||
│ ├── angular-order-tracking.component.ts # Angular example
|
||||
│ ├── nodejs-sse-client.js # Node.js example
|
||||
│ ├── browser-script.html # Browser script example
|
||||
│ ├── nextjs-sse-proxy.ts # Next.js SSE proxy example
|
||||
│ ├── express-sse-proxy.js # Express SSE proxy example
|
||||
│ └── fastify-sse-proxy.js # Fastify SSE proxy example
|
||||
├── package.json # Package configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
├── jest.config.js # Jest configuration
|
||||
├── .npmrc # npm registry configuration
|
||||
├── .gitignore # Git ignore file
|
||||
├── LICENSE # MIT license
|
||||
├── README.md # Documentation
|
||||
└── PUBLISHING.md # Publishing instructions
|
||||
```
|
||||
|
||||
## Key Components
|
||||
|
||||
### SSEClient
|
||||
|
||||
The main class that handles the connection to the SSE endpoint. It provides methods for:
|
||||
|
||||
- Connecting to the SSE endpoint
|
||||
- Adding event listeners
|
||||
- Handling reconnection
|
||||
- Parsing event data
|
||||
|
||||
### SSEConnectionManager
|
||||
|
||||
A singleton class that manages multiple SSE connections. It provides methods for:
|
||||
|
||||
- Creating and retrieving connections
|
||||
- Closing connections
|
||||
- Cleaning up connections
|
||||
|
||||
### Utility Functions
|
||||
|
||||
- `getSSEConnection`: Gets or creates a connection for a given ID
|
||||
- `closeSSEConnection`: Closes a connection for a given ID
|
||||
- `closeAllSSEConnections`: Closes all connections
|
||||
- `setupSSECleanup`: Sets up automatic cleanup of connections when the page is unloaded
|
||||
|
||||
## Configuration Options
|
||||
|
||||
The `SSEClient` constructor accepts the following options:
|
||||
|
||||
- `headers`: Headers to include in the SSE request
|
||||
- `withCredentials`: Whether to include credentials in the request
|
||||
- `heartbeatTimeout`: Timeout for heartbeat in milliseconds
|
||||
- `maxRetryAttempts`: Maximum number of retry attempts
|
||||
- `initialRetryDelay`: Initial delay for retry in milliseconds
|
||||
- `maxRetryDelay`: Maximum delay for retry in milliseconds
|
||||
- `autoReconnect`: Whether to automatically reconnect on error
|
||||
- `debug`: Debug mode
|
||||
- `skipSSLValidation`: Whether to bypass SSL certificate validation
|
||||
|
||||
## SSL Certificate Validation Bypass
|
||||
|
||||
The package includes the ability to bypass SSL certificate validation in Node.js environments, which is useful for development environments or when dealing with self-signed certificates.
|
||||
|
||||
## Publishing
|
||||
|
||||
The package is configured for publishing to the Generation One Gitea repository. See [PUBLISHING.md](./PUBLISHING.md) for detailed instructions.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
The package includes examples for various frameworks and environments:
|
||||
|
||||
- Basic usage
|
||||
- React integration
|
||||
- Vue integration
|
||||
- Angular integration
|
||||
- Node.js usage
|
||||
- Browser script
|
||||
- Next.js SSE proxy
|
||||
- Express SSE proxy
|
||||
- Fastify SSE proxy
|
||||
|
||||
These examples demonstrate how to use the package in different contexts and provide a starting point for integration into your own projects.
|
132
packages/sse-client/examples/angular-order-tracking.component.ts
Normal file
132
packages/sse-client/examples/angular-order-tracking.component.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
import { getSSEConnection, setupSSECleanup, SSEClient } from 'sse-client';
|
||||
|
||||
// Set up SSE cleanup once at the application level
|
||||
setupSSECleanup();
|
||||
|
||||
// Define the order state type
|
||||
interface OrderState {
|
||||
orderId: string;
|
||||
orderStatus: number;
|
||||
preparationState: {
|
||||
utcStarted: string;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
deliveryState: {
|
||||
utcStarted: string | null;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
// Add other properties as needed
|
||||
}
|
||||
|
||||
// Define the order state changed message type
|
||||
interface OrderStateChangedMessage {
|
||||
messageType: string;
|
||||
orderId: string;
|
||||
prevState: OrderState;
|
||||
currentState: OrderState;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-order-tracker',
|
||||
template: `
|
||||
<div class="order-tracker">
|
||||
<div *ngIf="error" class="error">{{ error }}</div>
|
||||
<div *ngIf="!orderState" class="loading">Loading order status...</div>
|
||||
<div *ngIf="orderState">
|
||||
<h2>Order #{{ orderState.orderId }}</h2>
|
||||
<div class="order-status">
|
||||
Status: {{ getStatusText(orderState.orderStatus) }}
|
||||
</div>
|
||||
<!-- Render other order details -->
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.order-tracker {
|
||||
padding: 1rem;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
}
|
||||
`]
|
||||
})
|
||||
export class OrderTrackerComponent implements OnInit, OnDestroy {
|
||||
@Input() orderId!: string;
|
||||
@Input() authToken!: string;
|
||||
|
||||
orderState: OrderState | null = null;
|
||||
error: string | null = null;
|
||||
private client: SSEClient | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
// API base URL
|
||||
const apiBaseUrl = 'https://api.example.com';
|
||||
|
||||
// SSE URL
|
||||
const sseUrl = `${apiBaseUrl}/events/orders/${this.orderId}`;
|
||||
|
||||
// Get or create a connection for this order
|
||||
this.client = getSSEConnection(sseUrl, `order-${this.orderId}`, {
|
||||
headers: {
|
||||
'X-Signed-Auth': this.authToken
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
this.client.on('orderStateChanged', (event: any) => {
|
||||
try {
|
||||
const data = event.parsedData as OrderStateChangedMessage;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// Update state with new order state
|
||||
this.orderState = data.currentState;
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ${this.orderId} ended, closing SSE connection`);
|
||||
this.client?.close();
|
||||
}
|
||||
} catch (err) {
|
||||
this.error = 'Error parsing order state data';
|
||||
console.error('Error parsing order state data:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
this.client.on('error', (event: any) => {
|
||||
this.error = 'SSE connection error';
|
||||
console.error('SSE connection error:', event.detail);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
// Clean up on destroy
|
||||
if (this.client) {
|
||||
this.client.close();
|
||||
this.client = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get status text from status code
|
||||
getStatusText(status: number): string {
|
||||
switch (status) {
|
||||
case 0: return 'Created';
|
||||
case 10: return 'Accepted';
|
||||
case 20: return 'Preparing';
|
||||
case 30: return 'Ready for Pickup';
|
||||
case 40: return 'Out for Delivery';
|
||||
case 50: return 'Delivered';
|
||||
case 1111: return 'Completed';
|
||||
default: return `Unknown (${status})`;
|
||||
}
|
||||
}
|
||||
}
|
203
packages/sse-client/examples/browser-script.html
Normal file
203
packages/sse-client/examples/browser-script.html
Normal file
@ -0,0 +1,203 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SSE Client Example</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.order-tracker {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.events {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.event {
|
||||
margin-bottom: 10px;
|
||||
padding: 5px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.event-time {
|
||||
color: #888;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>SSE Client Example</h1>
|
||||
|
||||
<div class="order-tracker">
|
||||
<h2>Order Tracker</h2>
|
||||
<div id="status" class="status">Connecting...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
|
||||
<h3>Events</h3>
|
||||
<div id="events" class="events"></div>
|
||||
|
||||
<button id="connect">Connect</button>
|
||||
<button id="disconnect">Disconnect</button>
|
||||
</div>
|
||||
|
||||
<!-- Include the SSE client library -->
|
||||
<script src="https://unpkg.com/sse-client@1.0.0/dist/index.js"></script>
|
||||
|
||||
<script>
|
||||
// Get elements
|
||||
const statusEl = document.getElementById('status');
|
||||
const errorEl = document.getElementById('error');
|
||||
const eventsEl = document.getElementById('events');
|
||||
const connectBtn = document.getElementById('connect');
|
||||
const disconnectBtn = document.getElementById('disconnect');
|
||||
|
||||
// Set up SSE cleanup
|
||||
SSEClient.setupSSECleanup();
|
||||
|
||||
let client = null;
|
||||
|
||||
// Add event to the events list
|
||||
function addEvent(type, data) {
|
||||
const eventEl = document.createElement('div');
|
||||
eventEl.className = 'event';
|
||||
|
||||
const timeEl = document.createElement('div');
|
||||
timeEl.className = 'event-time';
|
||||
timeEl.textContent = new Date().toLocaleTimeString();
|
||||
|
||||
const contentEl = document.createElement('div');
|
||||
contentEl.innerHTML = `<strong>${type}:</strong> ${JSON.stringify(data)}`;
|
||||
|
||||
eventEl.appendChild(timeEl);
|
||||
eventEl.appendChild(contentEl);
|
||||
|
||||
eventsEl.appendChild(eventEl);
|
||||
eventsEl.scrollTop = eventsEl.scrollHeight;
|
||||
}
|
||||
|
||||
// Connect to SSE
|
||||
function connect() {
|
||||
// Get auth token from localStorage
|
||||
const authToken = localStorage.getItem('authToken') || '';
|
||||
|
||||
// Order ID (in a real app, this would come from your app state)
|
||||
const orderId = '123456';
|
||||
|
||||
// API base URL
|
||||
const apiBaseUrl = 'https://api.example.com';
|
||||
|
||||
// SSE URL
|
||||
const sseUrl = `${apiBaseUrl}/events/orders/${orderId}`;
|
||||
|
||||
// Update status
|
||||
statusEl.textContent = 'Connecting...';
|
||||
errorEl.style.display = 'none';
|
||||
|
||||
// Create a new SSE client
|
||||
client = new SSEClient.SSEClient(sseUrl, {
|
||||
headers: {
|
||||
'X-Signed-Auth': authToken
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
client.on('orderStateChanged', (event) => {
|
||||
try {
|
||||
const data = event.parsedData;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// Add event to the events list
|
||||
addEvent('Order State Changed', data);
|
||||
|
||||
// Update status
|
||||
statusEl.textContent = `Status: ${getStatusText(data.currentState.orderStatus)}`;
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ${orderId} ended, closing SSE connection`);
|
||||
disconnect();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing order state data:', err);
|
||||
errorEl.textContent = 'Error parsing order state data';
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection open
|
||||
client.on('open', () => {
|
||||
console.log('SSE connection opened');
|
||||
addEvent('Connection', 'Opened');
|
||||
statusEl.textContent = 'Connected';
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
client.on('error', (event) => {
|
||||
console.error('SSE connection error:', event.detail);
|
||||
addEvent('Error', event.detail);
|
||||
errorEl.textContent = 'SSE connection error';
|
||||
errorEl.style.display = 'block';
|
||||
});
|
||||
|
||||
// Connect to the SSE endpoint
|
||||
client.connect();
|
||||
}
|
||||
|
||||
// Disconnect from SSE
|
||||
function disconnect() {
|
||||
if (client) {
|
||||
client.close();
|
||||
client = null;
|
||||
|
||||
// Update status
|
||||
statusEl.textContent = 'Disconnected';
|
||||
addEvent('Connection', 'Closed');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get status text from status code
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
case 0: return 'Created';
|
||||
case 10: return 'Accepted';
|
||||
case 20: return 'Preparing';
|
||||
case 30: return 'Ready for Pickup';
|
||||
case 40: return 'Out for Delivery';
|
||||
case 50: return 'Delivered';
|
||||
case 1111: return 'Completed';
|
||||
default: return `Unknown (${status})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listeners to buttons
|
||||
connectBtn.addEventListener('click', connect);
|
||||
disconnectBtn.addEventListener('click', disconnect);
|
||||
|
||||
// Connect on page load
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
78
packages/sse-client/examples/express-sse-proxy.js
Normal file
78
packages/sse-client/examples/express-sse-proxy.js
Normal file
@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Middleware to parse JSON
|
||||
app.use(express.json());
|
||||
|
||||
// SSE proxy endpoint
|
||||
app.get('/api/proxy/sse/events/orders/:orderId', async (req, res) => {
|
||||
try {
|
||||
// Get the order ID from the request parameters
|
||||
const orderId = req.params.orderId;
|
||||
|
||||
// Get the auth header from the request
|
||||
const authHeader = req.headers['x-signed-auth'];
|
||||
|
||||
// API base URL
|
||||
const apiBaseUrl = process.env.API_ENDPOINT || 'https://api.example.com';
|
||||
|
||||
// Target URL
|
||||
const targetUrl = `${apiBaseUrl}/events/orders/${orderId}`;
|
||||
|
||||
console.log(`Proxying SSE request to: ${targetUrl}`);
|
||||
|
||||
// Create headers for the proxy request
|
||||
const headers = {
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
// Add auth header if available
|
||||
if (authHeader) {
|
||||
headers['X-Signed-Auth'] = authHeader;
|
||||
}
|
||||
|
||||
// Make the request to the target URL
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: targetUrl,
|
||||
headers,
|
||||
responseType: 'stream',
|
||||
// Skip SSL certificate validation for development
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
||||
})
|
||||
});
|
||||
|
||||
// Set headers for SSE
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
|
||||
// Copy other headers from the response
|
||||
for (const [key, value] of Object.entries(response.headers)) {
|
||||
if (key.toLowerCase() !== 'content-length') {
|
||||
res.setHeader(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Pipe the response stream to the client
|
||||
response.data.pipe(res);
|
||||
|
||||
// Handle client disconnect
|
||||
req.on('close', () => {
|
||||
console.log(`Client disconnected from SSE for order ${orderId}`);
|
||||
response.data.destroy();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in SSE proxy:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
console.log(`SSE proxy server listening at http://localhost:${port}`);
|
||||
});
|
81
packages/sse-client/examples/fastify-sse-proxy.js
Normal file
81
packages/sse-client/examples/fastify-sse-proxy.js
Normal file
@ -0,0 +1,81 @@
|
||||
const fastify = require('fastify')({ logger: true });
|
||||
const axios = require('axios');
|
||||
|
||||
// SSE proxy endpoint
|
||||
fastify.get('/api/proxy/sse/events/orders/:orderId', async (request, reply) => {
|
||||
try {
|
||||
// Get the order ID from the request parameters
|
||||
const orderId = request.params.orderId;
|
||||
|
||||
// Get the auth header from the request
|
||||
const authHeader = request.headers['x-signed-auth'];
|
||||
|
||||
// API base URL
|
||||
const apiBaseUrl = process.env.API_ENDPOINT || 'https://api.example.com';
|
||||
|
||||
// Target URL
|
||||
const targetUrl = `${apiBaseUrl}/events/orders/${orderId}`;
|
||||
|
||||
fastify.log.info(`Proxying SSE request to: ${targetUrl}`);
|
||||
|
||||
// Create headers for the proxy request
|
||||
const headers = {
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
// Add auth header if available
|
||||
if (authHeader) {
|
||||
headers['X-Signed-Auth'] = authHeader;
|
||||
}
|
||||
|
||||
// Make the request to the target URL
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: targetUrl,
|
||||
headers,
|
||||
responseType: 'stream',
|
||||
// Skip SSL certificate validation for development
|
||||
...(process.env.NODE_ENV === 'development' && {
|
||||
httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false })
|
||||
})
|
||||
});
|
||||
|
||||
// Set headers for SSE
|
||||
reply.header('Content-Type', 'text/event-stream');
|
||||
reply.header('Cache-Control', 'no-cache');
|
||||
reply.header('Connection', 'keep-alive');
|
||||
|
||||
// Copy other headers from the response
|
||||
for (const [key, value] of Object.entries(response.headers)) {
|
||||
if (key.toLowerCase() !== 'content-length') {
|
||||
reply.header(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Send the response
|
||||
reply.send(response.data);
|
||||
|
||||
// Handle client disconnect
|
||||
request.raw.on('close', () => {
|
||||
fastify.log.info(`Client disconnected from SSE for order ${orderId}`);
|
||||
response.data.destroy();
|
||||
});
|
||||
} catch (error) {
|
||||
fastify.log.error('Error in SSE proxy:', error);
|
||||
reply.status(500).send({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen({ port: 3000 });
|
||||
fastify.log.info(`SSE proxy server listening at http://localhost:3000`);
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
start();
|
93
packages/sse-client/examples/nextjs-sse-proxy.ts
Normal file
93
packages/sse-client/examples/nextjs-sse-proxy.ts
Normal file
@ -0,0 +1,93 @@
|
||||
// Example Next.js API route for proxying SSE connections
|
||||
// File: app/api/proxy/sse/[...path]/route.ts
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import https from 'https';
|
||||
|
||||
/**
|
||||
* Proxy endpoint for SSE connections
|
||||
* This endpoint forwards requests to the SSE endpoint and streams the response back to the client
|
||||
*/
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { path: string[] } }
|
||||
) {
|
||||
try {
|
||||
// Get the path from the request
|
||||
const path = params.path.join('/');
|
||||
|
||||
// Get the API base URL from environment variables
|
||||
const apiBaseUrl = process.env.API_ENDPOINT || 'https://api.example.com';
|
||||
|
||||
// Construct the target URL
|
||||
const targetUrl = `${apiBaseUrl}/events/orders/${path}`;
|
||||
|
||||
// Get the auth header from the request
|
||||
const authHeader = request.headers.get('X-Signed-Auth');
|
||||
|
||||
// Create headers for the proxy request
|
||||
const headers: HeadersInit = {
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
};
|
||||
|
||||
// Add auth header if available
|
||||
if (authHeader) {
|
||||
headers['X-Signed-Auth'] = authHeader;
|
||||
}
|
||||
|
||||
// Make the request to the target URL
|
||||
const response = await fetch(targetUrl, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
// Don't verify SSL in development or when explicitly configured
|
||||
...((process.env.NODE_ENV === 'development' || process.env.SKIP_SSL_VALIDATION === 'true') && {
|
||||
//@ts-ignore
|
||||
agent: new https.Agent({ rejectUnauthorized: false }),
|
||||
}),
|
||||
});
|
||||
|
||||
// If the response is not OK, return an error
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to connect to SSE endpoint' },
|
||||
{ status: response.status }
|
||||
);
|
||||
}
|
||||
|
||||
// Get the response body as a readable stream
|
||||
const body = response.body;
|
||||
|
||||
// If the body is null, return an error
|
||||
if (!body) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Response body is null' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new response with the body stream
|
||||
const newResponse = new NextResponse(body);
|
||||
|
||||
// Copy headers from the original response
|
||||
response.headers.forEach((value, key) => {
|
||||
newResponse.headers.set(key, value);
|
||||
});
|
||||
|
||||
// Set required headers for SSE
|
||||
newResponse.headers.set('Content-Type', 'text/event-stream');
|
||||
newResponse.headers.set('Cache-Control', 'no-cache');
|
||||
newResponse.headers.set('Connection', 'keep-alive');
|
||||
|
||||
return newResponse;
|
||||
} catch (error) {
|
||||
console.error('Error in SSE proxy:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Internal server error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable response caching
|
||||
export const dynamic = 'force-dynamic';
|
43
packages/sse-client/examples/nodejs-sse-client.js
Normal file
43
packages/sse-client/examples/nodejs-sse-client.js
Normal file
@ -0,0 +1,43 @@
|
||||
const { SSEClient } = require('sse-client');
|
||||
|
||||
// Create a new SSE client with SSL certificate validation bypass
|
||||
const client = new SSEClient('https://api.example.com/events/orders/123', {
|
||||
headers: {
|
||||
'X-Signed-Auth': 'your-auth-token'
|
||||
},
|
||||
skipSSLValidation: true, // Bypass SSL certificate validation
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
client.on('orderStateChanged', (event) => {
|
||||
try {
|
||||
const data = event.parsedData;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ended, closing SSE connection`);
|
||||
client.close();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error parsing order state data:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
client.on('error', (event) => {
|
||||
console.error('SSE connection error:', event.detail);
|
||||
});
|
||||
|
||||
// Connect to the SSE endpoint
|
||||
client.connect();
|
||||
|
||||
// Handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
console.log('Closing SSE connection');
|
||||
client.close();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
console.log('Listening for order events. Press Ctrl+C to exit.');
|
57
packages/sse-client/examples/order-tracking.ts
Normal file
57
packages/sse-client/examples/order-tracking.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { getSSEConnection, setupSSECleanup } from '../src';
|
||||
|
||||
// Set up automatic cleanup
|
||||
setupSSECleanup();
|
||||
|
||||
// Example order tracking function
|
||||
function trackOrder(orderId: string, authToken: string) {
|
||||
const apiBaseUrl = 'https://api.example.com';
|
||||
const sseUrl = `${apiBaseUrl}/events/orders/${orderId}`;
|
||||
|
||||
console.log(`Connecting to SSE endpoint: ${sseUrl}`);
|
||||
|
||||
// Get or create a connection for this order
|
||||
const client = getSSEConnection(sseUrl, `order-${orderId}`, {
|
||||
headers: {
|
||||
'X-Signed-Auth': authToken
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
client.on('orderStateChanged', (event) => {
|
||||
const data = event.parsedData;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// Update UI with new order state
|
||||
updateOrderUI(data);
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ${orderId} ended, closing SSE connection`);
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
client.on('error', (event) => {
|
||||
console.error('SSE connection error:', event.detail);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// Example function to update UI with order state
|
||||
function updateOrderUI(data: any) {
|
||||
// Implementation would depend on your UI framework
|
||||
console.log('Updating UI with order state:', data);
|
||||
}
|
||||
|
||||
// Example usage
|
||||
const orderId = '123456';
|
||||
const authToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
|
||||
|
||||
const connection = trackOrder(orderId, authToken);
|
||||
|
||||
// Later, to manually close the connection
|
||||
// connection.close();
|
114
packages/sse-client/examples/react-order-tracking.tsx
Normal file
114
packages/sse-client/examples/react-order-tracking.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { getSSEConnection, setupSSECleanup } from 'sse-client';
|
||||
|
||||
// Set up SSE cleanup once at the application level
|
||||
setupSSECleanup();
|
||||
|
||||
// Define the order state type
|
||||
interface OrderState {
|
||||
orderId: string;
|
||||
orderStatus: number;
|
||||
preparationState: {
|
||||
utcStarted: string;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
deliveryState: {
|
||||
utcStarted: string | null;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
// Add other properties as needed
|
||||
}
|
||||
|
||||
// Define the order state changed message type
|
||||
interface OrderStateChangedMessage {
|
||||
messageType: string;
|
||||
orderId: string;
|
||||
prevState: OrderState;
|
||||
currentState: OrderState;
|
||||
}
|
||||
|
||||
const OrderTracker: React.FC<{ orderId: string, authToken: string }> = ({ orderId, authToken }) => {
|
||||
const [orderState, setOrderState] = useState<OrderState | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// API base URL
|
||||
const apiBaseUrl = 'https://api.example.com';
|
||||
|
||||
// SSE URL
|
||||
const sseUrl = `${apiBaseUrl}/events/orders/${orderId}`;
|
||||
|
||||
// Get or create a connection for this order
|
||||
const client = getSSEConnection(sseUrl, `order-${orderId}`, {
|
||||
headers: {
|
||||
'X-Signed-Auth': authToken
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
client.on('orderStateChanged', (event) => {
|
||||
try {
|
||||
const data = event.parsedData as OrderStateChangedMessage;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// Update state with new order state
|
||||
setOrderState(data.currentState);
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ${orderId} ended, closing SSE connection`);
|
||||
client.close();
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Error parsing order state data');
|
||||
console.error('Error parsing order state data:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
client.on('error', (event) => {
|
||||
setError('SSE connection error');
|
||||
console.error('SSE connection error:', event.detail);
|
||||
});
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
client.close();
|
||||
};
|
||||
}, [orderId, authToken]);
|
||||
|
||||
if (error) {
|
||||
return <div className="error">{error}</div>;
|
||||
}
|
||||
|
||||
if (!orderState) {
|
||||
return <div className="loading">Loading order status...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="order-tracker">
|
||||
<h2>Order #{orderState.orderId}</h2>
|
||||
<div className="order-status">
|
||||
Status: {getStatusText(orderState.orderStatus)}
|
||||
</div>
|
||||
{/* Render other order details */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to get status text from status code
|
||||
function getStatusText(status: number): string {
|
||||
switch (status) {
|
||||
case 0: return 'Created';
|
||||
case 10: return 'Accepted';
|
||||
case 20: return 'Preparing';
|
||||
case 30: return 'Ready for Pickup';
|
||||
case 40: return 'Out for Delivery';
|
||||
case 50: return 'Delivered';
|
||||
case 1111: return 'Completed';
|
||||
default: return `Unknown (${status})`;
|
||||
}
|
||||
}
|
||||
|
||||
export default OrderTracker;
|
148
packages/sse-client/examples/vue-order-tracking.vue
Normal file
148
packages/sse-client/examples/vue-order-tracking.vue
Normal file
@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="order-tracker">
|
||||
<div v-if="error" class="error">{{ error }}</div>
|
||||
<div v-else-if="!orderState" class="loading">Loading order status...</div>
|
||||
<div v-else>
|
||||
<h2>Order #{{ orderState.orderId }}</h2>
|
||||
<div class="order-status">
|
||||
Status: {{ getStatusText(orderState.orderStatus) }}
|
||||
</div>
|
||||
<!-- Render other order details -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
|
||||
import { getSSEConnection, setupSSECleanup } from 'sse-client';
|
||||
|
||||
// Set up SSE cleanup once at the application level
|
||||
setupSSECleanup();
|
||||
|
||||
// Define the order state type
|
||||
interface OrderState {
|
||||
orderId: string;
|
||||
orderStatus: number;
|
||||
preparationState: {
|
||||
utcStarted: string;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
deliveryState: {
|
||||
utcStarted: string | null;
|
||||
utcEnded: string | null;
|
||||
};
|
||||
// Add other properties as needed
|
||||
}
|
||||
|
||||
// Define the order state changed message type
|
||||
interface OrderStateChangedMessage {
|
||||
messageType: string;
|
||||
orderId: string;
|
||||
prevState: OrderState;
|
||||
currentState: OrderState;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'OrderTracker',
|
||||
props: {
|
||||
orderId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
authToken: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const orderState = ref<OrderState | null>(null);
|
||||
const error = ref<string | null>(null);
|
||||
let client: any = null;
|
||||
|
||||
onMounted(() => {
|
||||
// API base URL
|
||||
const apiBaseUrl = 'https://api.example.com';
|
||||
|
||||
// SSE URL
|
||||
const sseUrl = `${apiBaseUrl}/events/orders/${props.orderId}`;
|
||||
|
||||
// Get or create a connection for this order
|
||||
client = getSSEConnection(sseUrl, `order-${props.orderId}`, {
|
||||
headers: {
|
||||
'X-Signed-Auth': props.authToken
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Listen for order state changes
|
||||
client.on('orderStateChanged', (event: any) => {
|
||||
try {
|
||||
const data = event.parsedData as OrderStateChangedMessage;
|
||||
console.log('Order state changed:', data);
|
||||
|
||||
// Update state with new order state
|
||||
orderState.value = data.currentState;
|
||||
|
||||
// If order is ended, close the connection
|
||||
if (data.currentState && data.currentState.orderStatus === 1111) {
|
||||
console.log(`Order ${props.orderId} ended, closing SSE connection`);
|
||||
client.close();
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = 'Error parsing order state data';
|
||||
console.error('Error parsing order state data:', err);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for connection errors
|
||||
client.on('error', (event: any) => {
|
||||
error.value = 'SSE connection error';
|
||||
console.error('SSE connection error:', event.detail);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up on unmount
|
||||
if (client) {
|
||||
client.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to get status text from status code
|
||||
const getStatusText = (status: number): string => {
|
||||
switch (status) {
|
||||
case 0: return 'Created';
|
||||
case 10: return 'Accepted';
|
||||
case 20: return 'Preparing';
|
||||
case 30: return 'Ready for Pickup';
|
||||
case 40: return 'Out for Delivery';
|
||||
case 50: return 'Delivered';
|
||||
case 1111: return 'Completed';
|
||||
default: return `Unknown (${status})`;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
orderState,
|
||||
error,
|
||||
getStatusText
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-tracker {
|
||||
padding: 1rem;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
15
packages/sse-client/jest.config.js
Normal file
15
packages/sse-client/jest.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', {
|
||||
tsconfig: 'tsconfig.json',
|
||||
}],
|
||||
},
|
||||
};
|
43
packages/sse-client/package.json
Normal file
43
packages/sse-client/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@g1/sse-client",
|
||||
"version": "0.2.0",
|
||||
"description": "A custom SSE client that supports headers and bypasses certificate issues",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"sse",
|
||||
"eventsource",
|
||||
"server-sent-events",
|
||||
"realtime"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.generation.one/GenerationOne/g1-ts-common-packages.git"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://git.generation.one/api/packages/GenerationOne/npm/"
|
||||
},
|
||||
"dependencies": {
|
||||
"event-source-polyfill": "^1.0.31"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/event-source-polyfill": "^1.0.5",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
]
|
||||
}
|
2607
packages/sse-client/pnpm-lock.yaml
generated
Normal file
2607
packages/sse-client/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
83
packages/sse-client/src/__tests__/sseClient.test.ts
Normal file
83
packages/sse-client/src/__tests__/sseClient.test.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import { SSEClient } from '../sseClient';
|
||||
|
||||
// Mock EventSourcePolyfill
|
||||
jest.mock('event-source-polyfill', () => {
|
||||
return {
|
||||
EventSourcePolyfill: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
addEventListener: jest.fn(),
|
||||
close: jest.fn(),
|
||||
onopen: null,
|
||||
onerror: null
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
describe('SSEClient', () => {
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set debug mode
|
||||
(globalThis as any).__SSE_DEBUG_ENABLED = false;
|
||||
});
|
||||
|
||||
it('should create a new SSE client', () => {
|
||||
const client = new SSEClient('https://example.com/events');
|
||||
expect(client).toBeDefined();
|
||||
});
|
||||
|
||||
it('should connect to the SSE endpoint', () => {
|
||||
const client = new SSEClient('https://example.com/events');
|
||||
client.connect();
|
||||
|
||||
// EventSourcePolyfill should be called with the URL and options
|
||||
expect(require('event-source-polyfill').EventSourcePolyfill).toHaveBeenCalledWith(
|
||||
'https://example.com/events',
|
||||
expect.objectContaining({
|
||||
headers: {},
|
||||
withCredentials: true
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should add event listeners', () => {
|
||||
const client = new SSEClient('https://example.com/events');
|
||||
const listener = jest.fn();
|
||||
|
||||
client.on('message', listener);
|
||||
client.connect();
|
||||
|
||||
// EventSourcePolyfill.addEventListener should be called with the event name and a function
|
||||
expect(require('event-source-polyfill').EventSourcePolyfill().addEventListener)
|
||||
.toHaveBeenCalledWith('message', expect.any(Function));
|
||||
});
|
||||
|
||||
it('should close the connection', () => {
|
||||
const client = new SSEClient('https://example.com/events');
|
||||
client.connect();
|
||||
client.close();
|
||||
|
||||
// EventSourcePolyfill.close should be called
|
||||
expect(require('event-source-polyfill').EventSourcePolyfill().close).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle connection errors', () => {
|
||||
const client = new SSEClient('https://example.com/events', {
|
||||
autoReconnect: false
|
||||
});
|
||||
client.connect();
|
||||
|
||||
// Get the error handler
|
||||
const errorHandler = require('event-source-polyfill').EventSourcePolyfill().onerror;
|
||||
|
||||
// Simulate an error
|
||||
if (errorHandler) {
|
||||
errorHandler(new Error('Connection error'));
|
||||
}
|
||||
|
||||
// No reconnection should be attempted (autoReconnect is false)
|
||||
expect(require('event-source-polyfill').EventSourcePolyfill).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
11
packages/sse-client/src/index.ts
Normal file
11
packages/sse-client/src/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export {
|
||||
SSEClient,
|
||||
SSEClientOptions,
|
||||
SSEConnectionManager,
|
||||
getSSEConnection,
|
||||
closeSSEConnection,
|
||||
closeAllSSEConnections,
|
||||
setupSSECleanup
|
||||
} from './sseClient';
|
||||
|
||||
export { debugLog, debugInfo, debugWarn, debugError } from './utils/debug';
|
451
packages/sse-client/src/sseClient.ts
Normal file
451
packages/sse-client/src/sseClient.ts
Normal file
@ -0,0 +1,451 @@
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { debugLog, debugError, debugWarn } from './utils/debug';
|
||||
|
||||
/**
|
||||
* Configuration options for the SSE client
|
||||
*/
|
||||
export interface SSEClientOptions {
|
||||
/**
|
||||
* Headers to include in the SSE request
|
||||
*/
|
||||
headers?: Record<string, string>;
|
||||
|
||||
/**
|
||||
* Whether to include credentials in the request
|
||||
* @default true
|
||||
*/
|
||||
withCredentials?: boolean;
|
||||
|
||||
/**
|
||||
* Timeout for heartbeat in milliseconds
|
||||
* @default 21600000 (6 hours)
|
||||
*/
|
||||
heartbeatTimeout?: number;
|
||||
|
||||
/**
|
||||
* Maximum number of retry attempts
|
||||
* @default 10
|
||||
*/
|
||||
maxRetryAttempts?: number;
|
||||
|
||||
/**
|
||||
* Initial delay for retry in milliseconds
|
||||
* @default 1000
|
||||
*/
|
||||
initialRetryDelay?: number;
|
||||
|
||||
/**
|
||||
* Maximum delay for retry in milliseconds
|
||||
* @default 30000
|
||||
*/
|
||||
maxRetryDelay?: number;
|
||||
|
||||
/**
|
||||
* Whether to automatically reconnect on error
|
||||
* @default true
|
||||
*/
|
||||
autoReconnect?: boolean;
|
||||
|
||||
/**
|
||||
* Debug mode
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to bypass SSL certificate validation
|
||||
* Only works in Node.js environments
|
||||
* @default false
|
||||
*/
|
||||
skipSSLValidation?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default options for the SSE client
|
||||
*/
|
||||
const DEFAULT_OPTIONS: Required<Omit<SSEClientOptions, 'headers'>> = {
|
||||
withCredentials: true,
|
||||
heartbeatTimeout: 21600000, // 6 hours
|
||||
maxRetryAttempts: 10,
|
||||
initialRetryDelay: 1000,
|
||||
maxRetryDelay: 30000,
|
||||
autoReconnect: true,
|
||||
debug: false,
|
||||
skipSSLValidation: false
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper: delay for ms
|
||||
*/
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom SSE client that supports headers and bypasses certificate issues
|
||||
*/
|
||||
export class SSEClient {
|
||||
private eventSource: EventSourcePolyfill | null = null;
|
||||
private url: string;
|
||||
private options: Required<SSEClientOptions>;
|
||||
private retryCount = 0;
|
||||
private closeRequested = false;
|
||||
private eventListeners: Map<string, Set<(event: any) => void>> = new Map();
|
||||
private customEventTarget: EventTarget;
|
||||
|
||||
/**
|
||||
* Creates a new SSE client
|
||||
* @param url The URL to connect to
|
||||
* @param options Configuration options
|
||||
*/
|
||||
constructor(url: string, options: SSEClientOptions = {}) {
|
||||
this.url = url;
|
||||
this.options = {
|
||||
...DEFAULT_OPTIONS,
|
||||
headers: options.headers || {},
|
||||
...options
|
||||
};
|
||||
|
||||
// Set debug mode
|
||||
if (this.options.debug) {
|
||||
(globalThis as any).__SSE_DEBUG_ENABLED = true;
|
||||
}
|
||||
|
||||
// Create a custom event target for dispatching events
|
||||
this.customEventTarget = typeof window !== 'undefined'
|
||||
? document.createElement('div')
|
||||
: new EventTarget();
|
||||
|
||||
debugLog('SSEClient: Created new client for URL:', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the SSE endpoint
|
||||
* @returns The SSE client instance for chaining
|
||||
*/
|
||||
public connect(): SSEClient {
|
||||
if (this.eventSource) {
|
||||
debugWarn('SSEClient: Already connected, closing existing connection');
|
||||
this.close();
|
||||
}
|
||||
|
||||
this.closeRequested = false;
|
||||
this.createConnection();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the SSE connection
|
||||
*/
|
||||
public close(): void {
|
||||
this.closeRequested = true;
|
||||
if (this.eventSource) {
|
||||
debugLog('SSEClient: Closing connection');
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener for SSE events
|
||||
* @param eventName The name of the event to listen for
|
||||
* @param listener The event listener function
|
||||
* @returns The SSE client instance for chaining
|
||||
*/
|
||||
public on(eventName: string, listener: (event: any) => void): SSEClient {
|
||||
if (!this.eventListeners.has(eventName)) {
|
||||
this.eventListeners.set(eventName, new Set());
|
||||
}
|
||||
|
||||
this.eventListeners.get(eventName)!.add(listener);
|
||||
|
||||
// If already connected, add the listener to the event source
|
||||
if (this.eventSource) {
|
||||
this.addEventSourceListener(eventName, listener);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an event listener
|
||||
* @param eventName The name of the event
|
||||
* @param listener The event listener function to remove
|
||||
* @returns The SSE client instance for chaining
|
||||
*/
|
||||
public off(eventName: string, listener: (event: any) => void): SSEClient {
|
||||
const listeners = this.eventListeners.get(eventName);
|
||||
if (listeners) {
|
||||
listeners.delete(listener);
|
||||
if (listeners.size === 0) {
|
||||
this.eventListeners.delete(eventName);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all event listeners for a specific event
|
||||
* @param eventName The name of the event
|
||||
* @returns The SSE client instance for chaining
|
||||
*/
|
||||
public removeAllListeners(eventName?: string): SSEClient {
|
||||
if (eventName) {
|
||||
this.eventListeners.delete(eventName);
|
||||
} else {
|
||||
this.eventListeners.clear();
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new SSE connection
|
||||
*/
|
||||
private createConnection(): void {
|
||||
debugLog('SSEClient: Creating new connection to:', this.url);
|
||||
|
||||
try {
|
||||
// Create connection options
|
||||
const connectionOptions: any = {
|
||||
headers: this.options.headers,
|
||||
withCredentials: this.options.withCredentials,
|
||||
heartbeatTimeout: this.options.heartbeatTimeout
|
||||
};
|
||||
|
||||
// Add SSL bypass if requested and in Node.js environment
|
||||
if (this.options.skipSSLValidation && typeof window === 'undefined') {
|
||||
try {
|
||||
// Only import https in Node.js environment
|
||||
const https = require('https');
|
||||
connectionOptions.httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
debugLog('SSEClient: SSL certificate validation bypassed');
|
||||
} catch (e) {
|
||||
debugWarn('SSEClient: Failed to create https agent for SSL bypass:', e);
|
||||
}
|
||||
}
|
||||
|
||||
this.eventSource = new EventSourcePolyfill(this.url, connectionOptions);
|
||||
|
||||
// Set up event handlers
|
||||
this.eventSource.onopen = this.handleOpen.bind(this);
|
||||
this.eventSource.onerror = this.handleError.bind(this);
|
||||
|
||||
// Add event listeners for all registered events
|
||||
for (const [eventName, listeners] of this.eventListeners.entries()) {
|
||||
for (const listener of listeners) {
|
||||
this.addEventSourceListener(eventName, listener);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
debugError('SSEClient: Error creating connection:', error);
|
||||
this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener to the event source
|
||||
* @param eventName The name of the event
|
||||
* @param listener The event listener function
|
||||
*/
|
||||
private addEventSourceListener(eventName: string, listener: (event: any) => void): void {
|
||||
if (!this.eventSource) return;
|
||||
|
||||
this.eventSource.addEventListener(eventName, (event: any) => {
|
||||
try {
|
||||
debugLog(`SSEClient: Received ${eventName} event:`, event);
|
||||
|
||||
// Parse the event data if it's a string
|
||||
if (typeof event.data === 'string') {
|
||||
try {
|
||||
const parsedData = JSON.parse(event.data);
|
||||
event.parsedData = parsedData;
|
||||
} catch (e) {
|
||||
// If parsing fails, just use the raw data
|
||||
debugWarn(`SSEClient: Failed to parse event data as JSON:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Call the listener with the event
|
||||
listener(event);
|
||||
} catch (error) {
|
||||
debugError(`SSEClient: Error handling ${eventName} event:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the open event
|
||||
*/
|
||||
private handleOpen(): void {
|
||||
debugLog('SSEClient: Connection opened');
|
||||
this.retryCount = 0;
|
||||
|
||||
// Dispatch the open event
|
||||
const openEvent = new Event('open');
|
||||
this.customEventTarget.dispatchEvent(openEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the error event
|
||||
* @param error The error that occurred
|
||||
*/
|
||||
private handleError(error: any): void {
|
||||
debugError('SSEClient: Connection error:', error);
|
||||
|
||||
// Dispatch the error event
|
||||
const errorEvent = new CustomEvent('error', { detail: error });
|
||||
this.customEventTarget.dispatchEvent(errorEvent);
|
||||
|
||||
// If close was requested, don't reconnect
|
||||
if (this.closeRequested) {
|
||||
debugLog('SSEClient: Not reconnecting (close requested)');
|
||||
return;
|
||||
}
|
||||
|
||||
// If auto reconnect is disabled, don't reconnect
|
||||
if (!this.options.autoReconnect) {
|
||||
debugLog('SSEClient: Not reconnecting (auto reconnect disabled)');
|
||||
return;
|
||||
}
|
||||
|
||||
// If max retry attempts reached, don't reconnect
|
||||
if (this.retryCount >= this.options.maxRetryAttempts) {
|
||||
debugError(`SSEClient: Max retry attempts (${this.options.maxRetryAttempts}) reached, not reconnecting`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reconnect with exponential backoff
|
||||
this.reconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects to the SSE endpoint with exponential backoff
|
||||
*/
|
||||
private async reconnect(): Promise<void> {
|
||||
this.retryCount++;
|
||||
|
||||
// Calculate delay with exponential backoff
|
||||
const delayMs = Math.min(
|
||||
this.options.initialRetryDelay * Math.pow(2, this.retryCount - 1),
|
||||
this.options.maxRetryDelay
|
||||
);
|
||||
|
||||
debugLog(`SSEClient: Reconnecting in ${delayMs}ms (attempt ${this.retryCount}/${this.options.maxRetryAttempts})`);
|
||||
|
||||
// Wait for the delay
|
||||
await delay(delayMs);
|
||||
|
||||
// If close was requested during the delay, don't reconnect
|
||||
if (this.closeRequested) {
|
||||
debugLog('SSEClient: Not reconnecting (close requested during delay)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new connection
|
||||
this.createConnection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Singleton pattern for SSE connections
|
||||
*/
|
||||
export class SSEConnectionManager {
|
||||
private static instance: SSEConnectionManager;
|
||||
private connections: Map<string, SSEClient> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
/**
|
||||
* Gets the singleton instance
|
||||
* @returns The SSE connection manager instance
|
||||
*/
|
||||
public static getInstance(): SSEConnectionManager {
|
||||
if (!SSEConnectionManager.instance) {
|
||||
SSEConnectionManager.instance = new SSEConnectionManager();
|
||||
}
|
||||
return SSEConnectionManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a connection for the given ID
|
||||
* @param url The URL to connect to
|
||||
* @param id A unique identifier for the connection
|
||||
* @param options Configuration options
|
||||
* @returns The SSE client instance
|
||||
*/
|
||||
public getConnection(url: string, id: string, options: SSEClientOptions = {}): SSEClient {
|
||||
if (this.connections.has(id)) {
|
||||
debugLog(`SSEConnectionManager: Reusing existing connection for ID ${id}`);
|
||||
return this.connections.get(id)!;
|
||||
}
|
||||
|
||||
debugLog(`SSEConnectionManager: Creating new connection for ID ${id}`);
|
||||
const client = new SSEClient(url, options).connect();
|
||||
this.connections.set(id, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a connection for the given ID
|
||||
* @param id The connection ID
|
||||
*/
|
||||
public closeConnection(id: string): void {
|
||||
if (this.connections.has(id)) {
|
||||
debugLog(`SSEConnectionManager: Closing connection for ID ${id}`);
|
||||
this.connections.get(id)!.close();
|
||||
this.connections.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all connections
|
||||
*/
|
||||
public closeAllConnections(): void {
|
||||
debugLog(`SSEConnectionManager: Closing all connections`);
|
||||
for (const [id, client] of this.connections.entries()) {
|
||||
client.close();
|
||||
this.connections.delete(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or creates a connection for the given ID
|
||||
* @param url The URL to connect to
|
||||
* @param id A unique identifier for the connection
|
||||
* @param options Configuration options
|
||||
* @returns The SSE client instance
|
||||
*/
|
||||
export function getSSEConnection(url: string, id: string, options: SSEClientOptions = {}): SSEClient {
|
||||
return SSEConnectionManager.getInstance().getConnection(url, id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a connection for the given ID
|
||||
* @param id The connection ID
|
||||
*/
|
||||
export function closeSSEConnection(id: string): void {
|
||||
SSEConnectionManager.getInstance().closeConnection(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all connections
|
||||
*/
|
||||
export function closeAllSSEConnections(): void {
|
||||
SSEConnectionManager.getInstance().closeAllConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners to clean up SSE connections when the page is unloaded
|
||||
*/
|
||||
export function setupSSECleanup(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Clean up all SSE connections when the page is unloaded
|
||||
window.addEventListener('beforeunload', () => {
|
||||
debugLog('SSECleanup: Cleaning up all SSE connections before page unload');
|
||||
closeAllSSEConnections();
|
||||
});
|
||||
}
|
47
packages/sse-client/src/utils/debug.ts
Normal file
47
packages/sse-client/src/utils/debug.ts
Normal file
@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Debug utility that wraps console methods and checks the global debug flag.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if debug mode is enabled
|
||||
* @returns Whether debug mode is enabled
|
||||
*/
|
||||
function isDebugEnabled(): boolean {
|
||||
return typeof (globalThis as any).__SSE_DEBUG_ENABLED === 'boolean'
|
||||
? (globalThis as any).__SSE_DEBUG_ENABLED
|
||||
: false;
|
||||
}
|
||||
|
||||
/* eslint-disable no-console */
|
||||
/**
|
||||
* Logs a debug message to the console if debug mode is enabled
|
||||
* @param args The arguments to log
|
||||
*/
|
||||
export const debugLog = (...args: any[]): void => {
|
||||
if (isDebugEnabled()) console.log('[SSE]', ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs an info message to the console if debug mode is enabled
|
||||
* @param args The arguments to log
|
||||
*/
|
||||
export const debugInfo = (...args: any[]): void => {
|
||||
if (isDebugEnabled()) console.info('[SSE]', ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs a warning message to the console if debug mode is enabled
|
||||
* @param args The arguments to log
|
||||
*/
|
||||
export const debugWarn = (...args: any[]): void => {
|
||||
if (isDebugEnabled()) console.warn('[SSE]', ...args);
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs an error message to the console if debug mode is enabled
|
||||
* @param args The arguments to log
|
||||
*/
|
||||
export const debugError = (...args: any[]): void => {
|
||||
if (isDebugEnabled()) console.error('[SSE]', ...args);
|
||||
};
|
||||
/* eslint-enable no-console */
|
18
packages/sse-client/tsconfig.json
Normal file
18
packages/sse-client/tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["dom", "es2018"],
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
}
|
2617
pnpm-lock.yaml
generated
Normal file
2617
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
pnpm-workspace.yaml
Normal file
2
pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
packages:
|
||||
- 'packages/*'
|
115
scripts/publish-package.sh
Normal file
115
scripts/publish-package.sh
Normal file
@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
# Script to publish a package to the Gitea registry using the deploy key
|
||||
|
||||
# Ensure the scripts directory exists
|
||||
mkdir -p scripts
|
||||
|
||||
# Check if pnpm is installed
|
||||
if ! command -v pnpm &> /dev/null; then
|
||||
echo "Error: pnpm is not installed or not in your PATH."
|
||||
echo "Please install pnpm using one of the following methods:"
|
||||
echo ""
|
||||
echo "Using npm:"
|
||||
echo " npm install -g pnpm"
|
||||
echo ""
|
||||
echo "Using Homebrew (macOS):"
|
||||
echo " brew install pnpm"
|
||||
echo ""
|
||||
echo "Using Windows:"
|
||||
echo " npm install -g pnpm"
|
||||
echo " or"
|
||||
echo " winget install pnpm"
|
||||
echo ""
|
||||
echo "For more installation options, visit: https://pnpm.io/installation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
KEY_FILE=".deploy-key"
|
||||
TOKEN_FILE=".gitea-token"
|
||||
GITEA_URL="https://git.generation.one"
|
||||
REGISTRY_URL="$GITEA_URL/api/packages/GenerationOne/npm/"
|
||||
|
||||
# Check if arguments are provided
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 <package-directory> [version-bump]"
|
||||
echo "Example: $0 packages/sse-client patch"
|
||||
echo "Version bump can be: patch, minor, major, or none (default: none)"
|
||||
echo "To set a specific version: $0 packages/sse-client version:0.2.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PACKAGE_DIR="$1"
|
||||
VERSION_BUMP="${2:-none}"
|
||||
|
||||
# Check if package directory exists
|
||||
if [ ! -d "$PACKAGE_DIR" ]; then
|
||||
echo "Package directory '$PACKAGE_DIR' does not exist."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if token is available
|
||||
if [ -f "$TOKEN_FILE" ]; then
|
||||
# Read the token from file
|
||||
GITEA_TOKEN=$(cat "$TOKEN_FILE")
|
||||
echo "Using token from $TOKEN_FILE"
|
||||
else
|
||||
# Check if token is in environment variable
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Gitea token not found."
|
||||
echo "Please either:"
|
||||
echo "1. Create a file named '$TOKEN_FILE' with your Gitea access token, or"
|
||||
echo "2. Set the GITEA_TOKEN environment variable"
|
||||
echo "You can generate a token at $GITEA_URL/user/settings/applications"
|
||||
echo "Make sure the token has 'packages:write' permission."
|
||||
exit 1
|
||||
else
|
||||
echo "Using token from GITEA_TOKEN environment variable"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Navigate to the package directory
|
||||
cd "$PACKAGE_DIR" || exit 1
|
||||
|
||||
# Bump version if requested
|
||||
if [ "$VERSION_BUMP" != "none" ]; then
|
||||
# Check if it's a specific version (format: version:x.y.z)
|
||||
if [[ "$VERSION_BUMP" == version:* ]]; then
|
||||
SPECIFIC_VERSION="${VERSION_BUMP#version:}"
|
||||
echo "Setting specific version ($SPECIFIC_VERSION)..."
|
||||
npm version "$SPECIFIC_VERSION" --no-git-tag-version --allow-same-version
|
||||
else
|
||||
echo "Bumping version ($VERSION_BUMP)..."
|
||||
npm version "$VERSION_BUMP" --no-git-tag-version
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine which package manager to use (prefer pnpm, fallback to npm)
|
||||
PKG_MANAGER="npm"
|
||||
if command -v pnpm &> /dev/null; then
|
||||
PKG_MANAGER="pnpm"
|
||||
echo "Using pnpm as package manager"
|
||||
else
|
||||
echo "pnpm not found, falling back to npm"
|
||||
fi
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies..."
|
||||
$PKG_MANAGER install --no-frozen-lockfile
|
||||
|
||||
# Build the package
|
||||
echo "Building package..."
|
||||
$PKG_MANAGER run build
|
||||
|
||||
# Set up .npmrc for publishing
|
||||
echo "Setting up .npmrc for publishing..."
|
||||
echo "@g1:registry=$REGISTRY_URL" > .npmrc
|
||||
echo "//git.generation.one/api/packages/GenerationOne/npm/:_authToken=$GITEA_TOKEN" >> .npmrc
|
||||
echo "legacy-peer-deps=true" >> .npmrc
|
||||
echo "resolution-mode=highest" >> .npmrc
|
||||
|
||||
# Publish the package
|
||||
echo "Publishing package..."
|
||||
$PKG_MANAGER publish --no-git-checks
|
||||
|
||||
echo "Package published successfully!"
|
54
scripts/setup-deploy-key.sh
Normal file
54
scripts/setup-deploy-key.sh
Normal file
@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
# Script to generate and configure a Gitea deploy key for this repository
|
||||
|
||||
# Ensure the scripts directory exists
|
||||
mkdir -p scripts
|
||||
|
||||
# Configuration
|
||||
KEY_NAME="g1-ts-common-packages-deploy-key"
|
||||
KEY_FILE=".deploy-key"
|
||||
KEY_FILE_PUB=".deploy-key.pub"
|
||||
TOKEN_FILE=".gitea-token"
|
||||
GITEA_URL="https://git.generation.one"
|
||||
REPO_OWNER="GenerationOne"
|
||||
REPO_NAME="g1-ts-common-packages"
|
||||
|
||||
# Check if keys already exist
|
||||
if [ -f "$KEY_FILE" ] && [ -f "$KEY_FILE_PUB" ]; then
|
||||
echo "Deploy keys already exist. Using existing keys."
|
||||
else
|
||||
echo "Generating new SSH key pair for deployment..."
|
||||
ssh-keygen -t ed25519 -f "$KEY_FILE" -N "" -C "$KEY_NAME"
|
||||
echo "SSH key pair generated."
|
||||
fi
|
||||
|
||||
# Read the public key
|
||||
PUBLIC_KEY=$(cat "$KEY_FILE_PUB")
|
||||
|
||||
# Check if token file exists
|
||||
if [ ! -f "$TOKEN_FILE" ]; then
|
||||
echo "Gitea token file not found."
|
||||
echo "Please create a file named '$TOKEN_FILE' with your Gitea access token."
|
||||
echo "You can generate a token at $GITEA_URL/user/settings/applications"
|
||||
echo "Make sure the token has 'write:repository' permission."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read the token
|
||||
GITEA_TOKEN=$(cat "$TOKEN_FILE")
|
||||
|
||||
# Add the deploy key to the repository
|
||||
echo "Adding deploy key to repository..."
|
||||
curl -X POST \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"title\":\"$KEY_NAME\", \"key\":\"$PUBLIC_KEY\", \"read_only\":false}" \
|
||||
"$GITEA_URL/api/v1/repos/$REPO_OWNER/$REPO_NAME/keys"
|
||||
|
||||
echo ""
|
||||
echo "Deploy key setup complete."
|
||||
echo "The private key is stored in $KEY_FILE"
|
||||
echo "The public key is stored in $KEY_FILE_PUB"
|
||||
echo ""
|
||||
echo "To use this key in CI/CD pipelines, add the private key as a secret."
|
||||
echo "For GitHub Actions, you can add it as a repository secret named 'GITEA_DEPLOY_KEY'."
|
Loading…
x
Reference in New Issue
Block a user