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