diff --git a/benchmarks/run_benchmark.sh b/benchmarks/run_benchmark.sh new file mode 100755 index 0000000000..e300b0df39 --- /dev/null +++ b/benchmarks/run_benchmark.sh @@ -0,0 +1,186 @@ +#!/bin/bash + +# Benchmark script for front service: Node-only vs Nginx+Node +# Usage: ./run_benchmark.sh [--build] [--duration 15s] [--connections 100] +# +# --build Rebuild Docker images before benchmark +# --duration Test duration per scenario (default: 15s) +# --connections Concurrent connections (default: 100) + +set -e + +BENCHMARK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FRONT_DIR="$BENCHMARK_DIR/../pods/front" +RESULTS_DIR="$BENCHMARK_DIR/results" +TIMESTAMP=$(date +%Y%m%d_%H%M%S) +RESULTS_FILE="$RESULTS_DIR/benchmark_${TIMESTAMP}.md" +BENCHMARK_BIN="$BENCHMARK_DIR/front-benchmark/front-benchmark" + +BUILD=false +DURATION="15s" +CONNECTIONS=100 + +NODE_IMAGE="front-bench-node" +NGINX_IMAGE="front-bench-nginx" +NODE_CONTAINER="front-bench-node-run" +NGINX_CONTAINER="front-bench-nginx-run" +NODE_PORT=8091 +NGINX_PORT=8092 + +# Common environment variables (from dev/docker-compose.yaml) +ENV_ARGS=( + -e SERVER_SECRET=secret + -e ACCOUNTS_URL=http://huly.local:3000 + -e UPLOAD_URL=/files + -e "FILES_URL=http://huly.local:4030/blob/:workspace/:blobId/:filename" + -e MODEL_VERSION=test + -e REKONI_URL=http://huly.local:4004 + -e GMAIL_URL=http://huly.local:8093 + -e CALENDAR_URL=http://huly.local:8095 + -e TELEGRAM_URL=http://huly.local:8086 + -e "STORAGE_CONFIG=minio|localhost?accessKey=minioadmin&secretKey=minioadmin" + -e BRANDING_URL=http://huly.local:8087/branding.json + -e DATALAKE_URL=http://huly.local:4030 + -e LINK_PREVIEW_URL=http://huly.local:4042 +) + +# Parse arguments +while [[ $# -gt 0 ]]; do + case $1 in + --build) BUILD=true; shift ;; + --duration) DURATION="$2"; shift 2 ;; + --connections) CONNECTIONS="$2"; shift 2 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +mkdir -p "$RESULTS_DIR" + +echo "=== Front Service Benchmark ===" +echo "Timestamp: $(date)" +echo "Duration: $DURATION per scenario" +echo "Connections: $CONNECTIONS" +echo "Results: $RESULTS_FILE" +echo "" + +# Build Go benchmark tool (always rebuild to pick up code changes) +echo "Building benchmark tool..." +cd "$BENCHMARK_DIR/front-benchmark" +go build -o front-benchmark main.go + +# Build Docker images if requested +if [ "$BUILD" = true ]; then + echo "Building bundle..." + cd "$FRONT_DIR" + rushx bundle + + echo "Building Docker images..." + docker build --build-arg BUILD_ID=bench-node -t "$NODE_IMAGE" -f Dockerfile . & + docker build --build-arg BUILD_ID=bench-nginx -t "$NGINX_IMAGE" -f Dockerfile.nginx . & + wait + echo "Docker images built." +fi + +# Cleanup old containers +docker rm -f "$NODE_CONTAINER" "$NGINX_CONTAINER" 2>/dev/null || true + +# Start containers +echo "Starting Node-only container on port $NODE_PORT..." +docker run -d --name "$NODE_CONTAINER" -p "$NODE_PORT:8080" \ + -e SERVER_PORT=8080 "${ENV_ARGS[@]}" "$NODE_IMAGE" >/dev/null + +echo "Starting Nginx+Node container on port $NGINX_PORT..." +docker run -d --name "$NGINX_CONTAINER" -p "$NGINX_PORT:8080" \ + -e SERVER_PORT=3000 "${ENV_ARGS[@]}" "$NGINX_IMAGE" >/dev/null + +# Wait for containers to be ready +echo "Waiting for containers to start..." +for i in $(seq 1 30); do + NODE_OK=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$NODE_PORT/config.json" 2>/dev/null || echo "000") + NGINX_OK=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$NGINX_PORT/config.json" 2>/dev/null || echo "000") + if [ "$NODE_OK" = "200" ] && [ "$NGINX_OK" = "200" ]; then + echo "Both containers ready." + break + fi + sleep 1 +done + +# Extract file list +echo "Extracting file list..." +docker exec "$NODE_CONTAINER" find /app/dist -type f | sed 's|/app/dist/||' > /tmp/bench_files.txt +FILE_COUNT=$(wc -l < /tmp/bench_files.txt | tr -d ' ') +echo "Found $FILE_COUNT files." + +# Write results header +cat > "$RESULTS_FILE" << EOF +# Front Service Benchmark Results +**Date:** $(date) +**Duration:** $DURATION per scenario | **Connections:** $CONNECTIONS | **Files:** $FILE_COUNT + +--- + +EOF + +# Run a single benchmark scenario +run_scenario() { + local label="$1" + local url="$2" + local container="$3" + local extra_args="$4" + + echo "" + echo ">>> $label" + echo "## $label" >> "$RESULTS_FILE" + echo '```' >> "$RESULTS_FILE" + "$BENCHMARK_BIN" -url="$url" -c="$CONNECTIONS" -d="$DURATION" \ + -monitor-memory -container="$container" $extra_args 2>&1 | tee -a "$RESULTS_FILE" + echo '```' >> "$RESULTS_FILE" + echo "" >> "$RESULTS_FILE" + + # Cooldown + sleep 2 +} + +echo "" +echo "==========================================" +echo " Running benchmarks" +echo "==========================================" + +# --- Node-only --- +run_scenario "Node: config.json" \ + "http://localhost:$NODE_PORT/config.json" "$NODE_CONTAINER" "-exact" + +run_scenario "Node: index.html (SPA)" \ + "http://localhost:$NODE_PORT/" "$NODE_CONTAINER" "-exact" + +run_scenario "Node: random files" \ + "http://localhost:$NODE_PORT" "$NODE_CONTAINER" "-files=/tmp/bench_files.txt" + +run_scenario "Node: mixed (files + config.json)" \ + "http://localhost:$NODE_PORT" "$NODE_CONTAINER" \ + "-files=/tmp/bench_files.txt -mixed=/config.json -mixed-conns=10" + +# --- Nginx+Node --- +run_scenario "Nginx: config.json" \ + "http://localhost:$NGINX_PORT/config.json" "$NGINX_CONTAINER" "-exact" + +run_scenario "Nginx: index.html (SPA)" \ + "http://localhost:$NGINX_PORT/" "$NGINX_CONTAINER" "-exact" + +run_scenario "Nginx: random files" \ + "http://localhost:$NGINX_PORT" "$NGINX_CONTAINER" "-files=/tmp/bench_files.txt" + +run_scenario "Nginx: mixed (files + config.json)" \ + "http://localhost:$NGINX_PORT" "$NGINX_CONTAINER" \ + "-files=/tmp/bench_files.txt -mixed=/config.json -mixed-conns=10" + +# Print summary +echo "" +echo "==========================================" +echo " Benchmark complete!" +echo " Results: $RESULTS_FILE" +echo "==========================================" + +# Cleanup containers +echo "Stopping containers..." +docker rm -f "$NODE_CONTAINER" "$NGINX_CONTAINER" 2>/dev/null || true diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b2ef767fd7..4bdcdb4f15 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -420,6 +420,9 @@ importers: '@hcengineering/products-resources': specifier: workspace:^0.7.0 version: link:../plugins/products-resources + '@hcengineering/pulse': + specifier: workspace:^0.7.0 + version: link:../plugins/pulse '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../plugins/questions @@ -1474,6 +1477,9 @@ importers: '@hcengineering/products-resources': specifier: workspace:^0.7.0 version: link:../../plugins/products-resources + '@hcengineering/pulse': + specifier: workspace:^0.7.0 + version: link:../../plugins/pulse '@hcengineering/questions': specifier: workspace:^0.7.0 version: link:../../plugins/questions @@ -6061,6 +6067,9 @@ importers: '@hcengineering/model-products': specifier: workspace:^0.7.0 version: link:../products + '@hcengineering/model-pulse': + specifier: workspace:^0.7.0 + version: link:../pulse '@hcengineering/model-questions': specifier: workspace:^0.7.0 version: link:../questions @@ -9652,6 +9661,70 @@ importers: specifier: ^5.9.3 version: 5.9.3 + ../../models/pulse: + dependencies: + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../../plugins/contact + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../foundations/core/packages/core + '@hcengineering/model': + specifier: workspace:^0.7.17 + version: link:../../foundations/core/packages/model + '@hcengineering/model-core': + specifier: workspace:^0.7.0 + version: link:../core + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/platform + '@hcengineering/pulse': + specifier: workspace:^0.7.0 + version: link:../../plugins/pulse + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@types/node': + specifier: ^22.18.1 + version: 22.19.0 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ../../models/questions: dependencies: '@hcengineering/contact': @@ -14124,67 +14197,6 @@ importers: specifier: ^5.9.3 version: 5.9.3 - ../../packages/hulypulse-client: - dependencies: - '@hcengineering/core': - specifier: workspace:^0.7.26 - version: link:../../foundations/core/packages/core - '@hcengineering/platform': - specifier: workspace:^0.7.20 - version: link:../../foundations/core/packages/platform - devDependencies: - '@hcengineering/platform-rig': - specifier: workspace:^0.7.21 - version: link:../../foundations/utils/packages/platform-rig - '@types/jest': - specifier: ^29.5.5 - version: 29.5.14 - '@types/node': - specifier: ^22.18.1 - version: 22.19.0 - '@typescript-eslint/eslint-plugin': - specifier: ^6.21.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) - '@typescript-eslint/parser': - specifier: ^6.21.0 - version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) - cross-env: - specifier: ~7.0.3 - version: 7.0.3 - esbuild: - specifier: ^0.25.10 - version: 0.25.12 - eslint: - specifier: ^8.54.0 - version: 8.57.1 - eslint-config-standard-with-typescript: - specifier: ^40.0.0 - version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) - eslint-plugin-import: - specifier: ^2.26.0 - version: 2.32.0(eslint@8.57.1) - eslint-plugin-n: - specifier: ^15.4.0 - version: 15.7.0(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.1.1 - version: 6.6.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) - jest-fetch-mock: - specifier: ^3.0.3 - version: 3.0.3(encoding@0.1.13) - prettier: - specifier: ^3.6.2 - version: 3.6.2 - ts-jest: - specifier: ^29.1.1 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(esbuild@0.25.12)(jest-util@30.2.0)(jest@29.7.0(@types/node@22.19.0))(typescript@5.9.3) - typescript: - specifier: ^5.9.3 - version: 5.9.3 - ../../packages/importer: dependencies: '@hcengineering/attachment': @@ -14721,9 +14733,6 @@ importers: '@hcengineering/hulylake-client': specifier: workspace:^0.7.18 version: link:../../foundations/core/packages/hulylake-client - '@hcengineering/hulypulse-client': - specifier: workspace:^0.7.0 - version: link:../hulypulse-client '@hcengineering/notification': specifier: workspace:^0.7.0 version: link:../../plugins/notification @@ -23177,9 +23186,6 @@ importers: '@hcengineering/emoji-resources': specifier: workspace:^0.7.0 version: link:../emoji-resources - '@hcengineering/hulypulse-client': - specifier: workspace:^0.7.0 - version: link:../../packages/hulypulse-client '@hcengineering/login': specifier: workspace:^0.7.0 version: link:../login @@ -24312,9 +24318,6 @@ importers: '@hcengineering/core': specifier: workspace:^0.7.26 version: link:../../foundations/core/packages/core - '@hcengineering/hulypulse-client': - specifier: workspace:^0.7.0 - version: link:../../packages/hulypulse-client '@hcengineering/platform': specifier: workspace:^0.7.20 version: link:../../foundations/core/packages/platform @@ -24324,6 +24327,9 @@ importers: '@hcengineering/presentation': specifier: workspace:^0.7.0 version: link:../../packages/presentation + '@hcengineering/pulse': + specifier: workspace:^0.7.0 + version: link:../pulse '@hcengineering/theme': specifier: workspace:^0.7.0 version: link:../../packages/theme @@ -25109,6 +25115,58 @@ importers: specifier: ^5.9.3 version: 5.9.3 + ../../plugins/pulse: + dependencies: + '@hcengineering/contact': + specifier: workspace:^0.7.0 + version: link:../contact + '@hcengineering/core': + specifier: workspace:^0.7.26 + version: link:../../foundations/core/packages/core + '@hcengineering/platform': + specifier: workspace:^0.7.20 + version: link:../../foundations/core/packages/platform + devDependencies: + '@hcengineering/platform-rig': + specifier: workspace:^0.7.21 + version: link:../../foundations/utils/packages/platform-rig + '@types/jest': + specifier: ^29.5.5 + version: 29.5.14 + '@typescript-eslint/eslint-plugin': + specifier: ^6.21.0 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^6.21.0 + version: 6.21.0(eslint@8.57.1)(typescript@5.9.3) + eslint: + specifier: ^8.54.0 + version: 8.57.1 + eslint-config-standard-with-typescript: + specifier: ^40.0.0 + version: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3))(eslint-plugin-import@2.32.0(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)(typescript@5.9.3) + eslint-plugin-import: + specifier: ^2.26.0 + version: 2.32.0(eslint@8.57.1) + eslint-plugin-n: + specifier: ^15.4.0 + version: 15.7.0(eslint@8.57.1) + eslint-plugin-promise: + specifier: ^6.1.1 + version: 6.6.0(eslint@8.57.1) + jest: + specifier: ^29.7.0 + version: 29.7.0(@types/node@22.19.0)(ts-node@10.9.2(@types/node@22.19.0)(typescript@5.9.3)) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + ts-jest: + specifier: ^29.1.1 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@30.2.0)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@30.2.0)(jest@29.7.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + ../../plugins/questions: dependencies: '@hcengineering/contact': diff --git a/desktop/package.json b/desktop/package.json index 50f0100a81..63e927a3da 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -201,6 +201,7 @@ "@hcengineering/global-profile-resources": "workspace:^0.7.0", "@hcengineering/presence": "workspace:^0.7.0", "@hcengineering/presence-resources": "workspace:^0.7.0", + "@hcengineering/pulse": "workspace:^0.7.0", "@hcengineering/media": "workspace:^0.7.0", "@hcengineering/media-assets": "workspace:^0.7.0", "@hcengineering/media-resources": "workspace:^0.7.0", diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index 019125dc80..3250b8607c 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -52,6 +52,7 @@ import login, { loginId } from '@hcengineering/login' import notification, { notificationId } from '@hcengineering/notification' import onboard, { onboardId } from '@hcengineering/onboard' import presence, { presenceId } from '@hcengineering/presence' +import { pulseId } from '@hcengineering/pulse' import { processId } from '@hcengineering/process' import { productsId } from '@hcengineering/products' import { questionsId } from '@hcengineering/questions' @@ -359,7 +360,6 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise it.trim()).filter(it => it.length > 0) setMetadata(presentation.metadata.DisabledFeatures, new Set(disabledFeatures)) @@ -520,7 +520,7 @@ export async function configurePlatform (onWorkbenchConnect?: () => Promise dedicated pulse protocol + +- No second WebSocket — reuses transactor session. +- `createQuery` already gives live set subscriptions. +- `client.createDoc`/`updateDoc`/`removeDoc` — standard Tx pipeline, no custom middleware. +- `TransientMiddleware.tx()` refreshes TTL on **any** CUD — update = heartbeat, free. +- `SpaceSecurityMiddleware` filters visibility by `doc.space` — private-doc visibility handled automatically when writer sets `space = target doc's space`. +- TTL ticker cleans expired docs and broadcasts `TxRemoveDoc` — subscribers see removals without extra code. + +## Key design points + +- **Deterministic `_id`**: `presence:${objectId}:${personId}` / `typing:${objectId}:${socialId}` so repeated writes update a single doc instead of creating duplicates. +- **Space = target doc's space** (not `core.space.Workspace`). `PresenceContext.svelte` passes `object.space`; `MessageInput.svelte` / `ChatMessageInput.svelte` pass `card.space` / `object.space`. Prevents leaking presence/typing info to users who cannot see the private target doc. +- **TTL class-level only**: `DocumentPresence` = 10s, `TypingIndicator` = 3s. No per-doc TTL — scope didn't need it. +- **`TypingIndicator.objectId` is a plain string**, not `Ref`. It accepts composite keys like `peer:${peerId}` used by direct-message input. +- **`TransientTTL` mixin is class-level**: applied via `builder.mixin(classRef, core.class.Class, core.mixin.TransientTTL, { ttl })` in the model package, mirroring `@Mixin(core.mixin.TransientTTL, core.class.Class)` decorator pattern in `models/core/src/core.ts:451`. + +## Package structure + +- `plugins/pulse` — interfaces only (`DocumentPresence`, `TypingIndicator`, plugin id, class refs). Deps: `@hcengineering/core`, `@hcengineering/contact`, `@hcengineering/platform`. +- `models/pulse` — `@Model` classes + `createModel` that runs `createModel(...)` and applies `TransientTTL` mixins. Registered in `models/all/src/index.ts` and `rush.json`. +- `plugins/presence-resources/src/{presence,typing}.ts` — rewritten on top of `createQuery(true)` + `getClient()`. Svelte-action pattern preserved (`presence`, `typing` still return `{update, destroy}`). + +## Cleanup done in same PR + +- Deleted `packages/hulypulse-client`, `foundations/hulypulse`. +- Removed `packages/presentation/src/pulse.ts` + `PulseUrl` metadata. +- Dropped `@hcengineering/hulypulse-client` dep from `presentation`, `presence-resources`, `love-resources`. +- Removed `PULSE_URL` / `pulseUrl` from `dev/prod/src/platform.ts`, `pods/front/src/__start.ts`, `server/front/src/{index,starter}.ts`, `desktop/src/ui/{types,platform}.ts`, `benchmarks/run_benchmark.sh`, `dev/prod/public/config{,-dev}.json`. +- Dropped `hulypulse` service from `dev/docker-compose.yaml` and `pods/external/services.d/hulypulse.service`. +- Removed `build-hulypulse` job from `.github/workflows/main.yml`. +- Removed `hulypulse` subtree pull from `scripts/takeUpstream.sh`. +- Removed `hulypulse-client` entry from `rush.json`; added `@hcengineering/pulse`, `@hcengineering/model-pulse`. + +## Client-side plugin registration gotcha + +`pulse` has no `-resources` package (only interfaces + model), so `addLocation(pulseId, ...)` is not called. Without that, `returnUITxes` in `foundations/core/packages/client-resources/src/index.ts:189` treats pulse as not-allowed and strips every pulse tx — clients log `exclude plugin pulse:N`, classes have no `domain`/`ancestors`, `createDoc('pulse:class:TypingIndicator', ...)` throws silently, `createQuery` never fires. + +Fix: include `pulseId` in `ExtraPlugins` metadata: +- `dev/prod/src/platform.ts`: `setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin, pulseId as Plugin])` +- `desktop/src/ui/platform.ts`: same +- Add `@hcengineering/pulse` dep to both packages so `pulseId` can be imported. + +Symptom when missing: console shows `domain not found: pulse:class:DocumentPresence` / `ancestors not found: pulse:class:TypingIndicator` as `pageerror`, typing indicator span stays empty. Debug by injecting a probe calling `client.findAll('pulse:class:TypingIndicator', {})`. + +## Build / test workflow for pulse + +1. Rebuild front image after changing `dev/prod` config: `rush fast-build:docker-build --to @hcengineering/pod-front` (~8s incremental). Model changes also need `--to @hcengineering/pod-server`. Full rebuild: `rush fast-build:docker-build` (~3.5min, 42 images). +2. Restart sanity env: `cd tests && ./prepare-pg.sh` — includes `--remove-orphans` on both `down` and `up` to clean stale services. +3. Run Playwright pulse spec without auto-opening HTML report: + ``` + cd tests/sanity + PLAYWRIGHT_HTML_OPEN=never LOCAL_URL=http://localhost:3003/ DEV_URL= \ + ./node_modules/.bin/playwright test -c ./tests/playwright.config.ts pulse.spec.ts --reporter=list + ``` + `PLAYWRIGHT_HTML_OPEN=never` + `--reporter=list` stops HTML reporter from holding :9323 and opening a browser tab. +4. Spec: `tests/sanity/tests/chat/pulse.spec.ts` — two browser contexts via `getSecondPageByInvite`. Stable selectors added to source: + - `span[data-id="channel-typing-info"]` in `ChannelTypingInfo.svelte` — typing indicator + - `[data-id="document-presence"]` in `PresenceAvatars.svelte` — DocumentPresence avatars container + Two tests: (1) typing indicator appears on page2 while user1 types, clears after send; (2) DocumentPresence avatar appears on page1 when user2 opens the same channel, disappears after TTL when user2 navigates away. + +## Future ideas (see `docs/new-pulse.md`) + +A `UserActivity` transient class (active/away/busy/in-meeting) built on the same substrate — heartbeat from user input + love room participation, TTL ~30s, auto-away via middleware expiry. Not in this PR. diff --git a/docs/new-pulse.md b/docs/new-pulse.md new file mode 100644 index 0000000000..92c282167c --- /dev/null +++ b/docs/new-pulse.md @@ -0,0 +1,172 @@ +# New Pulse — Transient Docs Plan + +Replace `foundations/hulypulse` (Rust WS service) and `packages/hulypulse-client` with transient docs inside the existing transactor. Use existing `DOMAIN_TRANSIENT` + `TransientTTL` mixin + `createQuery` live queries. No new server code, no new WS connection. + +## Motivation + +- Dedicated pulse WS doubles connections per client. +- Transactor already has `DOMAIN_TRANSIENT` (`InMemory` adapter) wired via `server-pipeline/src/pipeline.ts`. +- `TransientMiddleware` already runs a 1s ticker, cleans expired docs, broadcasts `TxRemoveDoc`, and refreshes TTL on any CUD (heartbeat via update = free). +- `createQuery` + `client.createDoc/updateDoc/removeDoc` already give live sets + writes. +- `SpaceSecurityMiddleware` already filters by `doc.space` — private doc visibility handled automatically if we set space = target doc's space. + +## Real Usages Found + +Only two patterns in the whole codebase: + +1. **Document presence** — who is currently viewing a document. + - `plugins/presence-resources/src/presence.ts`: `subscribePresence`, `updatePresence`, `deletePresence`. + - `PresenceContext.svelte` writes every `presenceUpdateSeconds=2`, TTL `presenceTtlSeconds=5`. + - `PresenceAvatars.svelte` reads. +2. **Typing indicator** — who is typing in a chat/card. + - `plugins/presence-resources/src/typing.ts`: `subscribeTyping`, `setTyping`, `clearTyping`. + - `MessageInput.svelte` / `ChatMessageInput.svelte` write with 2s TTL. + - `objectId` is a composite string (e.g. `peer:${card.peerId}`), not necessarily a real `Ref`. + +Everything else in hulypulse protocol is unused. + +## Design + +### New package: `plugins/pulse` (interfaces only) + +```ts +// src/index.ts +export interface DocumentPresence extends Doc { + objectId: Ref + objectClass: Ref> + person: Ref + lastActive: Timestamp +} + +export interface TypingIndicator extends Doc { + objectId: string // composite key allowed (peer:xxx) + socialId: PersonId + status?: IntlString +} +``` + +`src/plugin.ts`: +```ts +export const pulseId = 'pulse' as Plugin +export default plugin(pulseId, { + class: { + DocumentPresence: '' as Ref>, + TypingIndicator: '' as Ref> + } +}) +``` + +### New package: `models/pulse` + +```ts +@Model(pulse.class.DocumentPresence, core.class.Doc, DOMAIN_TRANSIENT) +export class TDocumentPresence extends TDoc implements DocumentPresence { ... } + +@Mixin(core.mixin.TransientTTL, pulse.class.DocumentPresence) +export class TDocumentPresenceTTL extends TDocumentPresence { ttl = 10 } + +@Model(pulse.class.TypingIndicator, core.class.Doc, DOMAIN_TRANSIENT) +export class TTypingIndicator extends TDoc implements TypingIndicator { ... } + +@Mixin(core.mixin.TransientTTL, pulse.class.TypingIndicator) +export class TTypingIndicatorTTL extends TTypingIndicator { ttl = 3 } +``` + +Class-level TTL only. No per-doc TTL (simpler, middleware already supports the use case). + +Register in `rush.json` and `models/all/src/index.ts`. + +### Space = target doc's space + +- `PresenceContext.svelte` has `export let object: Doc` → pass `object.space` to writer. +- `MessageInput.svelte` has `export let card: Card` → pass `card.space` to writer. + +`SpaceSecurityMiddleware` then filters visibility for private docs automatically. No custom permission logic needed. + +### Rewrite client helpers + +- `presence.ts` / `typing.ts`: drop `HulypulseClient`, use `createQuery` + `client.createDoc/updateDoc/removeDoc`. Heartbeat = `updateDoc` (refreshes TTL in middleware). +- Key is now `_id` = deterministic hash of `(objectId, personId)` so repeated writes map to same doc. + +## Removal Scope (same PR) + +- `foundations/hulypulse/` (Rust service) +- `packages/hulypulse-client/` +- `packages/presentation/src/pulse.ts` + `PulseUrl` metadata +- Dependency drops: `presentation`, `presence-resources`, `love-resources` +- `rush.json` entries +- `docker-compose` pulse service +- `PULSE_URL` env in server/front configs +- `config-dev.json` entry +- CI `.github/workflows/main.yml` pulse job +- `pods/external` hulypulse.service + +## Tasks + +1. Create `plugins/pulse` interface package. +2. Create `models/pulse` + register. +3. Rewrite `presence-resources/src/presence.ts`. +4. Rewrite `presence-resources/src/typing.ts`. +5. Update callsites: `PresenceContext.svelte`, `PresenceAvatars.svelte`, `WorkbenchExtension.svelte`, `MessageInput.svelte`, `ChatMessageInput.svelte`. +6. Remove entire hulypulse stack. +7. Run `diagnostics`, verify build. +8. Memory note in `docs/memory/pulse.md`. + +Single PR. `rushx format` is user's responsibility. + +--- + +## Future Ideas (not in this PR) + +### User activity / presence status (away/active/busy) + +Natural extension of the same transient doc pattern. Goal: aggregate user activity signals so UI can show **active / away / in-meeting / busy** next to avatars globally, not per-document. + +**Signals already available (no extra plumbing):** +- `DocumentPresence` writes → user is viewing some doc. +- `TypingIndicator` writes → user is typing somewhere. +- `love` room participation (`ParticipantInfo`) → user is in a meeting/office room. +- LiveKit session state via love middleware. + +**Proposed transient class:** +```ts +export interface UserActivity extends Doc { + person: Ref + status: 'active' | 'away' | 'busy' | 'in-meeting' + lastInputAt: Timestamp // last keypress/click/presence write + currentRoom?: Ref // if in love room + currentDoc?: Ref // if viewing doc +} +``` +- TTL ~30s, class-level. +- `space: core.space.Workspace` (everyone sees everyone — same as existing `UserStatus`). +- Key: `_id` = hash(personId) → single doc per user. + +**Client writer (single place, e.g. `workbench-resources`):** +- Heartbeat every ~15s while tab focused + on user input events (throttled). +- Derive `status`: + - `in-meeting` if love reports active room participation. + - `busy` if user manually set (future UI). + - `away` if no heartbeat for >60s OR tab hidden for >5min. + - `active` otherwise. +- Drop heartbeat when tab hidden → middleware TTL cleanup flips to `away` automatically after timeout. + +**Why transient + TTL fits perfectly:** +- No heartbeat for N seconds → middleware auto-removes → subscribers see `TxRemoveDoc` → UI shows offline/away. No manual cleanup. +- Any input → single `updateDoc` refreshes TTL → no ticker needed on client. + +**Integration with existing `UserStatus`:** +- `UserStatus` (persistent) keeps online/offline for notifications/account-level logic. +- `UserActivity` (transient) is the richer, short-lived view (away/busy/in-meeting/current doc). +- Could eventually replace `UserStatus` entirely if persistence is not needed. + +**Potential improvements to explore:** +- **Idle detection** via `document.visibilityState` + mouse/keyboard listeners in workbench (debounced to 1 write / 10s). +- **Per-device presence**: multiple tabs/devices → multiple `UserActivity` docs keyed by `(person, sessionId)`. Aggregate on read side (`createQuery` groups by person). +- **Do Not Disturb**: explicit status mutation from UI, overrides auto status until expiry. +- **"Last seen" fallback**: when `UserActivity` expires, persist `lastSeenAt` to `UserStatus` so offline users still show a timestamp. +- **Cross-workspace presence** (if needed for org-wide): separate account-level pulse, out of scope for transactor-scoped transient docs. +- **Typing → activity**: typing writes could auto-refresh `UserActivity` instead of a separate heartbeat. +- **Meeting status broadcast**: love middleware could directly write/update `UserActivity` with `in-meeting` and `currentRoom` as source of truth, removing duplicate logic on client. + +All of the above are additive: the pulse-replacement PR does not block them and the transient-doc substrate is already in place. diff --git a/foundations/hulypulse/.github/workflows/build.yml b/foundations/hulypulse/.github/workflows/build.yml deleted file mode 100644 index 387987e13c..0000000000 --- a/foundations/hulypulse/.github/workflows/build.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Hulypulse - -on: - workflow_dispatch: - push: - tags: - - 'v*.*.*' - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Log to registry - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKER_USER }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - - run: echo VERSION=$(grep '^version =' Cargo.toml | cut -d '"' -f 2) >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and Push - uses: docker/build-push-action@v6 - with: - file: Dockerfile - push: true - tags: "${{ vars.DOCKER_USER }}/service_hulypulse:${{ env.VERSION }},${{ vars.DOCKER_USER }}/service_hulypulse:latest" - platforms: linux/amd64,linux/arm64 diff --git a/foundations/hulypulse/.github/workflows/ci.yml b/foundations/hulypulse/.github/workflows/ci.yml deleted file mode 100644 index c5dc5a0012..0000000000 --- a/foundations/hulypulse/.github/workflows/ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI Validation - -on: - workflow_dispatch: - pull_request: - push: - branches: - - main - - master - -jobs: - rust-validate: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo registry and target - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- - - - name: Format check - run: cargo fmt --all -- --check - - - name: Build - run: cargo build --locked - - - name: Clippy - run: cargo clippy --bin hulypulse --all-features -- -D warnings - - - name: Unit tests - run: cargo test --bin hulypulse --locked diff --git a/foundations/hulypulse/.gitignore b/foundations/hulypulse/.gitignore deleted file mode 100644 index e980812ead..0000000000 --- a/foundations/hulypulse/.gitignore +++ /dev/null @@ -1,22 +0,0 @@ -/off -/target -/scripts/other -/scripts/off -/scripts/typing-test.sh -/scripts/TEST_LLEOTOKEN.html -Justfile -commit.sh -bloat.sh -/src/GO.sh -/src/GOT.sh -pulse-status.sh -GO.sh -TEST.sh -TEST_WS.sh -DROP_DB.sh -TODO.txt -DOCKER.sh -/lleo -/client -/scripts - diff --git a/foundations/hulypulse/Cargo.lock b/foundations/hulypulse/Cargo.lock deleted file mode 100644 index d93f33437b..0000000000 --- a/foundations/hulypulse/Cargo.lock +++ /dev/null @@ -1,3992 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-cors" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d" -dependencies = [ - "actix-utils", - "actix-web", - "derive_more", - "futures-util", - "log", - "once_cell", - "smallvec", -] - -[[package]] -name = "actix-http" -version = "3.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cceded2fb55f3c4b67068fa64962e2ca59614edc5b03167de9ff82ae803da0" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-tls", - "actix-utils", - "base64", - "bitflags 2.9.4", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2", - "http 0.2.12", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d324164c51f63867b57e73ba5936ea151b8a41a1d23d1031eeb9f70d0236f8" -dependencies = [ - "bytestring", - "cfg-if", - "http 0.2.12", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-tls" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac453898d866cdbecdbc2334fe1738c747b4eba14a677261f2b768ba05329389" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "impl-more", - "pin-project-lite", - "tokio", - "tokio-rustls 0.23.4", - "tokio-util", - "tracing", - "webpki-roots 0.22.6", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a597b77b5c6d6a1e1097fddde329a83665e25c5437c696a3a9a4aa514a614dea" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-tls", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.5.10", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "actix-ws" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a1fb4f9f2794b0aadaf2ba5f14a6f034c7e86957b458c506a8cb75953f2d99" -dependencies = [ - "actix-codec", - "actix-http", - "actix-web", - "bytestring", - "futures-core", - "tokio", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "arc-swap" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-tungstenite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" -dependencies = [ - "atomic-waker", - "futures-core", - "futures-io", - "futures-task", - "futures-util", - "log", - "pin-project-lite", - "tungstenite 0.27.0", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backon" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "592277618714fbcecda9a02ba7a8781f319d26532a88553bbacc77ba5d2b3a8d" -dependencies = [ - "fastrand", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" - -[[package]] -name = "bytestring" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.2.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1354349954c6fc9cb0deab020f27f783cf0b604e8bb754dc4658ecf0d29c35f" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "chrono-tz" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" -dependencies = [ - "chrono", - "phf", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "config" -version = "0.15.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e549344080374f9b32ed41bf3b6b57885ff6a289367b3dbc10eea8acc1918" -dependencies = [ - "pathdiff", - "serde_core", - "serde_json", - "toml", - "winnow", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_builder" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" -dependencies = [ - "derive_builder_macro", -] - -[[package]] -name = "derive_builder_core" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_builder_macro" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" -dependencies = [ - "derive_builder_core", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fancy-regex" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "find-msvc-tools" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" - -[[package]] -name = "flate2" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fluent-uri" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fraction" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" -dependencies = [ - "lazy_static", - "num", -] - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasi 0.14.7+wasi-0.2.4", - "wasm-bindgen", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "globset" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" -dependencies = [ - "aho-corasick", - "bstr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "governor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" -dependencies = [ - "cfg-if", - "dashmap", - "futures-sink", - "futures-timer", - "futures-util", - "getrandom 0.3.3", - "hashbrown 0.15.5", - "nonzero_ext", - "parking_lot 0.12.4", - "portable-atomic", - "quanta", - "rand 0.9.2", - "smallvec", - "spinning_top", - "web-time", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.11.4", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.3.1", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hulypulse" -version = "0.4.2" -dependencies = [ - "actix-cors", - "actix-web", - "actix-ws", - "anyhow", - "config", - "futures", - "futures-util", - "hex", - "hulyrs", - "md5", - "redis", - "regorus", - "secrecy", - "serde", - "serde_json", - "serde_with", - "strum", - "tokio", - "tokio-stream", - "tokio-tungstenite", - "tracing", - "tracing-subscriber", - "url", - "uuid", -] - -[[package]] -name = "hulyrs" -version = "0.1.0" -source = "git+https://github.com/hcengineering/hulyrs.git#3684385152b5022a2ef2c44a4e99c561e965de57" -dependencies = [ - "actix-web", - "bytes", - "chrono", - "config", - "derive_builder", - "futures", - "governor", - "itoa", - "jsonwebtoken", - "num-traits", - "rand 0.9.2", - "reqwest", - "reqwest-middleware", - "reqwest-ratelimit", - "reqwest-retry", - "reqwest-websocket", - "ryu", - "secrecy", - "serde", - "serde_json", - "serde_with", - "strum", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio_with_wasm", - "tracing", - "url", - "uuid", - "wasmtimer", -] - -[[package]] -name = "hyper" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "http 1.3.1", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.3.1", - "hyper", - "hyper-util", - "rustls 0.23.32", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower-service", - "webpki-roots 1.0.2", -] - -[[package]] -name = "hyper-util" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.0", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-more" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" -dependencies = [ - "equivalent", - "hashbrown 0.16.0", - "serde", - "serde_core", -] - -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonschema" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d" -dependencies = [ - "ahash", - "base64", - "bytecount", - "email_address", - "fancy-regex", - "fraction", - "idna", - "itoa", - "num-cmp", - "num-traits", - "once_cell", - "percent-encoding", - "referencing", - "regex", - "regex-syntax", - "serde", - "serde_json", - "uuid-simd", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64", - "js-sys", - "pem", - "ring 0.17.14", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.176" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[package]] -name = "lock_api" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "md5" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae960838283323069879657ca3de837e9f7bbb4c7bf6ea7f1b290d5e9476d2e0" - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "msvc_spectre_libs" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e871a9861f3664f18b7e04e9301d4edd55090c2dadb4b1c602e26ab32b1f5b" -dependencies = [ - "cc", -] - -[[package]] -name = "nonzero_ext" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - -[[package]] -name = "parking_lot" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" -dependencies = [ - "lock_api", - "parking_lot_core 0.9.11", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.17", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "pathdiff" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" - -[[package]] -name = "pem" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" -dependencies = [ - "base64", - "serde", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "potential_utf" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "proc-macro2" -version = "1.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi 0.11.1+wasi-snapshot-preview1", - "web-sys", - "winapi", -] - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.32", - "socket2 0.6.0", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand 0.9.2", - "ring 0.17.14", - "rustc-hash", - "rustls 0.23.32", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.0", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "redis" -version = "0.32.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" -dependencies = [ - "arc-swap", - "backon", - "bytes", - "cfg-if", - "combine", - "futures-channel", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "rand 0.9.2", - "ryu", - "sha1_smol", - "socket2 0.6.0", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.9.4", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "referencing" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e" -dependencies = [ - "ahash", - "fluent-uri", - "once_cell", - "parking_lot 0.12.4", - "percent-encoding", - "serde_json", -] - -[[package]] -name = "regex" -version = "1.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b5288124840bee7b386bc413c487869b360b2b4ec421ea56425128692f2a82c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943f41321c63ef1c92fd763bfe054d2668f7f225a5c29f0105903dc2fc04ba30" - -[[package]] -name = "regex-syntax" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" - -[[package]] -name = "regorus" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee058fce2fefa4eb9364b0a514296c70235f0f29bb92ec2c0d24766b837f08aa" -dependencies = [ - "anyhow", - "chrono", - "chrono-tz", - "data-encoding", - "globset", - "jsonschema", - "lazy_static", - "msvc_spectre_libs", - "rand 0.9.2", - "regex", - "scientific", - "semver", - "serde", - "serde_json", - "serde_yaml", - "thiserror 2.0.17", - "url", - "uuid", -] - -[[package]] -name = "reqwest" -version = "0.12.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" -dependencies = [ - "base64", - "bytes", - "futures-core", - "http 1.3.1", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.32", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-rustls 0.26.4", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webpki-roots 1.0.2", -] - -[[package]] -name = "reqwest-middleware" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" -dependencies = [ - "anyhow", - "async-trait", - "http 1.3.1", - "reqwest", - "serde", - "thiserror 1.0.69", - "tower-service", -] - -[[package]] -name = "reqwest-ratelimit" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8fff0d8036f23dcad6c27605ca3baa8ae3867438d0a8b34072f40f6c8bf628" -dependencies = [ - "async-trait", - "http 1.3.1", - "reqwest", - "reqwest-middleware", -] - -[[package]] -name = "reqwest-retry" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c73e4195a6bfbcb174b790d9b3407ab90646976c55de58a6515da25d851178" -dependencies = [ - "anyhow", - "async-trait", - "futures", - "getrandom 0.2.16", - "http 1.3.1", - "hyper", - "parking_lot 0.11.2", - "reqwest", - "reqwest-middleware", - "retry-policies", - "thiserror 1.0.69", - "tokio", - "tracing", - "wasm-timer", -] - -[[package]] -name = "reqwest-websocket" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd5f79b25f7f17a62cc9337108974431a66ae5a723ac0d9fe78ac1cce2027720" -dependencies = [ - "async-tungstenite", - "bytes", - "futures-util", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tracing", - "tungstenite 0.27.0", - "web-sys", -] - -[[package]] -name = "retry-policies" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted 0.9.0", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - -[[package]] -name = "rustls" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" -dependencies = [ - "log", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls" -version = "0.23.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" -dependencies = [ - "once_cell", - "ring 0.17.14", - "rustls-pki-types", - "rustls-webpki 0.103.7", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" -dependencies = [ - "ring 0.17.14", - "rustls-pki-types", - "untrusted 0.9.0", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.1", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scientific" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a4b339a8de779ecb098a772ecbba2ace74e23ed959a5b4f30631d8bf1799a8" -dependencies = [ - "scientific-macro", -] - -[[package]] -name = "scientific-macro" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "secrecy" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.145" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_spanned" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5417783452c2be558477e104686f7de5dae53dba813c28435e0e70f82d9b04ee" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" -dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.11.4", - "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap 2.11.4", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" -dependencies = [ - "libc", -] - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.17", - "time", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - -[[package]] -name = "spinning_top" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" -dependencies = [ - "lock_api", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.47.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" -dependencies = [ - "backtrace", - "bytes", - "io-uring", - "libc", - "mio", - "parking_lot 0.12.4", - "pin-project-lite", - "signal-hook-registry", - "slab", - "socket2 0.6.0", - "tokio-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls 0.22.4", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls 0.23.32", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "rustls 0.22.4", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.25.0", - "tungstenite 0.21.0", -] - -[[package]] -name = "tokio-util" -version = "0.7.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio_with_wasm" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dfba9b946459940fb564dcf576631074cdfb0bfe4c962acd4c31f0dca7897e6" -dependencies = [ - "js-sys", - "tokio", - "tokio_with_wasm_proc", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "tokio_with_wasm_proc" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e04c1865c281139e5ccf633cb9f76ffdaabeebfe53b703984cf82878e2aabb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e5e5d9bf2475ac9d4f0d9edab68cc573dc2fd644b0dba36b0c30a92dd9eaa0" -dependencies = [ - "serde_core", - "serde_spanned", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_datetime" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_parser" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes", - "futures-util", - "http 1.3.1", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" -dependencies = [ - "once_cell", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.8.5", - "rustls 0.22.4", - "rustls-pki-types", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" -dependencies = [ - "bytes", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unicode-ident" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf-8" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" -dependencies = [ - "getrandom 0.3.3", - "js-sys", - "rand 0.9.2", - "serde", - "wasm-bindgen", -] - -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "uuid", - "vsimd", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.12.4", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.14", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - -[[package]] -name = "webpki-roots" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6844ee5416b285084d3d3fffd743b925a6c9385455f64f6d4fa3031c4c2749a9" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb307e42a74fb6de9bf3a02d9712678b22399c87e6fa869d6dfcd8c1b7754e0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0abd1ddbc6964ac14db11c7213d6532ef34bd9aa042c2e5935f59d7908b46a5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" - -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.4", -] - -[[package]] -name = "windows-sys" -version = "0.61.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d42b7b7f66d2a06854650af09cfdf8713e427a439c97ad65a6375318033ac4b" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - -[[package]] -name = "winnow" -version = "0.7.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" - -[[package]] -name = "yoke" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - -[[package]] -name = "zerotrie" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] diff --git a/foundations/hulypulse/Cargo.toml b/foundations/hulypulse/Cargo.toml deleted file mode 100644 index 51ea626f13..0000000000 --- a/foundations/hulypulse/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "hulypulse" -version = "0.4.2" -edition = "2024" - -[dependencies] -tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "signal"] } -tracing = "0.1.41" -tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } -config = { version = "0.15", default-features = false, features = ["json", "toml"] } -anyhow = "1.0.97" -serde = { version = "1.0.219", features = ["derive"] } -serde_with = "3" -serde_json = "1.0" -actix-web = { version = "4.10", default-features = false, features = ["macros"] } -actix-cors = "0.7.1" -actix-ws = "0.3.0" -md5 = "0.8.0" -url = "2" -hex = "0.4.3" -tokio-stream = "0.1" -strum = { version = "0.27.2", features = ["derive"] } -futures-util = "0.3" -futures = "0.3" - -# auth: -regorus = { version = "0.5.0", optional = true } -uuid = { version = "1.7", features = ["v4", "serde"], optional = true } -hulyrs = { git = "https://github.com/hcengineering/hulyrs.git", features = [ "actix" ], optional = true } -secrecy = { version = "0.10.3", optional = true } - -#redis -redis = { version = "=0.32.5", features = ["aio", "tokio-comp", "sentinel", "connection-manager"] } - -[[bin]] -name = "hulypulse" -path = "src/main.rs" - -[dev-dependencies] -tokio-tungstenite = { version = "0.21", default-features = false, features = [ - "rustls-tls-native-roots", - "connect", -] } - -[features] -default = ["auth"] # lopt -auth = ["regorus", "uuid", "hulyrs", "secrecy"] -lopt = [] diff --git a/foundations/hulypulse/Dockerfile b/foundations/hulypulse/Dockerfile deleted file mode 100644 index 7d80417bf7..0000000000 --- a/foundations/hulypulse/Dockerfile +++ /dev/null @@ -1,35 +0,0 @@ -FROM --platform=$BUILDPLATFORM rust:1.88 AS builder -ARG TARGETPLATFORM - -WORKDIR /tmp/build - -COPY . . - -RUN \ - if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ - cargo build --release --target=x86_64-unknown-linux-gnu ; \ - elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ - apt-get update && apt-get install -y \ - gcc-aarch64-linux-gnu \ - g++-aarch64-linux-gnu \ - libc6-dev-arm64-cross \ - && rm -rf /var/lib/apt/lists/* ; \ -\ - rustup target add aarch64-unknown-linux-gnu ; \ -\ - export CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc ; \ - export CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++ ; \ - export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc ; \ -\ - cargo build --release --target=aarch64-unknown-linux-gnu ; \ - else \ - echo "Unexpected target platform: $TARGETPLATFORM" && exit 1 ; \ - fi - -FROM debian:12-slim - -ARG TARGET -COPY --from=builder /tmp/build/target/*/release/hulypulse /usr/local/bin/hulypulse -RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* - -ENTRYPOINT ["/usr/local/bin/hulypulse"] diff --git a/foundations/hulypulse/LICENSE b/foundations/hulypulse/LICENSE deleted file mode 100644 index e48e096345..0000000000 --- a/foundations/hulypulse/LICENSE +++ /dev/null @@ -1,277 +0,0 @@ -Eclipse Public License - v 2.0 - - THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE - PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION - OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - - a) in the case of the initial Contributor, the initial content - Distributed under this Agreement, and - - b) in the case of each subsequent Contributor: - i) changes to the Program, and - ii) additions to the Program; - where such changes and/or additions to the Program originate from - and are Distributed by that particular Contributor. A Contribution - "originates" from a Contributor if it was added to the Program by - such Contributor itself or anyone acting on such Contributor's behalf. - Contributions do not include changes or additions to the Program that - are not Modified Works. - -"Contributor" means any person or entity that Distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which -are necessarily infringed by the use or sale of its Contribution alone -or when combined with the Program. - -"Program" means the Contributions Distributed in accordance with this -Agreement. - -"Recipient" means anyone who receives the Program under this Agreement -or any Secondary License (as applicable), including Contributors. - -"Derivative Works" shall mean any work, whether in Source Code or other -form, that is based on (or derived from) the Program and for which the -editorial revisions, annotations, elaborations, or other modifications -represent, as a whole, an original work of authorship. - -"Modified Works" shall mean any work in Source Code or other form that -results from an addition to, deletion from, or modification of the -contents of the Program, including, for purposes of clarity any new file -in Source Code form that contains any contents of the Program. Modified -Works shall not include works that contain only declarations, -interfaces, types, classes, structures, or files of the Program solely -in each case in order to link to, bind by name, or subclass the Program -or Modified Works thereof. - -"Distribute" means the acts of a) distributing or b) making available -in any manner that enables the transfer of a copy. - -"Source Code" means the form of a Program preferred for making -modifications, including but not limited to software source code, -documentation source, and configuration files. - -"Secondary License" means either the GNU General Public License, -Version 2.0, or any later versions of that license, including any -exceptions or additional permissions as identified by the initial -Contributor. - -2. GRANT OF RIGHTS - - a) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free copyright - license to reproduce, prepare Derivative Works of, publicly display, - publicly perform, Distribute and sublicense the Contribution of such - Contributor, if any, and such Derivative Works. - - b) Subject to the terms of this Agreement, each Contributor hereby - grants Recipient a non-exclusive, worldwide, royalty-free patent - license under Licensed Patents to make, use, sell, offer to sell, - import and otherwise transfer the Contribution of such Contributor, - if any, in Source Code or other form. This patent license shall - apply to the combination of the Contribution and the Program if, at - the time the Contribution is added by the Contributor, such addition - of the Contribution causes such combination to be covered by the - Licensed Patents. The patent license shall not apply to any other - combinations which include the Contribution. No hardware per se is - licensed hereunder. - - c) Recipient understands that although each Contributor grants the - licenses to its Contributions set forth herein, no assurances are - provided by any Contributor that the Program does not infringe the - patent or other intellectual property rights of any other entity. - Each Contributor disclaims any liability to Recipient for claims - brought by any other entity based on infringement of intellectual - property rights or otherwise. As a condition to exercising the - rights and licenses granted hereunder, each Recipient hereby - assumes sole responsibility to secure any other intellectual - property rights needed, if any. For example, if a third party - patent license is required to allow Recipient to Distribute the - Program, it is Recipient's responsibility to acquire that license - before distributing the Program. - - d) Each Contributor represents that to its knowledge it has - sufficient copyright rights in its Contribution, if any, to grant - the copyright license set forth in this Agreement. - - e) Notwithstanding the terms of any Secondary License, no - Contributor makes additional grants to any Recipient (other than - those set forth in this Agreement) as a result of such Recipient's - receipt of the Program under the terms of a Secondary License - (if permitted under the terms of Section 3). - -3. REQUIREMENTS - -3.1 If a Contributor Distributes the Program in any form, then: - - a) the Program must also be made available as Source Code, in - accordance with section 3.2, and the Contributor must accompany - the Program with a statement that the Source Code for the Program - is available under this Agreement, and informs Recipients how to - obtain it in a reasonable manner on or through a medium customarily - used for software exchange; and - - b) the Contributor may Distribute the Program under a license - different than this Agreement, provided that such license: - i) effectively disclaims on behalf of all other Contributors all - warranties and conditions, express and implied, including - warranties or conditions of title and non-infringement, and - implied warranties or conditions of merchantability and fitness - for a particular purpose; - - ii) effectively excludes on behalf of all other Contributors all - liability for damages, including direct, indirect, special, - incidental and consequential damages, such as lost profits; - - iii) does not attempt to limit or alter the recipients' rights - in the Source Code under section 3.2; and - - iv) requires any subsequent distribution of the Program by any - party to be under a license that satisfies the requirements - of this section 3. - -3.2 When the Program is Distributed as Source Code: - - a) it must be made available under this Agreement, or if the - Program (i) is combined with other material in a separate file or - files made available under a Secondary License, and (ii) the initial - Contributor attached to the Source Code the notice described in - Exhibit A of this Agreement, then the Program may be made available - under the terms of such Secondary Licenses, and - - b) a copy of this Agreement must be included with each copy of - the Program. - -3.3 Contributors may not remove or alter any copyright, patent, -trademark, attribution notices, disclaimers of warranty, or limitations -of liability ("notices") contained within the Program from any copy of -the Program which they Distribute, provided that Contributors may add -their own appropriate notices. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities -with respect to end users, business partners and the like. While this -license is intended to facilitate the commercial use of the Program, -the Contributor who includes the Program in a commercial product -offering should do so in a manner which does not create potential -liability for other Contributors. Therefore, if a Contributor includes -the Program in a commercial product offering, such Contributor -("Commercial Contributor") hereby agrees to defend and indemnify every -other Contributor ("Indemnified Contributor") against any losses, -damages and costs (collectively "Losses") arising from claims, lawsuits -and other legal actions brought by a third party against the Indemnified -Contributor to the extent caused by the acts or omissions of such -Commercial Contributor in connection with its distribution of the Program -in a commercial product offering. The obligations in this section do not -apply to any claims or Losses relating to any actual or alleged -intellectual property infringement. In order to qualify, an Indemnified -Contributor must: a) promptly notify the Commercial Contributor in -writing of such claim, and b) allow the Commercial Contributor to control, -and cooperate with the Commercial Contributor in, the defense and any -related settlement negotiations. The Indemnified Contributor may -participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial -product offering, Product X. That Contributor is then a Commercial -Contributor. If that Commercial Contributor then makes performance -claims, or offers warranties related to Product X, those performance -claims and warranties are such Commercial Contributor's responsibility -alone. Under this section, the Commercial Contributor would have to -defend claims against the other Contributors related to those performance -claims and warranties, and if a court requires any other Contributor to -pay any damages as a result, the Commercial Contributor must pay -those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" -BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR -IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF -TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR -PURPOSE. Each Recipient is solely responsible for determining the -appropriateness of using and distributing the Program and assumes all -risks associated with its exercise of rights under this Agreement, -including but not limited to the risks and costs of program errors, -compliance with applicable laws, damage to or loss of data, programs -or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT -PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS -SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST -PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE -EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under -applicable law, it shall not affect the validity or enforceability of -the remainder of the terms of this Agreement, and without further -action by the parties hereto, such provision shall be reformed to the -minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity -(including a cross-claim or counterclaim in a lawsuit) alleging that the -Program itself (excluding combinations of the Program with other software -or hardware) infringes such Recipient's patent(s), then such Recipient's -rights granted under Section 2(b) shall terminate as of the date such -litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it -fails to comply with any of the material terms or conditions of this -Agreement and does not cure such failure in a reasonable period of -time after becoming aware of such noncompliance. If all Recipient's -rights under this Agreement terminate, Recipient agrees to cease use -and distribution of the Program as soon as reasonably practicable. -However, Recipient's obligations under this Agreement and any licenses -granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, -but in order to avoid inconsistency the Agreement is copyrighted and -may only be modified in the following manner. The Agreement Steward -reserves the right to publish new versions (including revisions) of -this Agreement from time to time. No one other than the Agreement -Steward has the right to modify this Agreement. The Eclipse Foundation -is the initial Agreement Steward. The Eclipse Foundation may assign the -responsibility to serve as the Agreement Steward to a suitable separate -entity. Each new version of the Agreement will be given a distinguishing -version number. The Program (including Contributions) may always be -Distributed subject to the version of the Agreement under which it was -received. In addition, after a new version of the Agreement is published, -Contributor may elect to Distribute the Program (including its -Contributions) under the new version. - -Except as expressly stated in Sections 2(a) and 2(b) above, Recipient -receives no rights or licenses to the intellectual property of any -Contributor under this Agreement, whether expressly, by implication, -estoppel or otherwise. All rights in the Program not expressly granted -under this Agreement are reserved. Nothing in this Agreement is intended -to be enforceable by any entity that is not a Contributor or Recipient. -No third-party beneficiary rights are created under this Agreement. - -Exhibit A - Form of Secondary Licenses Notice - -"This Source Code may also be made available under the following -Secondary Licenses when the conditions for such availability set forth -in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), -version(s), and exceptions or additional permissions here}." - - Simply including a copy of this Agreement, including this Exhibit A - is not sufficient to license the Source Code under Secondary Licenses. - - If it is not possible or desirable to put the notice in a particular - file, then You may include the notice in a location (such as a LICENSE - file in a relevant directory) where a recipient would be likely to - look for such a notice. - - You may add additional accurate notices of copyright ownership. diff --git a/foundations/hulypulse/README.md b/foundations/hulypulse/README.md deleted file mode 100644 index 8e457bd91c..0000000000 --- a/foundations/hulypulse/README.md +++ /dev/null @@ -1,259 +0,0 @@ -# Hulypulse - -Hulypulse is a service that enables clients to share information on a “whiteboard”. Clients connected to the same “whiteboard” see data provided by other clients to the whiteboard. - -The service is exposed as REST and WebSocket API. - -**Usage scenarios:** - -- user presence in a document -- user is “typing” event -- user cursor position in editor or drawing board -- service posts a process status - -## Key - -Key is a string that consists of one or multiple segments separated by ‘/’. Example: foo/bar/baz. -Key may not end with ‘/’ -Segment may not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) -Segment may not be empty -Key segment may be private (prefixed with ‘$’) - - Query - -May not contain special characters (‘*’, ‘?’, ‘[’, ‘]’,‘\’,‘\x00..\xF1’,‘\x7F’,‘"’,‘'’) -It is possible to use prefix, for listings / subscriptions (prefix ends with segment separator ‘/’) - -- GET/SUBSCRIBE/.. a/b → single key -- GET/SUBSCRIBE/.. a/b/c/ → multiple - - If multiple - -select all keys starting with prefix -skip keys, containing private segments to the right from the prefix - - example - -- 1. /a/b/$c/$d, 2. /a/b/c, 3. /a/b/$c, 4. /a/b/$c/$d/e -- / → [2] -- /a/b/ → [2] -- /a/b/$c/ → [3] -- /a/b/$c/$d/ → [4] -- /a/b/$c/$d → [1] - - -## Data -“Data” is an arbitrary JSON document. -Size of data is limited to some reasonable size - -## HTTP API - -```GET /status``` - server status and websockets count -- Answer: `{"status":"OK","websockets":2}` - - -```PUT /{workspace}/{key}``` - Save key -- Input - - Body - data - - Content-Type: application/json - - Content-Length: optional - - Headers: TTL or absolute expiration time - - `HULY-TTL` — autodelete in N seconds - - or `HULY-EXPIRE-AT` — autodelete in UnixTime - - default max_ttl = 3600 (settings in config/default.toml) - - Conditional Headers: - - `If-Match: *` — update only if the key exists - - `If-Match: ` — update only if current value's MD5 matches - - `If-None-Match: *` — insert only if the key does not exist -- Output - - Status: - - `201` if inserted with `If-None-Match: *` - - `204` on successful insert or update - - `412` if the condition is not met - - `400` if headers are invalid - - Body: `DONE` - -```DELETE /{workspace}/{key}``` - Delete key -- Output - - Status: `204 No content`, no body - - `404 Not Found` if nothing to do - -```GET /{workspace}/{key}``` - Read one key -- Output - - Status 200 - - Content-type: application/json - - Header: `Etag: ` - - Body: - - workspace (copy of input) - - key (copy of input) - - data (copy of input) - - expiresAt / TTL (copy of input, optional) - - etag - -```GET /{workspace}/{key}/``` - Read array of keys -- Output - - Status 200 - - Content-type: application/json - - Body (array): - - [{"key","data","ttl","etag"}, ...] - -## WebSocket API - -**Client to Server** - -```PUT``` - - type: "put" - - correlation id (optional) - - key: - - “workspace/foo/bar“ - shared key - - “workspace/foo/bar/$/secret“ - secret key - - data - ** time control (optional) ** - - `TTL` — autodelete in N seconds - - `ExpireAt` — autodelete in UnixTime - - or default max_ttl = 3600 (settings in config/default.toml) - ** Conditional (optional) ** - - `ifMatch: *` — update only if the key exists - - `ifMatch: ` — update only if current value's MD5 matches - - `ifNoneMatch: *` — insert only if the key does not exist - -- Answer: `{"action":"put","correlation":"abc123","result":"OK"}` - - -```GET``` - - type: "get" - - correlation id (optional) - - key: - - “workspace/foo/bar“ - one shared key - - “workspace/foo/bar/$/secret“ - one secret key - -- Answer: `{"action":"get","result":{"data":"hello","etag":"5d41402abc4b2a76b9719d911017c592","ttl":3599,"key":"00000000-0000-0000-0000-000000000001/foo/bar"}}` - - -```LIST``` - - type: "list" - - correlation id (optional) - - key: - - “workspace/foo/bar/“ - keys from public space - - “workspace/foo/bar/$/secret/“ - keys from secret space - -- Answer: `{"action":"list","result":[{"data":"hello 1","etag":"df0649bc4f1be901c85b6183091c1d83","ttl":3570,"key":"00000000-0000-0000-0000-000000000001/foo/bar1"},{"data":"hello 2","etag":"bb21ec8394b75795622f61613a777a8b","ttl":3555,"key":"00000000-0000-0000-0000-000000000001/foo/bar2"}]}` - - -```DELETE``` - - type: "delete" - - correlation id (optional) - - key: “workspace/foo/bar“ - ** Conditional (optional) ** - - `ifMatch: ` — delete only if current value's MD5 matches - - `ifMatch: *` — return error if key does not exist - -- Answer: `{"action":"delete","result":"OK"}` - - -```SUBSCRIBE``` - type: "sub" - key: - - “workspace/foo/bar“ - subscribe one shared key - - “workspace/foo/bar/“ - subscribe all keys started with - - “workspace/foo/bar/$/my_secret“ - subscribe one secret key - - “workspace/foo/bar/$/my_secret/“ - subscribe all keys started with secret - -- Answer: `{"action":"sub","result":"OK"}` - - -```UNSUBSCRIBE``` - - type: "unsub" - - key: - - “workspace/foo/bar“ - unsubscribe subscribed key - - “*“ - unsubscribe all - -- Answer: `{"action":"unsub","result":"OK"}` - -```MY SUBSCRIBES``` - - type: "sublist" - -- Answer: `{"action":"list","result":["00000000-0000-0000-0000-000000000001/foo/bar1","00000000-0000-0000-0000-000000000001/foo/bar2"]}` - -```INFO``` - - type: "info" - -- Answer: `{"db_mode":"memory","memory_info":"1231 keys, 80345 bytes","status":"OK","websockets":164}` - - -** Server to Client ** subscribed events: - - - `{"message":"Set","key":"00000000-0000-0000-0000-000000000001/foo/bar","value":"hello"}` - - - `{"message":"Expired","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` - - - `{"message":"Del","key":"00000000-0000-0000-0000-000000000001/foo/bar"}` - -## Special options in config/default.toml - - ```backend = "memory"``` Use native memory storage instead Redis - - ```max_size = 100``` Max value size in bytes - -## Special cargo build options - - "auth" (default) - use huly-authorization - Disable auth: - cargo build --no-default-features - Enable auth: - cargo build --no-default-features --features "auth" - -## Running - -Pre-build docker images is available at: hardcoreeng/service_hulypulse:{tag}. - -You can use the following command to run the image locally: -```bash -docker run -p 8095:8095 -it --rm hardcoreeng/service_hulypulse:{tag} -``` - -Run from source using Redis: -```bash -HULY_REDIS_URLS=redis://huly.local:6379 cargo run -``` - -Run from source in in-memory mode: -```bash -HULY_BACKEND=memory cargo run -``` - -If you want to run the service as a part of local huly development environment use the following command: -```bash - export HULY_REDIS_URLS="redis://huly.local:6379" - docker run --rm -it --network dev_default -p 8095:8095 hardcoreeng/service_hulypulse:{tag} -``` -This will run Hulypulse in the same network as the rest of huly services, and set the redis connection string to the one matching the local dev redis instance. - -You can then access hulypulse at http://localhost:8095. - - -## Authetication -Hulypulse uses bearer JWT token authetication. At the moment, it will accept any token signed by the hulypulse secret. The secret is set in the environment variable HULY_TOKEN_SECRET variable. - -## Configuration -The following environment variables are used to configure hulypulse: - - ```HULY_BIND_HOST```: host to bind the server to (default: 0.0.0.0) - - ```HULY_BIND_PORT```: port to bind the server to (default: 8094) - - ```HULY_TOKEN_SECRET```: secret used to sign JWT tokens (default: secret) - - ```HULY_BACKEND```: storage backend "redis" or "memory" (default: "redis") - - ```HULY_REDIS_URLS```: redis connection string (default: redis://huly.local:6379) - - ```HULY_REDIS_PASSWORD```: redis password (default: "<invalid>") - - ```HULY_REDIS_MODE```: redis mode "direct" or "sentinel" (default: "direct") - - ```HULY_REDIS_SERVICE```: redis service (default: "mymaster") - - ```HULY_MAX_TTL```: maximum storage time (default: 3600) - - TODO: ```HULY_PAYLOAD_SIZE_LIMIT```: maximum size of the payload (default: 2Mb) - -## Todo (in no particular order) -- [ ] Optional value encryption -- [ ] Support for open telemetry -- [ ] Concurrency control for database migration (several instances of hulypulse are updated at the same time) -- [ ] TLS support -- [ ] Liveness/readiness probe endpoint - -## Contributing -Contributions are welcome! Please open an issue or a pull request if you have any suggestions or improvements. - -## License -This project is licensed under EPL-2.0 diff --git a/foundations/hulypulse/client/off/client.ts b/foundations/hulypulse/client/off/client.ts deleted file mode 100644 index 9992863f12..0000000000 --- a/foundations/hulypulse/client/off/client.ts +++ /dev/null @@ -1,294 +0,0 @@ -// -// Copyright © 2024-2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -// import { WebSocket } from 'ws'; // для Node <20 обязательно - -// Unknown: -// import { type Ref, concatLink } from '@hcengineering/core' -// import { getMetadata } from '@hcengineering/platform' - -// import { getCurrentEmployee, type Person } from '@hcengineering/contact' -// import presence from '@hcengineering/presence' -// import presentation from '@hcengineering/presentation' -// import { type Unsubscriber, get } from 'svelte/store' - -// import { myPresence, myData, isAnybodyInMyRoom, onPersonUpdate, onPersonLeave, onPersonData } from './store' -// import type { RoomPresence, MyDataItem } from './types' - -// interface PresenceMessage { -// id: Ref -// type: 'update' | 'remove' -// presence?: RoomPresence[] -// lastUpdate?: number -// } - -// interface DataMessage { -// type: 'data' -// sender: Ref -// topic: string -// data: any -// } - -// type IncomingMessage = PresenceMessage | DataMessage - - - -export class HulypulseClient implements Disposable { - private ws: WebSocket | null = null - private closed = false - private reconnectTimeout: number | undefined - private pingTimeout: number | undefined - private pingInterval: number | undefined - private readonly RECONNECT_INTERVAL = 1000 - private readonly PING_INTERVAL = 30 * 1000 - private readonly PING_TIMEOUT = 5 * 60 * 1000 - private readonly myDataThrottleInterval = 100 - // private readonly url: string | URL = 'ws://localhost:8095' - - // private presence: RoomPresence[] - private readonly myDataTimestamps = new Map() - // // private readonly myPresenceUnsub: Unsubscriber - // private readonly myDataUnsub: Unsubscriber - - constructor (private readonly url: string | URL) { - // this.presence = get(myPresence) - // this.myPresenceUnsub = myPresence.subscribe((presence) => { - // this.handlePresenceChanged(presence) - // }) - // this.myDataUnsub = myData.subscribe((data) => { - // this.handleMyDataChanged(data, false) - // }) - - this.connect() - } - - // Close the connection - close (): void { - console.log('Closing connection') - this.closed = true - clearTimeout(this.reconnectTimeout) - this.stopPing() - - // this.myPresenceUnsub() - // this.myDataUnsub() - - if (this.ws !== null) { - this.ws.close() - this.ws = null - } - } - - // Open the connection and reconnect if it fails - private connect (): void { - console.log('Connecting to WebSocket: ', this.url) - try { - const ws = new WebSocket(this.url) - console.log('WebSocket created: ', ws) - this.ws = ws - - ws.onopen = () => { - console.log('WebSocket.onopen') - if (this.ws !== ws) { - return - } - - this.handleConnect() - } - - ws.onclose = (event: CloseEvent) => { - console.log('WebSocket.onclose') - if (this.ws !== ws) { - ws.close() - return - } - - this.reconnect() - } - - ws.onmessage = (event: MessageEvent) => { - console.log('WebSocket.onmessage: ', event.data) - if (this.closed || this.ws !== ws) { - return - } - - this.handleMessage(event.data) - } - - ws.onerror = (event: Event) => { - console.log('client websocket error', event) - if (this.ws !== ws) { - return - } - } - } catch (err: any) { - console.error('WebSocket error', err) - this.reconnect() - } - } - - private startPing (): void { - console.log('Starting ping') - clearInterval(this.pingInterval) - this.pingInterval = window.setInterval(() => { - if (this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - this.ws.send('ping') - } - clearTimeout(this.pingTimeout) - this.pingTimeout = window.setTimeout(() => { - if (this.ws !== null) { - console.log('no response from server') - clearInterval(this.pingInterval) - this.ws.close(1000) - } - }, this.PING_TIMEOUT) - }, this.PING_INTERVAL) - } - - private stopPing (): void { - console.log('Stopping ping') - clearInterval(this.pingInterval) - this.pingInterval = undefined - - clearTimeout(this.pingTimeout) - this.pingTimeout = undefined - } - - private reconnect (): void { - console.log('Reconnecting...') - clearTimeout(this.reconnectTimeout) - this.stopPing() - - if (!this.closed) { - this.reconnectTimeout = window.setTimeout(() => { - this.connect() - }, this.RECONNECT_INTERVAL) - } - } - - private handleConnect (): void { -// this.sendPresence(getCurrentEmployee(), this.presence) - this.startPing() -// this.handleMyDataChanged(get(myData), true) - } - - private static isConnectionLikeError (err: string): boolean { - const s = err.toLowerCase() - return ( - s.includes('broken pipe') || - s.includes('connection reset') || - s.includes('connection refused') || - s.includes('connection aborted') || - s.includes('unexpected eof') || - s.includes('io error') - ) - } - - private handleMessage (data: string): void { - if (data === 'pong') { - clearTimeout(this.pingTimeout) - return - } - - try { - const message = JSON.parse(data); // as IncomingMessage - console.log('Received message', message); - if ( - typeof message === 'object' && - message !== null && - 'error' in message && - typeof (message as { error: unknown }).error === 'string' && - HulypulseClient.isConnectionLikeError((message as { error: string }).error) - ) { - console.warn('Pulse server reported connection-like error; reconnecting') - this.reconnect() - return - } - // const message = JSON.parse(data) as IncomingMessage - // if (message.type === 'update' && message.presence !== undefined) { - // onPersonUpdate(message.id, message.presence ?? []) - // } else if (message.type === 'remove') { - // onPersonLeave(message.id) - // } else if (message.type === 'data') { - // onPersonData(message.sender, message.topic, message.data) - // } else { - // console.warn('Unknown message type', message) - // } - } catch (err: any) { - console.error('Error parsing message', err, data) - } - } - - // private handlePresenceChanged (presence: RoomPresence[]): void { - // this.presence = presence - // this.sendPresence(getCurrentEmployee(), this.presence) - // this.handleMyDataChanged(get(myData), true) - // } - - // private sendPresence (person: Ref, presence: RoomPresence[]): void { - // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - // const message: PresenceMessage = { id: person, type: 'update', presence } - // this.ws.send(JSON.stringify(message)) - // } - // } - - // private handleMyDataChanged (data: Map, forceSend: boolean): void { - // if (!isAnybodyInMyRoom()) { - // return - // } - // if (!this.closed && this.ws !== null && this.ws.readyState === WebSocket.OPEN) { - // for (const [topic, value] of data) { - // const lastSend = this.myDataTimestamps.get(topic) ?? 0 - // if (value.lastUpdated >= lastSend + this.myDataThrottleInterval || forceSend) { - // this.myDataTimestamps.set(topic, value.lastUpdated) - // const message: DataMessage = { - // sender: getCurrentEmployee(), - // type: 'data', - // topic, - // data: value.data - // } - // this.ws.send(JSON.stringify(message)) - // } - // } - // } - // } - - [Symbol.dispose] (): void { - this.close() - } -} - -export function connect (): HulypulseClient | undefined { - // const wsUuid = getMetadata(presentation.metadata.WorkspaceUuid) - // if (wsUuid === undefined) { - // console.warn('Workspace uuid is not defined') - // return undefined - // } - - // const token = getMetadata(presentation.metadata.Token) - - // const presenceUrl = getMetadata(presence.metadata.PresenceUrl) - // if (presenceUrl === undefined || presenceUrl === '') { - // console.warn('Presence URL is not defined') - // return undefined - // } - - // const url = new URL(concatLink(presenceUrl, wsUuid)) - // if (token !== undefined) { - // url.searchParams.set('token', token) - // } - - // return new HulypulseClient(url) - return new HulypulseClient("ws://localhost:8095") -} \ No newline at end of file diff --git a/foundations/hulypulse/policy.repo b/foundations/hulypulse/policy.repo deleted file mode 100644 index 0888019a93..0000000000 --- a/foundations/hulypulse/policy.repo +++ /dev/null @@ -1,7 +0,0 @@ -default permit = true - -#permit if { -# input.command == "Get" -# contains(input.key, "/typing/") -# input.claim.workspace == "00000000-0000-0000-0000-000000000001" -#} diff --git a/foundations/hulypulse/scripts/TEST.html b/foundations/hulypulse/scripts/TEST.html deleted file mode 100644 index b7f47d8964..0000000000 --- a/foundations/hulypulse/scripts/TEST.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - - - - - - - - - - - - - - -

Waiting for server response...
- - - - - diff --git a/foundations/hulypulse/scripts/TEST00.sh b/foundations/hulypulse/scripts/TEST00.sh deleted file mode 100755 index b050c29890..0000000000 --- a/foundations/hulypulse/scripts/TEST00.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) - -get "00000000-0000-0000-0000-000000000001/foo/bar1" -put "00000000-0000-0000-0000-000000000001/foo/bar1" "rediska" -get "00000000-0000-0000-0000-000000000001/foo/bar1" - -exit - -#echo ${TOKEN} -#exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API.sh b/foundations/hulypulse/scripts/TEST_HTTP_API.sh deleted file mode 100755 index 1b8b9816dd..0000000000 --- a/foundations/hulypulse/scripts/TEST_HTTP_API.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) -#echo ${TOKEN} -#exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -#exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh b/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh deleted file mode 100755 index 7ae9608387..0000000000 --- a/foundations/hulypulse/scripts/TEST_HTTP_API_repo.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -get "00000000-0000-0000-0000-000000000001/TESTS/val1" -#list "00000000-0000-0000-0000-000000000001/TESTS/" -get "00000000-0000-0000-0000-000000000002/TESTS/val1" - -exit - -#echo ${TOKEN} - -ZP="00000000-0000-0000-0000-000000000001/TESTS" - - - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -#exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_WS_API.sh b/foundations/hulypulse/scripts/TEST_WS_API.sh deleted file mode 100755 index 2d111e134c..0000000000 --- a/foundations/hulypulse/scripts/TEST_WS_API.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -clear -#source ./pulse_lib.sh - -websocat ws://127.0.0.1:8095/ws/testworkspace - -exit - - -let ws = new WebSocket("ws://localhost:8095/ws/testworkspace"); -ws.onmessage = e => console.log("Message from server:", e.data); -ws.onopen = () => ws.send("Hello from browser!"); - - - - - - - - - - - - - - - - - - - -TOKEN=$(./token.sh claims.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS" -# /AnyKey" - -# put ${ZP} "one text" - -# put "00000000-0000-0000-0000-000000000001/TESTS" "text 1" "If-None-Match: *" "Blooooooooo: blya" - -#exit - -#put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -#echo "sleep 1 sec" -#sleep 1 -#get "00000000-0000-0000-0000-000000000001/TESTS" -#echo "sleep 3 sec" -#sleep 2 -#get "00000000-0000-0000-0000-000000000001/TESTS" - -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/TEST_lleo.html b/foundations/hulypulse/scripts/TEST_lleo.html deleted file mode 100644 index 3a9608a5e1..0000000000 --- a/foundations/hulypulse/scripts/TEST_lleo.html +++ /dev/null @@ -1,129 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - -

Waiting for server response...
- - - - - diff --git a/foundations/hulypulse/scripts/TEST_no_auth.html b/foundations/hulypulse/scripts/TEST_no_auth.html deleted file mode 100644 index 20e2e14c30..0000000000 --- a/foundations/hulypulse/scripts/TEST_no_auth.html +++ /dev/null @@ -1,149 +0,0 @@ - - - - - WebSocket JSON Tester - - - - -

WebSocket JSON Tester

- - - -
- - - -

- - - - - - - - - - - - - - - - - - - - - - -

Waiting for server response...
- - - - - \ No newline at end of file diff --git a/foundations/hulypulse/scripts/claims.json b/foundations/hulypulse/scripts/claims.json deleted file mode 100644 index af4a64c94b..0000000000 --- a/foundations/hulypulse/scripts/claims.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000001" -} diff --git a/foundations/hulypulse/scripts/claims2.json b/foundations/hulypulse/scripts/claims2.json deleted file mode 100644 index ed17b895b8..0000000000 --- a/foundations/hulypulse/scripts/claims2.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000002" -} diff --git a/foundations/hulypulse/scripts/claims_exp.json b/foundations/hulypulse/scripts/claims_exp.json deleted file mode 100644 index dcc90fe4b8..0000000000 --- a/foundations/hulypulse/scripts/claims_exp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", - "workspace": "00000000-0000-0000-0000-000000000001", - "exp": 1924236800 -} diff --git a/foundations/hulypulse/scripts/claims_system.json b/foundations/hulypulse/scripts/claims_system.json deleted file mode 100644 index a6f400009e..0000000000 --- a/foundations/hulypulse/scripts/claims_system.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "1749089e-22e6-48de-af4e-165e18fbd2f9", - "workspace": "00000000-0000-0000-0000-000000000001" -} diff --git a/foundations/hulypulse/scripts/claims_wrong_ws.json b/foundations/hulypulse/scripts/claims_wrong_ws.json deleted file mode 100644 index 8bd456b086..0000000000 --- a/foundations/hulypulse/scripts/claims_wrong_ws.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extra": { - "service": "account" - }, - "account": "lleo", - "workspace": "00000000-0000-0000-0000-000000000002" -} diff --git a/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh b/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh deleted file mode 100755 index a30faf5608..0000000000 --- a/foundations/hulypulse/scripts/lleo_TEST_HTTP_API.sh +++ /dev/null @@ -1,149 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib_lleo.sh - -#TOKEN=$(./token.sh claims.json) -#echo ${TOKEN} - - - -put "dnevnik/onlline/admin" "oki" "HULY-TTL: 3" - - - -exit -ZP="00000000-0000-0000-0000-000000000001/TESTS" - -put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 12" -put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 3" - -get "00000000-0000-0000-0000-000000000001/TESTS/" -sleep 4 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/scripts/pulse_lib.sh b/foundations/hulypulse/scripts/pulse_lib.sh deleted file mode 100755 index 19f565c8f8..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib.sh +++ /dev/null @@ -1,77 +0,0 @@ -#!/bin/bash - -clear - -URL="http://localhost:8099/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/pulse_lib_huly.sh b/foundations/hulypulse/scripts/pulse_lib_huly.sh deleted file mode 100755 index a500cf371f..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib_huly.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -clear - -# URL="http://localhost:8095/api" -URL="http://huly.local:8099/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/pulse_lib_lleo.sh b/foundations/hulypulse/scripts/pulse_lib_lleo.sh deleted file mode 100755 index 4cf8832fca..0000000000 --- a/foundations/hulypulse/scripts/pulse_lib_lleo.sh +++ /dev/null @@ -1,79 +0,0 @@ -#!/bin/bash - -clear - -# URL="http://localhost:8095/api" -URL="https://hulypulse_mem.lleo.me/api" - -R='\033[0;31m' # Color red -G='\033[0;32m' # Color green -W='\033[0;33m' # Color ? -S='\033[0;34m' # Color Blue -F='\033[0;35m' # Color Fiolet -L='\033[0;36m' # Color LightBlue -N='\033[0m' # No Color -GRAY='\033[90m' # bright black - -api() { - local tmpfile - tmpfile=$1 - local status - status=$(head -n 1 "$tmpfile") - local status_code - status_code=$(echo "$status" | awk '{print $2}') - local etag - etag=$(grep -i "^ETag:" "${tmpfile}") - local body - body=$(awk 'found { print; next } NF == 0 { found = 1 }' "$tmpfile") - case "$status_code" in - 2*) echo -en "${G}${status}${N}" ;; - 3*) echo -en "${F}${status}${N}" ;; - 4*) echo -en "${R}${status}${N}" ;; - 5*) echo -en "${R}${status}${N}" ;; - *) echo -en "${GRAY}${status}${N}" ;; - esac - if [ -n "$etag" ]; then echo -n -e " ${F}${etag}${N}" ; fi - - body=$(echo "$body" | sed 's/{/\\n{/g') - - if [ -n "$body" ]; then echo -e "\n ${GRAY}[${body}]${N}" ; else echo -e " ${L}(no body)${N}" ; fi - rm -f "$tmpfile" -} - -get() { - echo -n -e "📥 ${L}GET ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X GET "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -put() { # If-None-Match If-Match - local match - local match_prn -# if [ -n "$3" ]; then match=(-H "$3: $4") ; else match=() ; fi -# if [ -n "$3" ]; then match_prn=" ${F}$3:$4${N}" ; else match_prn="" ; fi -# echo -n -e "📥 ${L}PUT ${W}$1${N}${match_prn} > " - - if [ -n "$3" ]; then match1=(-H "$3") ; else match1=() ; fi - if [ -n "$3" ]; then match1_prn=" ${F}$3${N}" ; else match1_prn="" ; fi - if [ -n "$4" ]; then match2=(-H "$4") ; else match2=() ; fi - if [ -n "$4" ]; then match2_prn=" ${F}$4${N}" ; else match2_prn="" ; fi - echo -n -e "📥 ${L}PUT ${W}$1${N}${match1_prn}${match2_prn} > " - - local tmpfile - tmpfile=$(mktemp) -# curl -v -i -s -X PUT "$URL/$1" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - curl -i -s -X PUT "$URL/$1" -H "Authorization: Bearer ${TOKEN}" "${match1[@]}" "${match2[@]}" -H "Content-Type: application/json" -d "$2" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} - -delete() { - echo -n -e "📥 ${L}DELETE ${W}$1${N} > " - local tmpfile - tmpfile=$(mktemp) - curl -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" -# curl -v -i -s -X DELETE "$URL/$1" -H "Authorization: Bearer ${TOKEN}" | tr -d '\r' > "$tmpfile" - api ${tmpfile} -} diff --git a/foundations/hulypulse/scripts/test_pulse.sh b/foundations/hulypulse/scripts/test_pulse.sh deleted file mode 100755 index 808173ffe9..0000000000 --- a/foundations/hulypulse/scripts/test_pulse.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN=$(./token.sh claims.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS/AnyKey" - -echo "================> LIST" - put "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" "value1" - put "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" "value2" - get "00000000-0000-0000-0000-000000000001/Huome2" - delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey1" - delete "00000000-0000-0000-0000-000000000001/Huome2/MyKey2" - -echo "================> WRONG UUID" - get "WrongUUID/TESTS/AnyKey" - -echo "================> INSERT If-None-Match" - - echo "-- Expected Error: 400 Bad Request (If-None-Match may be only *)" - put ${ZP} "enother text" "If-None-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - delete ${ZP} - - echo "-- Expected OK: 201 Created (key was not exist)" - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (any hash)" - put ${ZP} "enother version2" "If-Match" "*" - get ${ZP} - - echo "-- Expected Error: 412 Precondition Failed (wrong hash)" - put ${ZP} "enother version3" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - delete ${ZP} - - echo "-- Expected Error: 412 Precondition Failed (any hash not found)" - put ${ZP} "enother version2" "If-Match" "*" - -echo "================> UPSERT (Expected OK)" - put ${ZP} "my value" - get ${ZP} - put ${ZP} "my new value" - get ${ZP} - -exit diff --git a/foundations/hulypulse/scripts/test_pulse_system.sh b/foundations/hulypulse/scripts/test_pulse_system.sh deleted file mode 100755 index be2c78fa0d..0000000000 --- a/foundations/hulypulse/scripts/test_pulse_system.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib.sh - -TOKEN_OK=$(./token.sh claims.json) -TOKEN_SYSTEM=$(./token.sh claims_system.json) -TOKEN_WRONG=$(./token.sh claims_wrong_ws.json) -ZP="00000000-0000-0000-0000-000000000001/TESTS/JWT_tests" - -echo "================> SYSTEM change - OK" - TOKEN=${TOKEN_SYSTEM} - # delete ${ZP} - put ${ZP} "system value" - -echo "================> USER read/change - OK" - TOKEN=${TOKEN_OK} - get ${ZP} - put ${ZP} "user value" - -echo "================> WRONG USER read/change - ERROR" - TOKEN=${TOKEN_WRONG} - get ${ZP} - put ${ZP} "wrong user value" - -echo "================> SYSTEM read/change - OK" - TOKEN=${TOKEN_SYSTEM} - get ${ZP} - put ${ZP} "system value 2" - -echo "================> USER read - OK" - TOKEN=${TOKEN_OK} - get ${ZP} - -exit diff --git a/foundations/hulypulse/scripts/token.sh b/foundations/hulypulse/scripts/token.sh deleted file mode 100755 index 78ae2ef971..0000000000 --- a/foundations/hulypulse/scripts/token.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -CONFIG_PATH="../src/config/default.toml" -SECRET=$(grep '^token_secret' "$CONFIG_PATH" | sed -E 's/.*=\s*"(.*)"/\1/') # " - -if [ -z "$SECRET" ]; then - echo "❌No token_secret in $CONFIG_PATH" - exit 1 -fi - -claims=$1 # "claims.json" - -#TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign claims.json) -TOKEN=$(echo -n "${SECRET}" | jwt -alg HS256 -key - -sign ${claims}) - -echo "$TOKEN" diff --git a/foundations/hulypulse/scripts/typing-test.sh b/foundations/hulypulse/scripts/typing-test.sh deleted file mode 100755 index 4c7990b504..0000000000 --- a/foundations/hulypulse/scripts/typing-test.sh +++ /dev/null @@ -1,151 +0,0 @@ -#!/bin/bash - -clear -source ./pulse_lib_huly.sh - -#TOKEN=$(./token.sh claims.json) -TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHRyYSI6e30sImFjY291bnQiOiI1NjBlNDRiYS1jM2ZhLTRmMzUtYjQxYi00NWMzY2FhYWZiZTAiLCJ3b3Jrc3BhY2UiOiI4NTk5ZWViZS0xZDEwLTRhNDYtYTIxZS04OWNkMzI4YjRmZmEifQ.rTmKG5ulwTONs6KPfmBOLnY6BaXfwP1kma_Pvay-pz8" -echo ${TOKEN} - -#exit -#ZP="00000000-0000-0000-0000-000000000001/TESTS" - -#put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/chunter:space:General/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"chunter:space:General\"}" "HULY-TTL: 120" - -put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68874fd619a81293751d001e" "{\"personId\":\"68874fd619a81293751d001e\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" -put "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/68e2585a62753bede49ee803" "{\"personId\":\"68e2585a62753bede49ee803\",\"objectId\":\"68e259323d9a9ae45c7dd0ea\"}" "HULY-TTL: 15" -# "8599eebe-1d10-4a46-a21e-89cd328b4ffa/typing/68e259323d9a9ae45c7dd0ea/" - -#put "00000000-0000-0000-0000-000000000001/TESTS/val1" "value" "HULY-TTL: 1" -#put "00000000-0000-0000-0000-000000000001/TESTS/val2" "value" "HULY-TTL: 120" -#put "00000000-0000-0000-0000-000000000001/TESTS/val3" "value" "HULY-TTL: 1" -#get "00000000-0000-0000-0000-000000000001/TESTS/" -#sleep 2 -# get "00000000-0000-0000-0000-000000000001/TESTS/val2" -#get "00000000-0000-0000-0000-000000000001/TESTS/" - - - -exit - -http://huly.local:8099/status - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - -#exit - delete "00000000-0000-0000-0000-000000000001/TESTS" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: *" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value" - delete "00000000-0000-0000-0000-000000000001/TESTS" "If-Match: dd358c74cb9cb897424838fbcb69c933" - -#exit - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_2" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2/$/secret" "Value_secret" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS/" - -#exit - - delete "0000000/TESTS" - delete ${ZP} - put ${ZP} "Value_1" "HULY-TTL: 2" - delete ${ZP} - -echo "--------- authorization_test ----------" -TOKEN="" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_system.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims_wrong_ws.json) - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" -TOKEN=$(./token.sh claims.json) - put "00000000-0000-0000-0000-000000000002/TESTS" "Value_1" "HULY-TTL: 2" - - - -echo "--------- if-match ----------" - - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/1" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/2" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3$" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/4" "Value_1" "HULY-TTL: 2" - get "00000000-0000-0000-0000-000000000001/TESTS" - get "00000000-0000-0000-0000-000000000001/TESTS/" - get "00000000-0000-0000-0000-000000000001/TESTS/3/secret$/" - - -echo "--------- Deprecated symbols ----------" - - put "00000000-0000-0000-0000-000000000001/'TESTS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TES?TS" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS*" "Value_1" "HULY-TTL: 2" - put "00000000-0000-0000-0000-000000000001/TESTS/" "Value_1" "HULY-TTL: 2" - -echo "--------- if-match ----------" - - delete ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 1" "If-Match: *" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 1" - get ${ZP} - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_3" "HULY-TTL: 1" "If-Match: dd358c74cb9cb897424838fbcb69c933" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_4" "HULY-TTL: 1" "If-Match: *" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_5" "HULY-TTL: 1" "If-Match: c7bcabf6b98a220f2f4888a18d01568d" - put "00000000-0000-0000-0000-000000000001/TESTS" "Value_6" "HULY-TTL: 1" "If-None-Match: *" - -echo "-- Expected OK: 201 Created (key was not exist)" - - put ${ZP} "enother text" "If-None-Match" "*" - - put ${ZP} "some text" - echo "-- Expected Error: 412 Precondition Failed (key was exist)" - put ${ZP} "enother text" "If-None-Match" "*" - -echo "================> UPDATE PUT If-Match" - - get ${ZP} - - echo "-- Expected OK: 204 No Content (right hash)" - put ${ZP} "some text" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - get ${ZP} - - echo "-- Expected OK: 204 No Content (hash still right)" - put ${ZP} "enother version" "If-Match" "552e21cd4cd9918678e3c1a0df491bc3" - - - - - - -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_1" "HULY-TTL: 3" -echo "sleep 1 sec" -sleep 1 -get "00000000-0000-0000-0000-000000000001/TESTS" -echo "sleep 3 sec" -sleep 2 -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- delete ----------" -put "00000000-0000-0000-0000-000000000001/TESTS" "Value_2" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001/TESTS" -delete "00000000-0000-0000-0000-000000000001/TESTS" -get "00000000-0000-0000-0000-000000000001/TESTS" - -echo "--------- prefix ----------" -put "00000000-0000-0000-0000-000000000001/TESTS1" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/TESTS2" "Value_1" "HULY-TTL: 3" -put "00000000-0000-0000-0000-000000000001/HREST2" "Value_1" "HULY-TTL: 3" -get "00000000-0000-0000-0000-000000000001?prefix=TES" -sleep 1 -get "00000000-0000-0000-0000-000000000001?prefix=" - -exit diff --git a/foundations/hulypulse/src/GOT.sh b/foundations/hulypulse/src/GOT.sh deleted file mode 100755 index 9497816776..0000000000 --- a/foundations/hulypulse/src/GOT.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -clear - -redis-cli set lleo value -redis-cli del lleo -redis-cli set ttlkey 1 EX 2 -# подожди ~2 сек → должно прийти expired diff --git a/foundations/hulypulse/src/config.rs b/foundations/hulypulse/src/config.rs deleted file mode 100644 index 712d6b9721..0000000000 --- a/foundations/hulypulse/src/config.rs +++ /dev/null @@ -1,101 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::{path::Path, sync::LazyLock}; - -#[cfg(feature = "auth")] -use secrecy::SecretString; - -use serde::Deserialize; -use serde_with::StringWithSeparator; -use serde_with::formats::CommaSeparator; -use serde_with::serde_as; -use url::Url; - -use config::FileFormat; - -#[derive(Deserialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum RedisMode { - Sentinel, - Direct, -} - -#[derive(Deserialize, Debug, PartialEq, strum::Display)] -#[serde(rename_all = "lowercase")] -pub enum BackendType { - Memory, - Redis, -} - -fn default_backend() -> BackendType { - BackendType::Redis -} - -#[serde_as] -#[derive(Deserialize, Debug)] -pub struct Config { - pub bind_port: u16, - pub bind_host: String, - - #[cfg(feature = "auth")] - pub token_secret: SecretString, - - #[serde(default = "default_backend")] - pub backend: BackendType, - - #[serde_as(as = "StringWithSeparator::")] - pub redis_urls: Vec, - pub redis_password: String, - pub redis_mode: RedisMode, - pub redis_service: String, - - pub max_ttl: usize, - pub max_size: Option, - - pub heartbeat_timeout: u64, - pub ping_timeout: u64, - - #[cfg(feature = "auth")] - pub policy_file: Option, - - pub loglevel: String, -} - -pub static CONFIG: LazyLock = LazyLock::new(|| { - const DEFAULTS: &str = std::include_str!("config/default.toml"); - - let mut builder = - config::Config::builder().add_source(config::File::from_str(DEFAULTS, FileFormat::Toml)); - - let path = Path::new("etc/config.toml"); - - if path.exists() { - builder = builder.add_source(config::File::with_name(path.as_os_str().to_str().unwrap())); - } - - let settings = builder - .add_source(config::Environment::with_prefix("HULY")) - .build() - .and_then(|c| c.try_deserialize::()); - - match settings { - Ok(settings) => settings, - Err(error) => { - eprintln!("configuration error: {error}"); - std::process::exit(1); - } - } -}); diff --git a/foundations/hulypulse/src/config/default.toml b/foundations/hulypulse/src/config/default.toml deleted file mode 100644 index 06551e18ea..0000000000 --- a/foundations/hulypulse/src/config/default.toml +++ /dev/null @@ -1,23 +0,0 @@ -bind_port = 8099 -bind_host = "0.0.0.0" - -token_secret = "secret" - -backend = "redis" -redis_urls = "redis://huly.local:6379" -redis_password = "" -redis_mode = "direct" -redis_service = "mymaster" - -max_ttl = 3600 - -heartbeat_timeout = 90 -ping_timeout = 30 - -loglevel = "INFO" - -# optional settings -# max_size = 100 - -# permit_file = "/home/user/hulipulse/permit.rego" - diff --git a/foundations/hulypulse/src/db.rs b/foundations/hulypulse/src/db.rs deleted file mode 100644 index 7d2ce38c34..0000000000 --- a/foundations/hulypulse/src/db.rs +++ /dev/null @@ -1,260 +0,0 @@ -use std::sync::Arc; - -use crate::hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}; -use crate::memory::{ - MemoryBackend, memory_delete, memory_info, memory_list, memory_read, memory_save, -}; -use crate::redis::{redis_delete, redis_info, redis_list, redis_read, redis_save}; -use redis::aio::ConnectionManager; -use serde::Serialize; -use tokio::sync::RwLock; - -#[derive(Debug)] -pub enum DbError { - Redis(redis::RedisError), - Message(String), -} - -pub type DbResult = Result; - -impl std::fmt::Display for DbError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Redis(err) => write!(f, "{err}"), - Self::Message(msg) => write!(f, "{msg}"), - } - } -} - -impl std::error::Error for DbError {} - -impl From for DbError { - fn from(value: redis::RedisError) -> Self { - Self::Redis(value) - } -} - -#[derive(Debug, Serialize)] -pub struct DbArray { - pub key: String, - pub data: String, - pub ttl: u64, // sec to expire TTL - pub etag: String, // md5 hash (data) -} - -#[derive(serde::Serialize)] -pub enum Ttl { - Sec(usize), // EX - At(u64), // EXAT (timestamp in seconds) -} - -#[derive(Debug)] -pub enum SaveMode { - Upsert, // default: set or overwrite - Insert, // only if not exists (NX) - Update, // only if exists (XX) - Equal(String), // only if md5 matches provided -} - -pub fn error(code: u16, msg: impl Into) -> DbResult { - Err(DbError::Message(format!("{}: {}", code, msg.into()))) -} - -/// Check for redis-deprecated symbols -pub fn deprecated_symbol(s: &str) -> bool { - s.chars().any(|c| { - matches!( - c, - '*' | '?' | '[' | ']' | '\\' | '\0'..='\x1F' | '\x7F' | '"' | '\'' - ) - }) -} - -pub fn deprecated_symbol_error(s: &str) -> DbResult<()> { - if deprecated_symbol(s) { - error(412, "Deprecated symbol in key") - } else { - Ok(()) - } -} - -#[derive(Clone)] -enum DbBackend { - Redis(ConnectionManager), - Memory { - db: MemoryBackend, - hub: Arc>, - }, -} - -#[derive(Clone)] -pub struct Db { - backend: DbBackend, -} - -impl Db { - pub fn new_redis(db: ConnectionManager) -> Self { - Self { - backend: DbBackend::Redis(db), - } - } - - pub fn new_memory(db: MemoryBackend, hub: Arc>) -> Self { - Self { - backend: DbBackend::Memory { db, hub }, - } - } - - pub fn mode(&self) -> &'static str { - match &self.backend { - DbBackend::Redis(_) => "redis", - DbBackend::Memory { .. } => "memory", - } - } - - pub async fn info(&self) -> DbResult { - match &self.backend { - DbBackend::Memory { db, .. } => memory_info(db).await, - DbBackend::Redis(conn) => { - let mut c = conn.clone(); - redis_info(&mut c).await - } - } - } - - pub async fn list(&self, key: &str) -> DbResult> { - match &self.backend { - DbBackend::Memory { db, .. } => memory_list(db, key).await, - DbBackend::Redis(conn) => { - let mut c = conn.clone(); - redis_list(&mut c, key).await - } - } - } - - pub async fn read(&self, key: &str) -> DbResult> { - match &self.backend { - DbBackend::Memory { db, .. } => memory_read(db, key).await, - DbBackend::Redis(conn) => { - let mut c = conn.clone(); - redis_read(&mut c, key).await - } - } - } - - pub async fn save>( - &self, - key: &str, - value: V, - ttl: Option, - mode: Option, - ) -> DbResult<()> { - match &self.backend { - DbBackend::Memory { db, hub } => { - memory_save(db, key, value.as_ref(), ttl, mode).await?; - let value_str = std::str::from_utf8(value.as_ref()) - .ok() - .map(|s| s.to_string()); - broadcast_event( - hub, - RedisEvent { - message: RedisEventAction::Set, - key: key.to_string(), - }, - value_str, - ) - .await; - Ok(()) - } - DbBackend::Redis(conn) => { - let mut c = conn.clone(); - redis_save(&mut c, key, value.as_ref(), ttl, mode).await - } - } - } - - pub async fn delete(&self, key: &str, mode: Option) -> DbResult { - match &self.backend { - DbBackend::Memory { db, hub } => { - let deleted = memory_delete(db, key, mode).await?; - if deleted { - broadcast_event( - hub, - RedisEvent { - message: RedisEventAction::Del, - key: key.to_string(), - }, - None, - ) - .await; - } - Ok(deleted) - } - DbBackend::Redis(conn) => { - let mut c = conn.clone(); - redis_delete(&mut c, key, mode).await - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::hub_service::HubState; - use crate::memory::MemoryBackend; - use std::sync::Arc; - use tokio::sync::RwLock; - - fn memory_db() -> Db { - let hub = Arc::new(RwLock::new(HubState::default())); - let backend = MemoryBackend::new(); - Db::new_memory(backend, hub) - } - - #[tokio::test] - async fn memory_db_mode_and_crud_work() { - let db = memory_db(); - assert_eq!(db.mode(), "memory"); - - db.save("workspace/tests/key1", b"hello", Some(Ttl::Sec(60)), None) - .await - .expect("save should succeed"); - - let item = db - .read("workspace/tests/key1") - .await - .expect("read should succeed") - .expect("key should exist"); - assert_eq!(item.data, "hello"); - - let list = db - .list("workspace/tests/") - .await - .expect("list should succeed"); - assert_eq!(list.len(), 1); - assert_eq!(list[0].key, "workspace/tests/key1"); - - let deleted = db - .delete("workspace/tests/key1", None) - .await - .expect("delete should succeed"); - assert!(deleted); - assert!( - db.read("workspace/tests/key1") - .await - .expect("read should succeed") - .is_none() - ); - } - - #[tokio::test] - async fn memory_db_status_reports_memory_backend() { - let hub = Arc::new(RwLock::new(HubState::default())); - let db = Db::new_memory(MemoryBackend::new(), hub.clone()); - - let info = hub.read().await.info_json(&db).await; - assert_eq!(info["backend"], "memory"); - assert_eq!(info["status"], "OK"); - } -} diff --git a/foundations/hulypulse/src/handlers_http.rs b/foundations/hulypulse/src/handlers_http.rs deleted file mode 100644 index 9e1fe427c4..0000000000 --- a/foundations/hulypulse/src/handlers_http.rs +++ /dev/null @@ -1,282 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use serde::Deserialize; -use std::str::FromStr; -use tracing::*; - -use actix_web::{ - Error, HttpResponse, - error::ParseError, - http::header::{self, HeaderName, HeaderValue, IfMatch, IfNoneMatch, TryIntoHeaderValue}, - web, -}; - -#[cfg(feature = "auth")] -use actix_web::HttpRequest; - -use crate::db::{Db, SaveMode, Ttl}; - -#[cfg(feature = "auth")] -use crate::workspace_owner::test_rego_http; - -pub fn map_redis_error(err: impl std::fmt::Display) -> Error { - let msg = err.to_string(); - - let detail = msg - .split(" - ExtensionError: ") - .nth(1) - .unwrap_or(msg.as_str()); - if let Some((code, text)) = detail.split_once(": ") { - let text = format!("{code} {text}"); - return match code { - "400" => actix_web::error::ErrorBadRequest(text), - "404" => actix_web::error::ErrorNotFound(text), - "412" => actix_web::error::ErrorPreconditionFailed(text), - "500" => actix_web::error::ErrorInternalServerError(text), - _ => actix_web::error::ErrorInternalServerError("unexpected error"), - }; - } - actix_web::error::ErrorInternalServerError("internal error") -} - -#[derive(Deserialize, Debug)] -pub struct PathParams { - key: String, - workspace: String, -} - -pub struct TtlSecsHeader(Option); -pub struct TtlExpiresAtHeader(Option); - -/// list -pub async fn list( - #[cfg(feature = "auth")] req: HttpRequest, - path: web::Path, - db: web::Data, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "list request"); - - #[cfg(feature = "auth")] - { - if !test_rego_http(req, "List", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - } - - let entries = db.list(&key).await.map_err(map_redis_error)?; - Ok(HttpResponse::Ok().json(entries)) -} - -/// get -pub async fn get( - #[cfg(feature = "auth")] req: HttpRequest, - path: web::Path, - db: web::Data, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "get request"); - - #[cfg(feature = "auth")] - { - if !test_rego_http(req, "Get", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - } - - let entry_opt = db.read(&key).await.map_err(map_redis_error)?; - let resp = match entry_opt { - Some(entry) => HttpResponse::Ok() - .insert_header((header::ETAG, entry.etag.clone())) - .json(entry), - None => HttpResponse::NotFound().body("empty"), - }; - Ok(resp) -} - -/// put -pub async fn put( - #[cfg(feature = "auth")] req: HttpRequest, - path: web::Path, - body: web::Bytes, - db: web::Data, - (secs, expires_at): ( - Result, ParseError>, - Result, ParseError>, - ), - (if_match, if_none_match): ( - web::Header, - web::Header, - ), -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "put request"); - - #[cfg(feature = "auth")] - { - if !test_rego_http(req, "Put", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - } - - // TTL logic - let ttl = match (secs?.into_inner().0, expires_at?.into_inner().0) { - (None, None) => None, - (Some(secs), None) => Some(Ttl::Sec(secs)), - (None, Some(timestamp)) => Some(Ttl::At(timestamp)), - _ => { - return Err(actix_web::error::ErrorBadRequest("Multiple ttl specified")); - } - }; - - // MODE logic - let mode = match (if_match.into_inner(), if_none_match.into_inner()) { - (IfMatch::Items(items), IfNoneMatch::Items(nitems)) - if items.is_empty() && nitems.is_empty() => - { - SaveMode::Upsert - } - (IfMatch::Any, IfNoneMatch::Items(nitems)) if nitems.is_empty() => SaveMode::Update, - (IfMatch::Items(etags), IfNoneMatch::Items(nitems)) - if etags.len() == 1 && nitems.is_empty() => - { - SaveMode::Equal(etags[0].tag().to_string()) - } - (IfMatch::Items(items), IfNoneMatch::Any) if items.is_empty() => SaveMode::Insert, - _ => { - return Err(actix_web::error::ErrorBadRequest( - "Unsupported combination of If-Match and If-None-Match", - )); - } - }; - - db.save(&key, &body[..], ttl, Some(mode)) - .await - .map_err(map_redis_error)?; - Ok(HttpResponse::Ok().body("DONE")) -} - -/// delete -pub async fn delete( - #[cfg(feature = "auth")] req: HttpRequest, - path: web::Path, - db: web::Data, - if_match: web::Header, -) -> Result { - let params = path.into_inner(); - let key = format!("{}/{}", ¶ms.workspace, ¶ms.key); - trace!(key, "delete request"); - - #[cfg(feature = "auth")] - { - if !test_rego_http(req, "Delete", &key) { - return Err(actix_web::error::ErrorForbidden("forbidden")); - } - } - - // MODE logic - let mode = match if_match.into_inner() { - IfMatch::Any => SaveMode::Update, - IfMatch::Items(etags) => { - if etags.len() == 1 { - SaveMode::Equal(etags[0].tag().to_string()) - } else if etags.is_empty() { - SaveMode::Upsert - } else { - return Err(actix_web::error::ErrorBadRequest( - "Multiple If-Match are not supported", - )); - } - } - }; - - let deleted = db.delete(&key, Some(mode)).await.map_err(map_redis_error)?; - let response = match deleted { - true => HttpResponse::NoContent().finish(), - false => HttpResponse::NotFound().body("not found"), - }; - - Ok(response) -} - -impl TryIntoHeaderValue for TtlSecsHeader { - type Error = std::convert::Infallible; - - fn try_into_value(self) -> Result { - Ok(self - .0 - .map(HeaderValue::from) - .unwrap_or(HeaderValue::from_static(""))) - } -} - -impl header::Header for TtlSecsHeader { - fn name() -> HeaderName { - HeaderName::from_static("huly-ttl") - } - - fn parse(msg: &M) -> Result { - let mut values = msg.headers().get_all(Self::name()); - let val = if let Some(value) = values.next() { - Some( - usize::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) - .map_err(|_| ParseError::Header)?, - ) - } else { - None - }; - if values.next().is_some() { - return Err(ParseError::TooLarge); - } - Ok(Self(val)) - } -} - -impl TryIntoHeaderValue for TtlExpiresAtHeader { - type Error = std::convert::Infallible; - - fn try_into_value(self) -> Result { - Ok(self - .0 - .map(HeaderValue::from) - .unwrap_or(HeaderValue::from_static(""))) - } -} - -impl header::Header for TtlExpiresAtHeader { - fn name() -> HeaderName { - HeaderName::from_static("huly-expire-at") - } - - fn parse(msg: &M) -> Result { - let mut values = msg.headers().get_all(Self::name()); - let val = if let Some(value) = values.next() { - Some( - u64::from_str(value.to_str().map_err(|_| ParseError::Header)?.trim()) - .map_err(|_| ParseError::Header)?, - ) - } else { - None - }; - if values.next().is_some() { - return Err(ParseError::TooLarge); - } - Ok(Self(val)) - } -} diff --git a/foundations/hulypulse/src/handlers_ws.rs b/foundations/hulypulse/src/handlers_ws.rs deleted file mode 100644 index af6be725f0..0000000000 --- a/foundations/hulypulse/src/handlers_ws.rs +++ /dev/null @@ -1,494 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use futures_util::StreamExt; - -use futures::future::{AbortHandle, Abortable}; - -use actix_web::{Error, HttpRequest, HttpResponse, web}; - -#[cfg(feature = "auth")] -use actix_web::HttpMessage; - -use serde::Deserialize; -use serde_json::{Value, json}; -use std::sync::Arc; -use tokio::sync::RwLock; - -use crate::{ - db::{Db, SaveMode, Ttl}, - hub_service::{HubState, SessionId, new_session_id}, -}; - -#[cfg(feature = "lopt")] -use crate::hub_service::send_to_name; - -#[cfg(feature = "auth")] -use crate::workspace_owner::check_workspace_core; - -#[cfg(feature = "auth")] -use crate::workspace_owner::test_rego_claims; - -use strum::AsRefStr; - -#[derive(Deserialize, Debug, AsRefStr)] -#[serde(rename_all = "lowercase", tag = "type")] -pub enum WsCommand { - #[cfg(feature = "lopt")] - Personal { - to: String, - correlation: String, - data: String, - }, - - #[cfg(feature = "lopt")] - Answer { - to: String, - correlation: String, - data: String, - }, - - Put { - #[serde(default = "default_corr")] - correlation: String, - key: String, - data: String, - - #[serde(rename = "expiresAt")] - #[serde(default)] - expires_at: Option, - - #[serde(rename = "TTL")] - #[serde(default)] - ttl: Option, - - #[serde(rename = "ifMatch")] - #[serde(default)] - if_match: Option, - - #[serde(rename = "ifNoneMatch")] - #[serde(default)] - if_none_match: Option, - }, - - Delete { - #[serde(default = "default_corr")] - correlation: String, - key: String, - - #[serde(rename = "ifMatch")] - #[serde(default)] - if_match: Option, - }, - - Get { - #[serde(default = "default_corr")] - correlation: String, - key: String, - }, - - List { - #[serde(default = "default_corr")] - correlation: String, - key: String, - }, - - Sub { - #[serde(default = "default_corr")] - correlation: String, - key: String, - }, - - Unsub { - #[serde(default = "default_corr")] - correlation: String, - key: String, - }, - - Sublist { - #[serde(default = "default_corr")] - correlation: String, - }, - - Info { - #[serde(default = "default_corr")] - correlation: String, - }, -} - -fn default_corr() -> String { - "1".to_string() -} - -#[cfg(feature = "auth")] -use hulyrs::services::jwt::Claims; - -#[cfg(feature = "auth")] -async fn wrong_workspace( - claims: &Option, - key: &str, - correlation: &String, - session: &mut actix_ws::Session, -) -> bool { - if let Err(e) = check_workspace_core(claims.clone(), key) { - result_err(e, correlation, session).await; - return true; - } - false -} - -async fn result>( - result: T, - correlation: &String, - session: &mut actix_ws::Session, -) { - let _ = session - .text(json!({ "correlation": correlation, "result": result.into()}).to_string()) - .await; -} - -async fn result_err(err: impl Into, correlation: &String, session: &mut actix_ws::Session) { - let _ = session - .text(json!({ "correlation": correlation, "error": err.into()}).to_string()) - .await; -} - -async fn handle_command( - ws: &mut actix_ws::Session, - cmd: WsCommand, - db: &Db, - hub_state: &Arc>, - #[cfg(feature = "auth")] claims: Option, - session_id: SessionId, - #[cfg(feature = "lopt")] client_name: &str, -) { - match cmd { - #[cfg(feature = "lopt")] - WsCommand::Personal { - to, - correlation, - data, - } => { - use crate::hub_service::send_to_name; - - tracing::debug!("PERSONAL from {} to {}", &client_name, &to); - let payload = - json!({ "personal": client_name, "correlation": correlation, "data": data }); - if !send_to_name(hub_state, &to, payload).await { - tracing::debug!("PERSONAL send from [{}] to [{}] failed", &client_name, &to); - result_err("failed", &correlation, ws).await; - } - } - - #[cfg(feature = "lopt")] - WsCommand::Answer { - to, - correlation, - data, - } => { - tracing::debug!("ANSWER from {} to {}", &client_name, &to); - let payload = json!({ "correlation": correlation, "data": data }); - if !send_to_name(hub_state, &to, payload).await { - tracing::debug!("PERSONAL send_to failed: no such session {}", to); - } - } - - // INFO - WsCommand::Info { correlation } => { - tracing::debug!("INFO"); - let info = hub_state.read().await.info_json(db).await; - result(info, &correlation, ws).await - } - - // PUT - WsCommand::Put { - key, - data, - expires_at, - ttl, - if_match, - if_none_match, - correlation, - } => { - tracing::debug!("PUT {} = {}", &key, &data); - - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - // TTL logic - let real_ttl = if let Some(secs) = ttl { - Some(Ttl::Sec(secs as usize)) - } else { - expires_at.map(Ttl::At) - }; - - // SaveMode logic - let mut mode = Some(SaveMode::Upsert); - if let Some(s) = if_match { - if s == "*" { - mode = Some(SaveMode::Update); - } else { - mode = Some(SaveMode::Equal(s)); - } - } else if let Some(s) = if_none_match { - if s == "*" { - mode = Some(SaveMode::Insert); - } else { - result_err("ifNoneMatch must contain only '*'", &correlation, ws).await; - return; - } - } - - match db.save(&key, &data, real_ttl, mode).await { - Ok(_) => result("OK", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Delete { - key, - correlation, - if_match, - } => { - tracing::debug!("DELETE {}", &key); // correlation:{:?} , &correlation - - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - // MODE logic - let mut mode = Some(SaveMode::Upsert); - if let Some(s) = if_match { - if s == "*" { - // `If-Match: *` - return error if not exist - mode = Some(SaveMode::Update); - } else { - // `If-Match: ` - delete only if current - mode = Some(SaveMode::Equal(s)); - } - } - - // Delete - match db.delete(&key, mode).await { - Ok(true) => result("OK", &correlation, ws).await, - Ok(false) => result_err("not found", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Get { key, correlation } => { - tracing::debug!("GET {}", &key); - - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - match db.read(&key).await { - Ok(Some(data)) => match serde_json::to_value(&data) { - Ok(v) => result(v, &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - }, - Ok(None) => result_err("not found", &correlation, ws).await, - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::List { key, correlation } => { - tracing::debug!("LIST {:?}", &key); - - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - match db.list(&key).await { - Ok(data) => { - let values: Vec = data.into_iter().map(|item| json!(item)).collect(); - result(values, &correlation, ws).await; - } - Err(e) => result_err(e.to_string(), &correlation, ws).await, - } - } - - WsCommand::Sub { key, correlation } => { - tracing::debug!("SUB {}", &key); - - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - hub_state.write().await.subscribe(session_id, key); - result("OK", &correlation, ws).await; - } - - WsCommand::Unsub { key, correlation } => { - tracing::debug!("UNSUB {}", &key); - if key == "*" { - hub_state.write().await.unsubscribe_all(session_id); - result("OK", &correlation, ws).await; - } else { - #[cfg(feature = "auth")] - if wrong_workspace(&claims, &key, &correlation, ws).await { - return; - } - - hub_state.write().await.unsubscribe(session_id, key); - result("OK", &correlation, ws).await; - } - } - - WsCommand::Sublist { correlation } => { - tracing::debug!("SUBLIST"); - // w/o Check workspace! - let keys = hub_state.read().await.subscribe_list(session_id); - result(keys, &correlation, ws).await; - } // End of commands - } -} -// } - -pub async fn handler( - req: HttpRequest, - payload: web::Payload, - db: web::Data, - hub_state: web::Data>>, -) -> Result { - #[cfg(feature = "auth")] - let claims = Some( - req.extensions() - .get::() - .expect("Missing claims") - .to_owned(), - ); - - #[cfg(feature = "lopt")] - let client_name = req - .match_info() - .get("client_name") - .unwrap_or("") - .to_string(); - - let (response, mut session, mut msg_stream) = actix_ws::handle(&req, payload)?; - - let session_id = new_session_id(); - - let (abort_handle, abort_reg) = AbortHandle::new_pair(); - - hub_state.write().await.connect( - session_id, - session.clone(), - abort_handle, - #[cfg(feature = "lopt")] - client_name.clone(), - ); - tracing::debug!("WebSocket connected: {}", session_id); - - actix_web::rt::spawn(Abortable::new( - async move { - while let Some(Ok(msg)) = msg_stream.next().await { - if !matches!(msg, actix_ws::Message::Pong(_)) { - tracing::debug!("WebSocket message: {:?}", msg); - } - - // renew heartbeat to unixtime (all messages is activity, including "ping") - hub_state.write().await.renew_heartbeat(session_id); - - match msg { - actix_ws::Message::Ping(bytes) => { - session.pong(&bytes).await.ok(); - continue; - } - - actix_ws::Message::Pong(_) => { - continue; - } - - actix_ws::Message::Text(text) if text == "ping" => { - let _ = session.text("pong").await; - continue; - } - actix_ws::Message::Text(text) if text == "pong" => { - continue; - } - - actix_ws::Message::Text(text) => match serde_json::from_str::(&text) - { - Ok(cmd) => { - #[cfg(feature = "auth")] - { - let key = match &cmd { - WsCommand::Put { key, .. } - | WsCommand::Delete { key, .. } - | WsCommand::Get { key, .. } - | WsCommand::List { key, .. } - | WsCommand::Sub { key, .. } - | WsCommand::Unsub { key, .. } => key.as_str(), - // | WsCommand::Personal { key, .. } => key.as_str(), - _ => "", - }; - - if let Some(ref claim) = claims - && !test_rego_claims(claim, cmd.as_ref(), key) - { - let _ = session.text("Unauthorized: Rego policy").await; - break; - } - } - - handle_command( - &mut session, - cmd, - &db, - &hub_state, - #[cfg(feature = "auth")] - claims.clone(), - session_id, - #[cfg(feature = "lopt")] - &client_name, - ) - .await; - } - - Err(err) => { - let _ = session.text(format!("Invalid JSON: {err}")).await; - } - }, - - actix_ws::Message::Close(reason) => { - if let Err(e) = session.close(reason).await { - tracing::warn!("WS close error: {:?}", e); - } - break; - } - - _ => { - tracing::warn!("Unhandled WS message: {:?}", msg); - } - } - } - - hub_state.write().await.disconnect(session_id); - tracing::debug!("WebSocket disconnected by client: {}", session_id); - }, - abort_reg, - )); - - Ok(response) -} diff --git a/foundations/hulypulse/src/hub_service.rs b/foundations/hulypulse/src/hub_service.rs deleted file mode 100644 index 0327a883ab..0000000000 --- a/foundations/hulypulse/src/hub_service.rs +++ /dev/null @@ -1,307 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use crate::config::CONFIG; - -use serde::Serialize; -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering}; -use tokio::sync::RwLock; - -use serde_json::{Value, json}; - -use crate::db::Db; - -fn subscription_matches(sub_key: &str, key: &str) -> bool { - if sub_key == key { - return true; - } - if sub_key.ends_with('/') && key.starts_with(sub_key) { - let rest = &key[sub_key.len()..]; - return !rest.contains('$'); - } - false -} - -#[derive(Clone, Serialize, Debug)] -pub struct ServerMessage { - #[serde(flatten)] - pub event: RedisEvent, - #[serde(skip_serializing_if = "Option::is_none")] - pub value: Option, -} - -pub type SessionId = u64; -static NEXT_ID: AtomicU64 = AtomicU64::new(1); -pub fn new_session_id() -> SessionId { - NEXT_ID.fetch_add(1, Ordering::SeqCst) -} - -#[derive(Debug, Clone, Serialize)] -pub enum RedisEventAction { - Set, - Del, - Unlink, - Expired, - Other(String), -} - -#[derive(Debug, Clone, Serialize)] -pub struct RedisEvent { - // pub db: u32, - pub message: RedisEventAction, - pub key: String, -} - -#[derive(Default)] // Debug, -pub struct HubState { - sessions: HashMap, - subs: HashMap>, - heartbeats: HashMap, - serverping: HashMap, - abort_handles: HashMap, - // client_ids: HashMap, - #[cfg(feature = "lopt")] - name_by_session: HashMap, - #[cfg(feature = "lopt")] - session_by_name: HashMap, -} - -use futures::future::AbortHandle; - -impl HubState { - pub fn renew_heartbeat(&mut self, session_id: SessionId) { - if self.sessions.contains_key(&session_id) { - let now = std::time::Instant::now(); - self.heartbeats.insert(session_id, now); - self.serverping.insert(session_id, now); - } - } - - pub fn connect( - &mut self, - session_id: SessionId, - session: actix_ws::Session, - abort_handle: AbortHandle, - #[cfg(feature = "lopt")] client_name: String, - ) { - self.sessions.insert(session_id, session); - self.heartbeats - .insert(session_id, std::time::Instant::now()); - self.serverping - .insert(session_id, std::time::Instant::now()); - self.abort_handles.insert(session_id, abort_handle); - - #[cfg(feature = "lopt")] - self.name_by_session.insert(session_id, client_name.clone()); - #[cfg(feature = "lopt")] - self.session_by_name.insert(client_name, session_id); - } - - pub fn disconnect(&mut self, session_id: SessionId) { - self.sessions.remove(&session_id); - self.heartbeats.remove(&session_id); - self.serverping.remove(&session_id); - self.abort_handles.remove(&session_id); - self.subs.retain(|_, ids| { - ids.remove(&session_id); - !ids.is_empty() - }); - - #[cfg(feature = "lopt")] - if let Some(client_id) = self.name_by_session.remove(&session_id) { - self.session_by_name.remove(&client_id); - } - - tracing::debug!( - "hub.disconnected {}, all: {}", - session_id, - self.sessions.len() - ); - } - - pub fn subscribe(&mut self, session_id: SessionId, key: String) { - self.subs.entry(key).or_default().insert(session_id); - } - - pub fn unsubscribe(&mut self, session_id: SessionId, key: String) { - if let Some(set) = self.subs.get_mut(&key) { - set.remove(&session_id); - if set.is_empty() { - self.subs.remove(&key); - } - } - } - pub fn unsubscribe_all(&mut self, session_id: SessionId) { - self.subs.retain(|_, ids| { - ids.remove(&session_id); - !ids.is_empty() - }); - } - - pub fn subscribe_list(&self, session_id: SessionId) -> Vec { - self.subs - .iter() - .filter_map(|(key, ids)| { - if ids.contains(&session_id) { - Some(key.clone()) - } else { - None - } - }) - .collect() - } - - pub async fn info_json(&self, db: &Db) -> Value { - let info = db.info().await.unwrap_or_else(|_| "error".to_string()); - json!({ - "memory_info": info, - "backend": db.mode(), - "websockets": self.sessions.len(), - "subscriptions": self.subs.len(), - "heartbeats": self.heartbeats.len(), - "serverping": self.serverping.len(), - "loops": self.abort_handles.len(), - "loglevel": &CONFIG.loglevel, - "status": "OK", - "version": env!("CARGO_PKG_VERSION"), - }) - } - - pub fn recipients_for_key(&self, key: &str) -> Vec { - let mut out = Vec::new(); - for (sub_key, set) in &self.subs { - if subscription_matches(sub_key, key) { - for sid in set { - if let Some(r) = self.sessions.get(sid) { - out.push(r.clone()); - } - } - } - } - out - } -} - -// Send messages about new db events -pub async fn broadcast_event( - hub_state: &Arc>, - ev: RedisEvent, - value: Option, -) { - // Collect - let recipients: Vec = { hub_state.read().await.recipients_for_key(&ev.key) }; - if recipients.is_empty() { - return; - } - - // Send - let payload = ServerMessage { event: ev, value }; - for mut rcpt in recipients { - let json = serde_json::to_string(&payload).unwrap(); - let _ = rcpt.text(json).await; - } -} - -#[cfg(feature = "lopt")] -pub async fn send_to_name(hub_state: &Arc>, to: &str, payload: Value) -> bool { - let hub = hub_state.read().await; - - let to_sid = if let Some(&sid) = hub.session_by_name.get(to) { - sid - } else { - return false; - }; - - let Some(mut session) = hub.sessions.get(&to_sid).cloned() else { - return false; - }; - - session.text(payload.to_string()).await.is_ok() -} - -pub fn check_heartbeat(hub_state: Arc>) { - tokio::spawn(async move { - let mut ticker = tokio::time::interval(std::time::Duration::from_secs(2)); - loop { - ticker.tick().await; - - let now = std::time::Instant::now(); - let timelimit = now - std::time::Duration::from_secs(CONFIG.heartbeat_timeout); - let pinglimit = now - std::time::Duration::from_secs(CONFIG.ping_timeout); - - let hub = hub_state.read().await; - - let ids_expired: Vec = hub - .heartbeats - .iter() - .filter_map( - |(&sid, &last)| { - if last < timelimit { Some(sid) } else { None } - }, - ) - .collect(); - - let expired_sessions: Vec = ids_expired - .iter() - .filter_map(|sid| hub.sessions.get(sid).cloned()) - .collect(); - - let ids_to_ping: Vec = hub - .serverping - .iter() - .filter_map(|(&sid, &last_ping)| { - if last_ping < pinglimit { - Some(sid) - } else { - None - } - }) - .collect(); - - drop(hub); - - for session in &expired_sessions { - let _ = session.clone().close(None).await; - } - - if !ids_to_ping.is_empty() || !ids_expired.is_empty() { - let mut hub = hub_state.write().await; - - for sid in &ids_expired { - if let Some(abort_handle) = hub.abort_handles.get(sid) { - abort_handle.abort(); - } - tracing::debug!("WebSocket disconnected by timeout: {}", sid); - hub.disconnect(*sid); - } - - for sid in &ids_to_ping { - if ids_expired.contains(sid) { - continue; - } - - if let Some(session) = hub.sessions.get_mut(sid) { - let _ = session.ping(&[]).await; - } - hub.serverping.insert(*sid, now); - } - - drop(hub); - } - } - }); -} diff --git a/foundations/hulypulse/src/main.rs b/foundations/hulypulse/src/main.rs deleted file mode 100644 index 2c2afb0595..0000000000 --- a/foundations/hulypulse/src/main.rs +++ /dev/null @@ -1,256 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use actix_cors::Cors; -use actix_web::{ - App, HttpResponse, HttpServer, - middleware::{self}, - web::{self}, -}; -use std::sync::Arc; -use tokio::sync::RwLock; - -#[cfg(feature = "auth")] -use actix_web::{ - Error, HttpMessage, - body::MessageBody, - dev::{ServiceRequest, ServiceResponse}, - middleware::Next, - web::{Path, Query}, -}; - -#[cfg(feature = "auth")] -use hulyrs::services::jwt::{Claims, actix::ServiceRequestExt}; - -#[cfg(feature = "auth")] -use secrecy::ExposeSecret; - -#[cfg(feature = "auth")] -use tracing::*; - -#[cfg(feature = "auth")] -use uuid::Uuid; - -mod config; -mod handlers_http; -mod handlers_ws; - -mod memory; -mod redis; - -#[cfg(feature = "auth")] -mod workspace_owner; - -mod hub_service; -use hub_service::HubState; - -use config::CONFIG; - -mod db; -use crate::config::BackendType; -use crate::db::Db; -use crate::memory::MemoryBackend; - -use crate::hub_service::check_heartbeat; - -fn initialize_tracing() { - use tracing_subscriber::{filter::targets::Targets, prelude::*}; - - let level = match CONFIG.loglevel.as_str() { - "TRACE" => tracing::Level::TRACE, // full - "DEBUG" => tracing::Level::DEBUG, // for developer - "INFO" => tracing::Level::INFO, // normal - "WARN" => tracing::Level::WARN, // something went wrong - "ERROR" => tracing::Level::ERROR, // serious error - _ => tracing::Level::TRACE, - }; - - tracing_subscriber::registry() - .with( - Targets::new() - .with_target(env!("CARGO_BIN_NAME"), level) - .with_target("actix", tracing::Level::WARN), - ) - .with(tracing_subscriber::fmt::layer().compact()) - .init(); -} - -#[cfg(feature = "auth")] -async fn extract_claims( - mut request: ServiceRequest, - next: Next, -) -> Result, Error> { - #[derive(serde::Deserialize)] - struct QueryString { - token: Option, - } - - let query = request.extract::>().await?.into_inner(); - - let claims = if let Some(token) = query.token { - Claims::from_token(token, CONFIG.token_secret.expose_secret()).unwrap() - } else { - request.extract_claims(&CONFIG.token_secret)? - }; - request.extensions_mut().insert(claims); - - next.call(request).await -} - -#[cfg(feature = "auth")] -async fn check_workspace( - mut request: ServiceRequest, - next: Next, -) -> Result, Error> { - let workspace = Uuid::parse_str(&request.extract::>().await?); - let claims = request.extensions().get::().cloned().unwrap(); - - if claims.is_system() || Ok(claims.workspace) == workspace.clone().map(Some) { - next.call(request).await - } else { - warn!( - expected = ?claims.workspace, - actual = ?workspace, - "Unauthorized request, workspace mismatch" - ); - Err(actix_web::error::ErrorUnauthorized("Unauthorized")) - } -} - -#[actix_web::main] -async fn main() -> anyhow::Result<()> { - initialize_tracing(); - - tracing::info!("{}/{}", env!("CARGO_BIN_NAME"), env!("CARGO_PKG_VERSION")); - - // starting HubService - let hub_state = Arc::new(RwLock::new(HubState::default())); - - // starting heartbeat checker - check_heartbeat(hub_state.clone()); - - let db_backend = match &CONFIG.backend { - BackendType::Redis => { - let redis_client = redis::client().await?; - let db_connection = redis_client - .get_connection_manager() - .await - .inspect_err(|_e| { - tracing::error!( - "REDIS not found: {:?}", - &CONFIG - .redis_urls - .iter() - .map(|u| u.as_str()) - .collect::>() - .join(", ") - ); - })?; - tokio::spawn({ - let hub_state = hub_state.clone(); - async move { - crate::redis::receiver(redis_client, hub_state).await; - } - }); - Db::new_redis(db_connection) - } - BackendType::Memory => { - let db_connection = MemoryBackend::new(); - db_connection.spawn_ticker(hub_state.clone()); - Db::new_memory(db_connection, hub_state.clone()) - } - }; - - tracing::info!("DB mode: {}", db_backend.mode()); - - let socket = std::net::SocketAddr::new(CONFIG.bind_host.as_str().parse()?, CONFIG.bind_port); - - let url = format!("http://{}:{}", &CONFIG.bind_host, &CONFIG.bind_port); - tracing::info!("Server running at {}", &url); - tracing::info!("Log level: {}", &CONFIG.loglevel); - tracing::info!("API: {}/api", &url); - tracing::info!( - "WS: {}/ws", - format!("ws://{}:{}", &CONFIG.bind_host, &CONFIG.bind_port) - ); - tracing::info!("Status: {}/status", &url); - - let server = HttpServer::new(move || { - let cors = Cors::default() - .allow_any_origin() - .allow_any_method() - .allow_any_header() - .supports_credentials() - .max_age(3600); - - let api_scope = { - #[cfg(feature = "auth")] - { - web::scope("/api/{workspace}") - .wrap(middleware::from_fn(check_workspace)) - .wrap(middleware::from_fn(extract_claims)) - .route("/{key:.+/}", web::get().to(handlers_http::list)) - .route("/{key:.+}", web::get().to(handlers_http::get)) - .route("/{key:.+}", web::put().to(handlers_http::put)) - .route("/{key:.+}", web::delete().to(handlers_http::delete)) - } - - #[cfg(not(feature = "auth"))] - { - web::scope("/api/{workspace}") - .route("/{key:.+/}", web::get().to(handlers_http::list)) - .route("/{key:.+}", web::get().to(handlers_http::get)) - .route("/{key:.+}", web::put().to(handlers_http::put)) - .route("/{key:.+}", web::delete().to(handlers_http::delete)) - } - }; - - let ws_route = { - let r = web::get().to(handlers_ws::handler); - - #[cfg(feature = "auth")] - let r = r.wrap(middleware::from_fn(extract_claims)); - - r - }; - - App::new() - .app_data(web::Data::new(db_backend.clone())) - .app_data(web::Data::new(hub_state.clone())) - .wrap(middleware::Logger::default()) - .wrap(cors) - .service(api_scope) - .route("/ws/{client_name}", web::get().to(handlers_ws::handler)) - .route("/ws", ws_route) - .route( - "/status", - web::get().to({ - move |hub_state: web::Data>>, db_backend: web::Data| { - let hub_state = hub_state.clone(); - async move { - let info = hub_state.read().await.info_json(&db_backend).await; - Ok::<_, actix_web::Error>(HttpResponse::Ok().json(info)) - } - } - }), - ) - }) - .bind(socket)? - .run(); - - server.await?; - - Ok(()) -} diff --git a/foundations/hulypulse/src/memory.rs b/foundations/hulypulse/src/memory.rs deleted file mode 100644 index 70d6fe5dd5..0000000000 --- a/foundations/hulypulse/src/memory.rs +++ /dev/null @@ -1,330 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use crate::{ - config::CONFIG, - db::{DbArray, DbResult, SaveMode, Ttl, deprecated_symbol_error, error}, - hub_service::{HubState, RedisEvent, RedisEventAction, broadcast_event}, -}; -use std::{ - collections::HashMap, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; -use tokio::{ - sync::RwLock, - time::{self, Duration}, -}; - -#[derive(Debug, Clone)] -struct Entry { - data: String, - ttl: u8, -} - -#[derive(Clone, Default)] -pub struct MemoryBackend { - inner: Arc>>, - tick: Arc>, // counter -} - -impl MemoryBackend { - pub fn new() -> Self { - Self { - inner: Arc::new(RwLock::new(HashMap::new())), - tick: Arc::new(RwLock::new(0)), - } - } - - pub fn spawn_ticker(&self, hub_state: Arc>) { - let inner = self.inner.clone(); - let tick = self.tick.clone(); - - tokio::spawn(async move { - let mut ticker = time::interval(Duration::from_secs(1)); - loop { - ticker.tick().await; - - let current_tick = { - let mut t = tick.write().await; - *t = t.wrapping_add(1); - *t - }; - - let expired_keys: Vec = { - let map = inner.read().await; - map.iter() - .filter(|(_, v)| v.ttl == current_tick) - .map(|(k, _)| k.clone()) - .collect() - }; - - { - let mut map = inner.write().await; - for k in &expired_keys { - map.remove(k); - } - } - - for k in expired_keys { - broadcast_event( - &hub_state, - RedisEvent { - message: RedisEventAction::Expired, - key: k, - }, - None, - ) - .await; - } - } - }); - } -} - -/// memory_list(&backend, "prefix/") → Vec -pub async fn memory_list(backend: &MemoryBackend, key_prefix: &str) -> DbResult> { - deprecated_symbol_error(key_prefix)?; - if !key_prefix.ends_with('/') { - return error(412, "Key must end with slash"); - } - - let map = backend.inner.read().await; - let current_tick = *backend.tick.read().await; - - let mut results = Vec::new(); - for (k, v) in map.iter() { - if !k.starts_with(key_prefix) { - continue; - } - - if k.strip_prefix(key_prefix).is_some_and(|s| s.contains('$')) { - continue; - } - - if v.ttl == 0 { - continue; - } - - let expires = v.ttl.wrapping_sub(current_tick); - - results.push(DbArray { - key: k.clone(), - data: v.data.clone(), - ttl: expires as u64, - etag: hex::encode(md5::compute(&v.data).0), - }); - } - - Ok(results) -} - -/// memory_info(&backend) -pub async fn memory_info(backend: &MemoryBackend) -> DbResult { - let map = backend.inner.read().await; - let keys = map.len(); - let memory: usize = map.values().map(|v| v.data.len()).sum(); - Ok(format!("{keys} keys, {memory} bytes")) -} - -/// memory_read(&backend, "key") -pub async fn memory_read(backend: &MemoryBackend, key: &str) -> DbResult> { - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let map = backend.inner.read().await; - - match map.get(key) { - None => Ok(None), - Some(entry) => { - let data = entry.data.clone(); - let current_tick = *backend.tick.read().await; - let expires = entry.ttl.wrapping_sub(current_tick); - - Ok(Some(DbArray { - key: key.to_string(), - data: data.clone(), - ttl: expires as u64, - etag: hex::encode(md5::compute(&data).0), - })) - } - } -} - -/// TTL in sec -fn compute_ttl_u8(ttl: Option) -> DbResult { - let sec_usize = match ttl { - Some(Ttl::Sec(secs)) => secs, - Some(Ttl::At(timestamp)) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - if timestamp <= now { - return error(400, "TTL timestamp exceeds MAX_TTL limit"); - } - (timestamp - now) as usize - } - None => CONFIG.max_ttl, - }; - - if sec_usize == 0 { - return error(400, "TTL must be > 0"); - } - - if sec_usize > CONFIG.max_ttl || sec_usize > 255 { - return error(412, "TTL exceeds MAX_TTL or 255 sec"); - } - - let capped = sec_usize.min(u8::MAX as usize); - Ok(capped as u8) -} - -/// memory_save(&backend, "key", value, ttl, mode) -pub async fn memory_save>( - backend: &MemoryBackend, - key: &str, - bytes_value: V, - ttl: Option, - mode: Option, -) -> DbResult<()> { - // u8 - String - let value = match std::str::from_utf8(bytes_value.as_ref()) { - Ok(s) => s.to_string(), - Err(_) => return error(400, "Value must be valid UTF-8"), - }; - - // If max_size != 0 and value size > max_size, return error - let max_size = CONFIG.max_size.unwrap_or(0); - if max_size != 0 && value.len() > max_size { - return error( - 400, - format!("Value in memory mode must be less than {max_size} bytes"), - ); - } - - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let ttl_u8 = compute_ttl_u8(ttl)?; - let current_tick = *backend.tick.read().await; - let expire_tick = current_tick.wrapping_add(ttl_u8); - - let val = value.to_string(); - - let mut map = backend.inner.write().await; - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Upsert => { - map.insert( - key.to_string(), - Entry { - data: val, - ttl: expire_tick, - }, - ); - } - SaveMode::Insert => { - if map.contains_key(key) { - return error(412, "Insert: key already exists"); - } - map.insert( - key.to_string(), - Entry { - data: val, - ttl: expire_tick, - }, - ); - } - SaveMode::Update => { - let Some(existing) = map.get_mut(key) else { - return error(404, "Update: key does not exist"); - }; - *existing = Entry { - data: val, - ttl: expire_tick, - }; - } - SaveMode::Equal(ref expected_md5) => { - let Some(existing) = map.get_mut(key) else { - return error(404, "Equal: key does not exist"); - }; - let actual_md5 = hex::encode(md5::compute(&existing.data).0); - if &actual_md5 != expected_md5 { - return error( - 412, - format!("md5 mismatch, current: {actual_md5}, expected: {expected_md5}"), - ); - } - *existing = Entry { - data: val, - ttl: expire_tick, - }; - } - } - - Ok(()) -} - -/// memory_delete(&backend, "key", mode) -pub async fn memory_delete( - backend: &MemoryBackend, - key: &str, - mode: Option, -) -> DbResult { - deprecated_symbol_error(key)?; - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let mut map = backend.inner.write().await; - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Insert => error(412, "Insert mode is not supported for delete"), - SaveMode::Update | SaveMode::Upsert => { - let existed = map.remove(key).is_some(); - Ok(existed) - } - SaveMode::Equal(ref expected_md5) => { - match map.get(key) { - None => return error(404, "Equal: key does not exist"), - Some(existing) => { - let actual_md5 = hex::encode(md5::compute(&existing.data).0); - if &actual_md5 != expected_md5 { - return error( - 412, - format!( - "md5 mismatch, current: {actual_md5}, expected: {expected_md5}" - ), - ); - } - } - } - let existed = map.remove(key).is_some(); - if !existed { - // WHF?! - return error(404, "Delete: key does not exist"); - } - Ok(true) - } - } -} diff --git a/foundations/hulypulse/src/redis.rs b/foundations/hulypulse/src/redis.rs deleted file mode 100644 index 1c1c0750d2..0000000000 --- a/foundations/hulypulse/src/redis.rs +++ /dev/null @@ -1,530 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use std::{ - sync::Arc, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; - -use ::redis::Msg; -use tokio::sync::RwLock; -use tokio::time::sleep; -use tokio_stream::StreamExt; -use tracing::*; - -use crate::{ - config::{CONFIG, RedisMode}, - db::{DbArray, DbResult, SaveMode, Ttl, deprecated_symbol_error, error}, - hub_service::{HubState, RedisEvent, RedisEventAction}, -}; - -use redis::{ - Client, ConnectionInfo, ProtocolVersion, RedisConnectionInfo, ToRedisArgs, - aio::ConnectionManager, -}; -// use serde::Serialize; - -use crate::hub_service::broadcast_event; - -static MAX_LOOP_COUNT: usize = 1000; // to avoid infinite loops - -pub async fn push_event( - hub_state: &Arc>, - redis: &mut ConnectionManager, - ev: RedisEvent, -) { - // Value only for Set - let mut value: Option = None; - if matches!(ev.message, RedisEventAction::Set) { - match ::redis::cmd("GET") - .arg(&ev.key) - .query_async::>(redis) - .await - { - Ok(v) => value = v, - Err(e) => tracing::warn!("redis GET {} failed: {}", &ev.key, e), - } - } - - broadcast_event(hub_state, ev, value).await; -} - -/// redis_info(&connection) -pub async fn redis_info(conn: &mut ConnectionManager) -> DbResult { - let info: String = redis::cmd("INFO").query_async(conn).await?; - - let mut redis_keys: Option = None; - let mut redis_bytes: Option = None; - - for line in info.lines() { - if line.starts_with("db0:") { - // parsing: db0:keys=152,expires=10,avg_ttl=456789 - if let Some(keys_part) = line.split(',').find(|s| s.starts_with("keys=")) - && let Some(val) = keys_part.strip_prefix("keys=") - { - redis_keys = val.parse::().ok(); - } - } - if line.starts_with("used_memory:") - && let Some(val) = line.strip_prefix("used_memory:") - { - redis_bytes = val.parse::().ok(); - } - } - - Ok(format!( - "{} keys, {} bytes", - redis_keys.unwrap_or(0), - redis_bytes.unwrap_or(0) - )) -} - -/// redis_list(&connection,prefix) -pub async fn redis_list(conn: &mut ConnectionManager, key: &str) -> DbResult> { - deprecated_symbol_error(key)?; - if !key.ends_with('/') { - return error(412, "Key must end with slash"); - } - let pattern = format!("{key}*"); - - let mut cursor = 0u64; - let mut results = Vec::new(); - - loop { - let mut cmd = redis::cmd("SCAN"); - cmd.arg(cursor); - cmd.arg("MATCH").arg(&pattern); - // cmd.arg("COUNT").arg(100); // Optionally adjust batch size - - let (next_cursor, keys): (u64, Vec) = cmd.query_async(conn).await?; - - for k in keys { - // Check for $-security path - if k.strip_prefix(key).is_some_and(|s| s.contains('$')) { - continue; - } - - // Get value - let value: Option = redis::cmd("GET").arg(&k).query_async(conn).await?; - let Some(value) = value else { - continue; - }; // Old and deleted - - // Get TTL - let ttl: i64 = redis::cmd("TTL").arg(&k).query_async(conn).await?; - if ttl >= 0 { - results.push(DbArray { - key: k, - data: value.clone(), - ttl: ttl as u64, - etag: hex::encode(md5::compute(&value).0), - }); - } - } - - if next_cursor == 0 { - break; - } - cursor = next_cursor; - } - - Ok(results) -} - -/// redis_read(&connection,key) -pub async fn redis_read(conn: &mut ConnectionManager, key: &str) -> DbResult> { - deprecated_symbol_error(key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let data: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - - let Some(data) = data else { - return Ok(None); - }; - - let ttl: i64 = redis::cmd("TTL").arg(key).query_async(conn).await?; - match ttl { - -1 => return error(500, "TTL not set"), - -2 => return error(500, "Key not found"), - x if x < 0 => return error(500, "Unknown TTL error"), - _ => {} // ttl >= 0, ок - } - - Ok(Some(DbArray { - key: key.to_string(), - data: data.clone(), - ttl: ttl as u64, - etag: hex::encode(md5::compute(&data).0), - })) -} - -/// redis_save(&connection,key,value,[ttl?],[mode?]) -pub async fn redis_save( - conn: &mut ConnectionManager, - key: &str, - value: T, - ttl: Option, - mode: Option, -) -> DbResult<()> { - deprecated_symbol_error(key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - // If max_size != 0 and value size > max_size, return error - let max_size = CONFIG.max_size.unwrap_or(0); - if max_size != 0 && value.to_redis_args().iter().map(|a| a.len()).sum::() > max_size { - return error( - 400, - format!("Value in memory mode must be less than {max_size} bytes"), - ); - } - - // TTL logic - let sec = match ttl { - Some(Ttl::Sec(secs)) => secs, - Some(Ttl::At(timestamp)) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - if timestamp <= now { - return error(400, "TTL timestamp exceeds MAX_TTL limit"); - } - (timestamp - now) as usize - } - None => CONFIG.max_ttl, - }; - if sec == 0 { - return error(400, "TTL must be > 0"); - } - if sec > CONFIG.max_ttl { - return error(412, "TTL exceeds MAX_TTL"); - } - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Upsert | SaveMode::Insert | SaveMode::Update => { - let mut cmd = redis::cmd("SET"); - cmd.arg(key).arg(value).arg("EX").arg(sec); - match mode { - SaveMode::Insert => { - cmd.arg("NX"); - } // if NOT Exist - SaveMode::Update => { - cmd.arg("XX"); - } // if Exist - _ => {} - }; - let result: Option = cmd.query_async(conn).await?; - if result.is_none() { - return match mode { - SaveMode::Insert => error(412, "Insert: key already exists"), - SaveMode::Update => error(404, "Update: key does not exist"), - _ => error(500, "Unexpected Redis SET failure"), - }; - } - Ok(()) - } - - SaveMode::Equal(ref expected_md5) => { - let mut loop_count = 0; - loop { - let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; - - let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - let existing = match current { - None => { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(404, "Equal: key does not exist"); - } - Some(v) => v, - }; - // check md5 - let actual_md5 = hex::encode(md5::compute(&existing).0); - if &actual_md5 != expected_md5 { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error( - 412, - format!("md5 mismatch, current: {actual_md5}, expected: {expected_md5}"), - ); - } - - // MULTI/EXEC - let mut pipe = redis::pipe(); - pipe.atomic() - .cmd("SET") - .arg(key) - .arg(value.to_redis_args()) - .arg("EX") - .arg(sec); - - let result: Option = pipe.query_async(conn).await?; - if result.is_some() { - break; - } - // None -> key was changed -> repeat loop - loop_count += 1; - if loop_count > MAX_LOOP_COUNT { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(500, "Something wrong: too many retries on Equal mode"); - } - } - - Ok(()) - } - } -} - -/// redis_delete(&connection,key) -pub async fn redis_delete( - conn: &mut ConnectionManager, - key: &str, - mode: Option, -) -> DbResult { - deprecated_symbol_error(key)?; - - if key.ends_with('/') { - return error(412, "Key must not end with a slash"); - } - - let mode = mode.unwrap_or(SaveMode::Upsert); - - match mode { - SaveMode::Update | SaveMode::Upsert => { - let deleted: i32 = redis::cmd("DEL").arg(key).query_async(conn).await?; - Ok(deleted > 0) - } - - SaveMode::Equal(ref expected_md5) => { - let mut loop_count = 0; - loop { - let _: () = redis::cmd("WATCH").arg(key).query_async(conn).await?; - - let current: Option = redis::cmd("GET").arg(key).query_async(conn).await?; - let existing = match current { - None => { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(404, "Equal: key does not exist"); - } - Some(val) => val, - }; - - // check md5 - let actual_md5 = hex::encode(md5::compute(&existing).0); - if &actual_md5 != expected_md5 { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error( - 412, - format!("md5 mismatch, current: {actual_md5}, expected: {expected_md5}"), - ); - } - - let mut pipe = redis::pipe(); - pipe.atomic().cmd("DEL").arg(key); - - let deleted: Option = pipe.query_async(conn).await?; - if let Some(n) = deleted { - return Ok(n > 0); - } - // None -> key was changed -> repeat loop - loop_count += 1; - if loop_count > MAX_LOOP_COUNT { - let _: () = redis::cmd("UNWATCH").query_async(conn).await?; - return error(500, "Something wrong: too many retries on Equal mode"); - } - } - } - - SaveMode::Insert => error(412, "Insert mode is not supported for delete"), - } -} - -impl TryFrom for RedisEvent { - type Error = anyhow::Error; - - fn try_from(msg: Msg) -> Result { - let channel = match msg.get_channel::() { - Ok(c) => c, - Err(e) => { - anyhow::bail!("[redis_events] bad channel: {e}"); - } - }; - let payload = match msg.get_payload::() { - Ok(p) => p, - Err(e) => { - anyhow::bail!("[redis_events] bad payload: {e}"); - } - }; - - // parsing: "__keyevent@0__:set" → event="set", db=0; payload = key - let event = channel.rsplit(':').next().unwrap_or(""); - let message = match event { - "set" => RedisEventAction::Set, - "del" => RedisEventAction::Del, - "unlink" => RedisEventAction::Unlink, - "expired" => RedisEventAction::Expired, - other => RedisEventAction::Other(other.to_string()), - }; - - Ok(RedisEvent { - // db, - key: payload.clone(), - message, - }) - } -} - -pub async fn receiver(redis_client: Client, hub_state: Arc>) { - let mut backoff_secs = 1_u64; - - 'subscriber: loop { - let cmd_conn = match redis_client.get_connection_manager().await { - Ok(c) => c, - Err(e) => { - error!("Redis connection manager (keyspace subscriber): {e}"); - sleep(Duration::from_secs(backoff_secs)).await; - backoff_secs = (backoff_secs * 2).min(60); - continue; - } - }; - - { - let mut c = cmd_conn.clone(); - if let Err(e) = ::redis::cmd("CONFIG") - .arg("SET") - .arg("notify-keyspace-events") - .arg("E$gx") - .query_async::(&mut c) - .await - { - error!("Redis CONFIG SET notify-keyspace-events failed: {e}"); - sleep(Duration::from_secs(backoff_secs)).await; - backoff_secs = (backoff_secs * 2).min(60); - continue; - } - } - - let mut pubsub = match redis_client.get_async_pubsub().await { - Ok(p) => p, - Err(e) => { - error!("Redis pub/sub connect failed: {e}"); - sleep(Duration::from_secs(backoff_secs)).await; - backoff_secs = (backoff_secs * 2).min(60); - continue; - } - }; - - for pattern in [ - "__keyevent@*__:set", - "__keyevent@*__:del", - "__keyevent@*__:unlink", - "__keyevent@*__:expired", - ] { - if let Err(e) = pubsub.psubscribe(pattern).await { - error!("Redis PSUBSCRIBE {pattern} failed: {e}"); - sleep(Duration::from_secs(backoff_secs)).await; - backoff_secs = (backoff_secs * 2).min(60); - continue 'subscriber; - } - } - - info!("Redis keyspace subscriber connected"); - backoff_secs = 1; - - let mut messages = pubsub.on_message(); - while let Some(message) = messages.next().await { - match RedisEvent::try_from(message) { - Ok(ev) => { - let mut c = cmd_conn.clone(); - push_event(&hub_state, &mut c, ev).await; - } - Err(e) => { - warn!("invalid redis message: {e}"); - } - } - } - - warn!("Redis keyspace message stream ended; reconnecting after {backoff_secs}s"); - sleep(Duration::from_secs(backoff_secs)).await; - backoff_secs = (backoff_secs * 2).min(60); - } -} - -/// redis_connect() -pub async fn client() -> anyhow::Result { - let default_port = match CONFIG.redis_mode { - RedisMode::Sentinel => 6379, - RedisMode::Direct => 6380, - }; - - let urls = CONFIG - .redis_urls - .iter() - .map(|url| { - redis::ConnectionAddr::Tcp( - url.host().unwrap().to_string(), - url.port().unwrap_or(default_port), - ) - }) - .collect::>(); - - if CONFIG.redis_mode == RedisMode::Sentinel { - use redis::sentinel::{SentinelClientBuilder, SentinelServerType}; - - debug!(urls=?urls, service=CONFIG.redis_service, "sentinel configuration"); - - let mut sentinel = SentinelClientBuilder::new( - urls, - CONFIG.redis_service.to_owned(), - SentinelServerType::Master, - ) - .unwrap() - .set_client_to_redis_protocol(ProtocolVersion::RESP3) - .set_client_to_redis_db(11) - .set_client_to_redis_password(CONFIG.redis_password.clone()) - .set_client_to_sentinel_password(CONFIG.redis_password.clone()) - .build()?; - - let client = sentinel.async_get_client().await?; - - Ok(client) - } else { - let single = urls - .first() - .ok_or_else(|| anyhow::anyhow!("No redis URL provided"))?; - - let redis_connection_info = RedisConnectionInfo { - db: 0, - username: None, - password: Some(CONFIG.redis_password.clone()), - protocol: ProtocolVersion::RESP3, - }; - - let connection_info = ConnectionInfo { - addr: single.clone(), - redis: redis_connection_info, - }; - - let client = Client::open(connection_info)?; - - Ok(client) - } -} diff --git a/foundations/hulypulse/src/workspace_owner.rs b/foundations/hulypulse/src/workspace_owner.rs deleted file mode 100644 index 14dc545e4f..0000000000 --- a/foundations/hulypulse/src/workspace_owner.rs +++ /dev/null @@ -1,105 +0,0 @@ -// -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License. -// - -use actix_web::{HttpMessage, HttpRequest}; -use hulyrs::services::jwt::Claims; -use serde_json::json; -use std::{fs, path::Path, sync::LazyLock}; -use uuid::Uuid; - -use crate::{config::CONFIG, db::deprecated_symbol}; - -// common checker - -pub fn check_workspace_core(claims_opt: Option, key: &str) -> Result<(), &'static str> { - if deprecated_symbol(key) { - return Err("Invalid key: deprecated symbols"); - } - - #[cfg(not(feature = "auth"))] - return Ok(()); - - let claims = claims_opt.ok_or("Missing authorization")?; - - if claims.is_system() { - return Ok(()); - } - - let jwt_workspace = claims - .workspace - .as_ref() - .ok_or("Missing workspace in token")?; - let path_ws = key - .split('/') - .next() - .ok_or("Invalid key: missing workspace")?; - if path_ws.is_empty() { - return Err("Invalid key: missing workspace"); - } - - let path_ws_uuid = Uuid::parse_str(path_ws).map_err(|_| "Invalid workspace UUID in key")?; - if jwt_workspace != &path_ws_uuid { - return Err("Workspace mismatch"); - } - - Ok(()) -} - -pub fn test_rego_claims(claim: &Claims, command: &str, key: &str) -> bool { - let data = serde_json::to_value(claim).unwrap_or_default(); - let mut rego = REGORUS_ENGINE.clone(); - - rego.set_input(regorus::Value::from(json!({ - "command": command, - "claim": data, - "key": key, - }))); - let result = rego.eval_rule(String::from("data.main.permit")).unwrap(); - - result == regorus::Value::Bool(true) -} - -pub fn test_rego_http(req: HttpRequest, command: &str, key: &str) -> bool { - let claims = req - .extensions() - .get::() - .expect("Missing claims") - .to_owned(); - test_rego_claims(&claims, command, key) -} - -pub static POLICY_TEXT: LazyLock = LazyLock::new(|| { - let Some(policy_file) = CONFIG.policy_file.as_ref() else { - return "package main\n\ndefault permit = true\n".to_string(); - }; - let path = Path::new(policy_file); - if !path.exists() { - panic!("Policy file not found: {}", path.display()); - } - - match fs::read_to_string(path) { - Ok(text) => format!("package main\n\n{text}"), - Err(e) => { - panic!("Failed to read policy file {}: {}", path.display(), e); - } - } -}); - -pub static REGORUS_ENGINE: LazyLock = LazyLock::new(|| { - let mut e = regorus::Engine::new(); - e.add_policy("policy.rego".to_string(), POLICY_TEXT.to_string()) - .expect("can't add policy"); - e -}); diff --git a/foundations/hulypulse/tests/rest_api.rs b/foundations/hulypulse/tests/rest_api.rs deleted file mode 100644 index a072d8deac..0000000000 --- a/foundations/hulypulse/tests/rest_api.rs +++ /dev/null @@ -1,335 +0,0 @@ -use core::panic; -use reqwest::StatusCode; -use serde_json::Value; -use std::env; - -#[derive(Debug)] -struct ApiResponse { - code: u16, - #[allow(dead_code)] - headers: reqwest::header::HeaderMap, - etag: String, - text: String, - json: Value, -} - -fn server_url() -> String { - env::var("TEST_SERVER_URL").unwrap_or_else(|_| "http://127.0.0.1/api".to_string()) -} - -#[allow(dead_code)] -fn etag(data: &str) -> String { - format!("{:x}", md5::compute(data)) -} - -#[allow(dead_code)] -async fn status(base: &str, client: &reqwest::Client) -> () { - let status_url = format!("{}/status", &base); - let resp = client.get(&status_url).send().await.unwrap(); - - assert_eq!(resp.status(), StatusCode::OK); - let text = resp.text().await.unwrap(); - let json: Value = serde_json::from_str(&text).unwrap(); - - let backend = json["backend"].as_str().unwrap_or_default(); - if let Ok(expected_backend) = env::var("TEST_BACKEND") { - assert_eq!(backend, expected_backend); - } else { - assert!(backend == "memory" || backend == "redis"); - } - assert_eq!(json["status"], "OK"); - assert!(json.get("memory_info").is_some()); - assert!(json.get("websockets").is_some()); - // println!("Status: {}", text); -} - -#[allow(dead_code)] -async fn get_not(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 404); -} -#[allow(dead_code)] -async fn get(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); -} -#[allow(dead_code)] -async fn get_data(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.json["data"].as_str().unwrap_or("").to_string() -} -#[allow(dead_code)] -async fn get_ttl(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> u64 { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.json["expires_at"].as_u64().unwrap_or(0) -} -#[allow(dead_code)] -async fn get_etag(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> String { - let r = engine("get", base, client, workspace, key, "", &[]).await; - assert!(r.code == 200); - r.etag -} -#[allow(dead_code)] -async fn put( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - data: &str, - headers: &[&str], -) -> () { - let r = engine("put", base, client, workspace, key, data, headers).await; - // assert!(r.code == 412); - assert!(r.code == 200); - assert!(r.text == "DONE"); -} - -async fn engine( - method: &str, - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - data: &str, - headers: &[&str], -) -> ApiResponse { - let url = format!("{}/api/{}/{}", base, workspace, key); - let req = client; - let mut req = match method { - "get" => req.get(&url), - "put" => req.put(&url).body(data.to_string()), - "delete" => req.delete(&url), - "list" => req.get(&url), - _ => panic!("Unknown method: {}", method), - }; - - if method == "put" { - req = req.body(data.to_string()); - } - - for h in headers { - if let Some((mut k, v)) = h.split_once(':') { - k = k.trim(); - if k != "IF-MATCH" && k != "IF-NON-MATCH" && k != "HULY-TTL" { - panic!("Only `IF-MATCH`, `IF-NON-MATCH`, `HULY-TTL` headers allowed"); - } - req = req.header(k, v.trim()); - } else { - panic!("Unknown format for header: {}", h); - } - } - - let resp = req.send().await.unwrap(); - - let code = resp.status(); - let headers = resp.headers().clone(); - let text = resp.text().await.unwrap(); - ApiResponse { - code: code.as_u16(), - etag: headers - .get("ETag") - .and_then(|h| h.to_str().ok()) - .unwrap_or("") - .to_string(), - text: text.clone(), - json: serde_json::from_str(&text).unwrap_or_default(), - headers: headers.clone(), - } -} - -#[allow(dead_code)] -async fn delete_any(base: &str, client: &reqwest::Client, workspace: &str, key: &str) -> () { - engine("delete", base, client, workspace, key, "", &[]).await; -} -#[allow(dead_code)] -async fn delete( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - headers: &[&str], -) -> () { - let r = engine("delete", base, client, workspace, key, "", headers).await; - assert_eq!(r.code, StatusCode::NO_CONTENT); // 204 - assert!(r.text.is_empty()); -} -#[allow(dead_code)] -async fn delete_not( - base: &str, - client: &reqwest::Client, - workspace: &str, - key: &str, - headers: &[&str], -) -> () { - let r = engine("delete", base, client, workspace, key, "", headers).await; - assert!( - (r.code == StatusCode::NOT_FOUND && r.text == "not found") - || (!headers.is_empty() - && r.code == StatusCode::PRECONDITION_FAILED - && (r.text.contains("412 md5 mismatch") - || r.text.contains("404 Equal: key does not exist"))) - ); - // println!("CODE: {}", r.code); - // println!("TEXT: {}", r.text); -} - -#[tokio::test] -async fn put_and_get() { - let base = server_url(); - // println!("Use url: {}", &base); - - let client = reqwest::Client::builder() - .danger_accept_invalid_certs(true) // только на билдере! - .build() - .unwrap(); - - // Check status - status(&base, &client).await; - - let workspace = "00000000-0000-0000-0000-000000000001"; - let key = "TESTS/key1"; - let data = "Value_1"; - - // test put/get/delete - delete_any(&base, &client, workspace, key).await; - get_not(&base, &client, workspace, key).await; - put(&base, &client, workspace, key, data, &[]).await; - get(&base, &client, workspace, key).await; - delete(&base, &client, workspace, key, &[]).await; - delete_not(&base, &client, workspace, key, &[]).await; - - // test If-Match - delete_any(&base, &client, workspace, key).await; - // delete only tag - put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; - delete_not( - &base, - &client, - workspace, - key, - &["IF-MATCH: 11111111111111111111111111111111"], - ) - .await; - get(&base, &client, workspace, key).await; - delete( - &base, - &client, - workspace, - key, - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - get_not(&base, &client, workspace, key).await; - - // update with tag - put(&base, &client, workspace, key, data, &["HULY-TTL: 2"]).await; - // replace with match - put( - &base, - &client, - workspace, - key, - "Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); - // replace with wrong mismatch - let r = engine( - "put", - &base, - &client, - &workspace, - &key, - "Next Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(r.code == StatusCode::PRECONDITION_FAILED && r.text.contains("412 md5 mismatch")); - // match: * - assert!(get_data(&base, &client, workspace, key).await == "Another Data".to_string()); - put( - &base, - &client, - workspace, - key, - "Another Data 2", - &["IF-MATCH: *"], - ) - .await; - assert!(get_data(&base, &client, workspace, key).await == "Another Data 2".to_string()); - // unknown key matched - let fake_key = format!("{}{}", &key, "xxx"); - let r = engine( - "put", - &base, - &client, - &workspace, - &fake_key, - "Next Another Data", - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(r.code == StatusCode::NOT_FOUND && r.text.contains("404 Equal: key does not exist")); - // if not match - let data3 = "Another Data 3"; - put( - &base, - &client, - &workspace, - &key, - &data3, - &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - assert!(get_data(&base, &client, &workspace, &key).await == data3.to_string()); - - // DELETE if match - put(&base, &client, &workspace, &key, &data, &[]).await; - delete( - &base, - &client, - &workspace, - &key, - &["IF-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - delete(&base, &client, &workspace, &key, &["IF-MATCH: *"]).await; - get_not(&base, &client, &workspace, &key).await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - delete(&base, &client, &workspace, &key, &["IF-NON-MATCH: *"]).await; // TODO !!! - get_not(&base, &client, &workspace, &key).await; - - put(&base, &client, &workspace, &key, &data, &[]).await; - let tag = get_etag(&base, &client, &workspace, &key).await; - assert!(tag == "c7bcabf6b98a220f2f4888a18d01568d".to_string()); - delete( - &base, - &client, - &workspace, - &key, - &["IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"], - ) - .await; - get_not(&base, &client, &workspace, &key).await; - - // let r = engine("put", &base, &client, &workspace, &key, "Next Another Data", &["HULY-TTL: 2","IF-NON-MATCH: c7bcabf6b98a220f2f4888a18d01568d"]).await; - // println!("ALL: {:?}", r); - - // test TTL - delete_any(&base, &client, workspace, key).await; - put(&base, &client, workspace, key, data, &["HULY-TTL: 7"]).await; - let ttl = get_ttl(&base, &client, workspace, key).await; - assert!(ttl == 7 || ttl == 6, "TTL should be 7 (ok, may by 6)"); - put(&base, &client, workspace, key, data, &["HULY-TTL: 1"]).await; - let ttl = get_ttl(&base, &client, workspace, key).await; - assert!(ttl == 1 || ttl == 0, "TTL should be 1 (ok, may by 0)"); - // wait for 1.05 seconds - tokio::time::sleep(tokio::time::Duration::from_millis(1050)).await; - get_not(&base, &client, workspace, key).await; -} diff --git a/foundations/hulypulse/tests/ws.rs b/foundations/hulypulse/tests/ws.rs deleted file mode 100644 index 68f85595a2..0000000000 --- a/foundations/hulypulse/tests/ws.rs +++ /dev/null @@ -1,57 +0,0 @@ -use futures_util::{SinkExt, StreamExt}; -use serde_json::{Value, json}; -use tokio_tungstenite::{WebSocketStream, connect_async, tungstenite::Message}; - -#[tokio::test] -async fn websocket_echo_id() { - let workspace = "00000000-0000-0000-0000-000000000001"; - let key = "TESTS/key1"; - - let base = - std::env::var("TEST_SERVER_URL").unwrap_or_else(|_| "ws://127.0.0.1:8099/ws".to_string()); - - let (ws_stream, _) = connect_async(&base).await.expect("Can't connect WebSocket"); - let (mut write, mut read) = ws_stream.split(); - - let mut req_id = 1; - - let msg = json!({ - "correlation": req_id.to_string(), - "type": "info" - }); - let r = send(&mut write, &mut read, &msg).await; - assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); - - req_id += 1; - let msg = json!({ - "correlation": req_id.to_string(), - "type": "sub", - "workspace": workspace, - "key": key - }); - let r = send(&mut write, &mut read, &msg).await; - // println!("Answer: {:?}", r); - assert_eq!(r["correlation"], req_id.to_string(), "ID should match"); - assert_eq!(r["result"], "OK", "Subscription should be ok"); -} - -async fn send( - write: &mut futures_util::stream::SplitSink, Message>, - read: &mut futures_util::stream::SplitStream>, - data: &Value, -) -> Value -where - S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin, -{ - // Sending - let msg = Message::Text(data.to_string()); - write.send(msg).await.unwrap(); - - // Waiting for response - while let Some(Ok(Message::Text(resp))) = read.next().await { - let json_resp: Value = serde_json::from_str(&resp).unwrap(); - return json_resp; - } - - panic!("No answer from server"); -} diff --git a/models/all/package.json b/models/all/package.json index 7f2f7d2d92..e4713e7b02 100644 --- a/models/all/package.json +++ b/models/all/package.json @@ -124,6 +124,7 @@ "@hcengineering/model-test-management": "workspace:^0.7.0", "@hcengineering/model-survey": "workspace:^0.7.0", "@hcengineering/model-presence": "workspace:^0.7.0", + "@hcengineering/model-pulse": "workspace:^0.7.0", "@hcengineering/model-card": "workspace:^0.7.0", "@hcengineering/model-mail": "workspace:^0.7.0", "@hcengineering/model-chat": "workspace:^0.7.0", diff --git a/models/all/src/index.ts b/models/all/src/index.ts index e33dfc98ab..e483fe8848 100644 --- a/models/all/src/index.ts +++ b/models/all/src/index.ts @@ -119,6 +119,7 @@ import { communicationId, createModel as communicationModel } from '@hcengineeri import { emojiId, createModel as emojiModel } from '@hcengineering/model-emoji' import { inboxId, createModel as inboxModel } from '@hcengineering/model-inbox' import { presenceId, createModel as presenceModel } from '@hcengineering/model-presence' +import { pulseId, createModel as pulseModel } from '@hcengineering/model-pulse' import processes, { processId, createModel as processModel } from '@hcengineering/model-process' import { serverDocumentsId, @@ -487,6 +488,7 @@ export default function buildModel (): Builder { } ], [presenceModel, presenceId], + [pulseModel, pulseId], [ chatModel, chatId, diff --git a/models/pulse/.eslintrc.js b/models/pulse/.eslintrc.js new file mode 100644 index 0000000000..c1cf82cba0 --- /dev/null +++ b/models/pulse/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/models/pulse/config/rig.json b/models/pulse/config/rig.json new file mode 100644 index 0000000000..0691a71573 --- /dev/null +++ b/models/pulse/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "model" +} diff --git a/packages/hulypulse-client/jest.config.js b/models/pulse/jest.config.js similarity index 100% rename from packages/hulypulse-client/jest.config.js rename to models/pulse/jest.config.js diff --git a/models/pulse/package.json b/models/pulse/package.json new file mode 100644 index 0000000000..24c1744fbb --- /dev/null +++ b/models/pulse/package.json @@ -0,0 +1,44 @@ +{ + "name": "@hcengineering/model-pulse", + "version": "0.7.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Intabia Fusion", + "template": "@hcengineering/model-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:format": "format src", + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "test": "jest --passWithNoTests --silent --forceExit" + }, + "devDependencies": { + "@hcengineering/platform-rig": "workspace:^0.7.21", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.6.2", + "typescript": "^5.9.3", + "@types/node": "^22.18.1", + "jest": "^29.7.0", + "@types/jest": "^29.5.5", + "ts-jest": "^29.1.1" + }, + "dependencies": { + "@hcengineering/core": "workspace:^0.7.26", + "@hcengineering/model": "workspace:^0.7.17", + "@hcengineering/model-core": "workspace:^0.7.0", + "@hcengineering/contact": "workspace:^0.7.0", + "@hcengineering/pulse": "workspace:^0.7.0", + "@hcengineering/platform": "workspace:^0.7.20" + } +} diff --git a/models/pulse/src/index.ts b/models/pulse/src/index.ts new file mode 100644 index 0000000000..687761a08a --- /dev/null +++ b/models/pulse/src/index.ts @@ -0,0 +1,51 @@ +// +// Copyright © 2026 Intabia Fusion. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Person } from '@hcengineering/contact' +import { DOMAIN_TRANSIENT, type Class, type Doc, type PersonId, type Ref, type Timestamp } from '@hcengineering/core' +import { Model, type Builder } from '@hcengineering/model' +import core, { TDoc } from '@hcengineering/model-core' +import type { IntlString } from '@hcengineering/platform' +import type { DocumentPresence, TypingIndicator } from '@hcengineering/pulse' +import pulse from './plugin' + +export { pulseId } from '@hcengineering/pulse' + +@Model(pulse.class.DocumentPresence, core.class.Doc, DOMAIN_TRANSIENT) +export class TDocumentPresence extends TDoc implements DocumentPresence { + objectId!: Ref + objectClass!: Ref> + person!: Ref + lastActive!: Timestamp +} + +@Model(pulse.class.TypingIndicator, core.class.Doc, DOMAIN_TRANSIENT) +export class TTypingIndicator extends TDoc implements TypingIndicator { + objectId!: string + socialId!: PersonId + status?: IntlString +} + +export function createModel (builder: Builder): void { + builder.createModel(TDocumentPresence, TTypingIndicator) + + builder.mixin(pulse.class.DocumentPresence, core.class.Class, core.mixin.TransientTTL, { + ttl: 10 + }) + + builder.mixin(pulse.class.TypingIndicator, core.class.Class, core.mixin.TransientTTL, { + ttl: 3 + }) +} diff --git a/models/pulse/src/plugin.ts b/models/pulse/src/plugin.ts new file mode 100644 index 0000000000..eaafed67e9 --- /dev/null +++ b/models/pulse/src/plugin.ts @@ -0,0 +1,19 @@ +// +// Copyright © 2026 Intabia Fusion. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import pulse, { pulseId } from '@hcengineering/pulse' +import { mergeIds } from '@hcengineering/platform' + +export default mergeIds(pulseId, pulse, {}) diff --git a/models/pulse/tsconfig.json b/models/pulse/tsconfig.json new file mode 100644 index 0000000000..584b85a1b7 --- /dev/null +++ b/models/pulse/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} diff --git a/packages/hulypulse-client/README.md b/packages/hulypulse-client/README.md deleted file mode 100644 index fa12f00281..0000000000 --- a/packages/hulypulse-client/README.md +++ /dev/null @@ -1,152 +0,0 @@ -# HulypulseClient - -A TypeScript/Node.js client for the Hulypulse WebSocket server. -Supports automatic reconnection, request–response correlation, `get` / `put` / `delete`, and subscriptions. - ---- - -### Main Methods - -## put(key: string, data: string, TTL?: number): Promise - -Stores a value under a key. - - TTL (optional) — time-to-live in seconds. - - Resolves with true if the operation succeeded. - -await client.put("workspace/users/123", "Alice", 60) → true - -## get(key: string): Promise - -Retrieves the value for a key. - - Resolves with the value if found. - Resolves with false if the key does not exist. - -const value = await client.get("workspace/users/123") -if (value) { - console.log("User data:", value) -} else { - console.log("User not found") -} - -## get_full(key: string): Promise<{data, etag, expires_at} | false> - -Retrieves the full record: - - data — stored value, - etag — data identifier, - expires_at — expiration in seconds. - -const full = await client.get_full("workspace/users/123") -if (full) { - console.log(full.data, full.etag, full.expires_at) -} - -## delete(key: string): Promise - -Deletes a key. - - Resolves with true if the key was deleted. - Resolves with false if the key was not found. - -const deleted = await client.delete("workspace/users/123") -console.log(deleted ? "Deleted" : "Not found") - -## subscribe(key: string, callback: (msg, key, index) => void): Promise - -Subscribes to updates for a key (or prefix). - - The callback is invoked on every event: Set, Del, Expired - - Resolves with true if a new subscription was created. - Resolves with false if the callback was already subscribed. - -const cb = (msg, key, index) => { - if( msg.message === 'Expired' ) console.log(`${msg.key} was expired`) -} - -await client.subscribe("workspace/users/", cb) -// Now cb will be called when any key starting with "workspace/users/" changes - -## unsubscribe(key: string, callback: Callback): Promise - -Unsubscribes a specific callback. - - Resolves with true if the callback was removed (and if it was the last one, the server gets an unsub message). - Resolves with false if the callback was not found. - -await client.unsubscribe("workspace/users/", cb) - -## send(message: any): Promise - -Low-level method to send a raw message. - - Automatically attaches a correlation id. - Resolves when a response with the same correlation is received. - -const reply = await client.send({ type: "get", key: "workspace/users/123" }) -console.log("Raw reply:", reply) - -## Reconnection - - If the connection drops, the client automatically reconnects. - All active subscriptions are re-sent to the server after reconnect. - -## Closing - -The client supports both manual closing and the new using syntax (TypeScript 5.2+). - -client[Symbol.dispose]() // closes the connection - -or, if needed internally: - -(client as any).close() - ---- - -## Usage Example - -```ts -import { HulypulseClient } from "./hulypulse_client.js" - -async function main() { - // connect - const client = await HulypulseClient.connect("wss://hulypulse_mem.lleo.me/ws") - - // subscribe to updates - const cb = (msg, key, index) => { - console.log("Update for", key, ":", msg) - } - await client.subscribe("workspace/users/", cb) - - // put value - await client.put("workspace/users/123", JSON.stringify({ name: "Alice" }), 5) - - // get value - const value = await client.get("workspace/users/123") - console.log("Fetched:", value) - - // get full record - const full = await client.get_full("workspace/users/123") - if (full) { - console.log(full.data, full.etag, full.expires_at) - } - - // delete key - const deleted = await client.delete("workspace/users/123") - console.log(deleted ? "Deleted" : "Not found") - - // unsubscribe - await client.unsubscribe("workspace/users/", cb) - - // low-level send - const reply = await client.send({ type: "sublist" }) - console.log("My sublists:", reply) - - // dispose - client[Symbol.dispose]() -} - -main() diff --git a/packages/hulypulse-client/jest.setup.js b/packages/hulypulse-client/jest.setup.js deleted file mode 100644 index 78d57e3d23..0000000000 --- a/packages/hulypulse-client/jest.setup.js +++ /dev/null @@ -1,2 +0,0 @@ -// Set up fetch mock -require('jest-fetch-mock').enableMocks() diff --git a/packages/presentation/package.json b/packages/presentation/package.json index 04988fc0d0..c0502d398f 100644 --- a/packages/presentation/package.json +++ b/packages/presentation/package.json @@ -63,7 +63,6 @@ "@hcengineering/theme": "workspace:^0.7.0", "@hcengineering/retry": "workspace:^0.7.18", "@hcengineering/hulylake-client": "workspace:^0.7.18", - "@hcengineering/hulypulse-client": "workspace:^0.7.0", "@hcengineering/storage-client": "workspace:^0.7.18", "fast-equals": "^5.2.2", "png-chunks-extract": "^1.0.0", diff --git a/packages/presentation/src/index.ts b/packages/presentation/src/index.ts index a10eb62591..184dde9f02 100644 --- a/packages/presentation/src/index.ts +++ b/packages/presentation/src/index.ts @@ -79,4 +79,3 @@ export * from './drawingCommand' export * from './drawingCommandsProcessor' export * from './link-preview' export * from './communication' -export * from './pulse' diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index bf48b1d4ad..7611d2a53d 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -186,7 +186,6 @@ export default plugin(presentationId, { MailUrl: '' as Metadata, DisabledFeatures: '' as Metadata>, PreviewUrl: '' as Metadata, - PulseUrl: '' as Metadata, HulylakeUrl: '' as Metadata, PaymentUrl: '' as Metadata, SignupUrl: '' as Metadata diff --git a/packages/presentation/src/pulse.ts b/packages/presentation/src/pulse.ts deleted file mode 100644 index e0019da98f..0000000000 --- a/packages/presentation/src/pulse.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright © 2025 Hardcore Engineering Inc. -// -// Licensed under the Eclipse Public License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. You may -// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// -// See the License for the specific language governing permissions and -// limitations under the License - -import { HulypulseClient } from '@hcengineering/hulypulse-client' -import { getMetadata } from '@hcengineering/platform' -import presentation from './plugin' - -let currentWorkspaceUuid: string | undefined -let currentToken: string | undefined -let promise: Promise | undefined - -export async function createPulseClient (): Promise { - const pulseUrl = getMetadata(presentation.metadata.PulseUrl) ?? '' - const token = getMetadata(presentation.metadata.Token) ?? '' - const workspaceUuid = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' - - if (pulseUrl === '' || token === '' || workspaceUuid === '') { - return undefined - } - - // Token or workspace changed, need to reconnect - if (token !== currentToken || workspaceUuid !== currentWorkspaceUuid) { - closePulseClient() - } - - if (promise !== undefined) { - // eslint-disable-next-line @typescript-eslint/return-await - return promise - } - - promise = new Promise((resolve) => { - HulypulseClient.connect(`${pulseUrl}?token=${token}`) - .then(resolve) - .catch(() => { - resolve(undefined) - }) - }) - currentToken = token - currentWorkspaceUuid = workspaceUuid - - // eslint-disable-next-line @typescript-eslint/return-await - return promise -} - -export function closePulseClient (): void { - if (promise !== undefined) { - void promise.then((client) => { - client?.close() - }) - } - promise = undefined - currentToken = undefined - currentWorkspaceUuid = undefined -} diff --git a/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte b/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte index 1cbe484a3d..0a81f9ef86 100644 --- a/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte +++ b/plugins/chunter-resources/src/components/ChannelTypingInfo.svelte @@ -87,6 +87,7 @@ { - void setTyping(acc.primarySocialId, object._id) + void setTyping(acc.primarySocialId, object._id, object.space) }) } diff --git a/plugins/communication-resources/src/components/input/MessageInput.svelte b/plugins/communication-resources/src/components/input/MessageInput.svelte index 530a2f2e8f..46de459319 100644 --- a/plugins/communication-resources/src/components/input/MessageInput.svelte +++ b/plugins/communication-resources/src/components/input/MessageInput.svelte @@ -306,7 +306,7 @@ if (message !== undefined) return if (!isEmptyMarkup(markup)) { throttle.call(() => { - void setTyping(acc.primarySocialId, card.peerId ? `peer:${card.peerId}` : card._id) + void setTyping(acc.primarySocialId, card.peerId ? `peer:${card.peerId}` : card._id, card.space) }) } } diff --git a/plugins/love-resources/package.json b/plugins/love-resources/package.json index 367e4774d2..15d2bb3f84 100644 --- a/plugins/love-resources/package.json +++ b/plugins/love-resources/package.json @@ -67,7 +67,6 @@ "@hcengineering/emoji-resources": "workspace:^0.7.0", "@hcengineering/theme": "workspace:^0.7.0", "@hcengineering/account-client": "workspace:^0.7.25", - "@hcengineering/hulypulse-client": "workspace:^0.7.0", "@livekit/krisp-noise-filter": "^0.4.1", "@livekit/track-processors": "^0.7.2", "livekit-client": "^2.19.0", diff --git a/plugins/presence-resources/package.json b/plugins/presence-resources/package.json index e78fc771db..8e28eedc84 100644 --- a/plugins/presence-resources/package.json +++ b/plugins/presence-resources/package.json @@ -48,7 +48,7 @@ "@hcengineering/contact": "workspace:^0.7.0", "@hcengineering/contact-resources": "workspace:^0.7.0", "@hcengineering/presence": "workspace:^0.7.0", - "@hcengineering/hulypulse-client": "workspace:^0.7.0", + "@hcengineering/pulse": "workspace:^0.7.0", "svelte": "^4.2.20", "fast-equals": "^5.2.2" } diff --git a/plugins/presence-resources/src/components/PresenceAvatars.svelte b/plugins/presence-resources/src/components/PresenceAvatars.svelte index a89a959391..eeec1764ed 100644 --- a/plugins/presence-resources/src/components/PresenceAvatars.svelte +++ b/plugins/presence-resources/src/components/PresenceAvatars.svelte @@ -54,6 +54,7 @@
diff --git a/plugins/presence-resources/src/presence.ts b/plugins/presence-resources/src/presence.ts index 65b3f9c282..49e9806198 100644 --- a/plugins/presence-resources/src/presence.ts +++ b/plugins/presence-resources/src/presence.ts @@ -1,4 +1,6 @@ +// // Copyright © 2025 Hardcore Engineering Inc. +// Copyright © 2026 Intabia Fusion. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -12,15 +14,15 @@ // limitations under the License import { type Employee, type Person } from '@hcengineering/contact' -import { type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client' -import { type Class, type Doc, type Ref } from '@hcengineering/core' -import { getMetadata } from '@hcengineering/platform' -import presentation, { createPulseClient } from '@hcengineering/presentation' +import { type Class, type Doc, type Ref, type Space } from '@hcengineering/core' +import { createQuery, getClient } from '@hcengineering/presentation' +import pulse, { type DocumentPresence } from '@hcengineering/pulse' export interface PresenceInfo { personId: Ref objectId: string objectClass: Ref> + space: Ref } export interface PresenceActionParams { @@ -30,96 +32,89 @@ export interface PresenceActionParams { onPresence: (presence: Map>) => void } +function presenceDocId (objectId: string, personId: Ref): Ref { + return `presence:${objectId}:${personId}` as Ref +} + export function presence (node: HTMLElement, params: PresenceActionParams): any { - let unsubscribe: Promise | undefined - let presence = new Map>() + const liveQuery = createQuery(true) + let presenceMap = new Map>() let personId = params.personId let objectId = params.objectId let objectClass = params.objectClass let onPresence = params.onPresence - function handlePresenceInfo (key: string, value: PresenceInfo | undefined): void { - if (value?.personId === personId) { - return - } - - if (value === undefined) { - presence.delete(key) - } else { - presence.set(key, value.personId) - } - - onPresence(presence) + function runQuery (): void { + liveQuery.query(pulse.class.DocumentPresence, { objectId: objectId as Ref, objectClass }, (result) => { + const next = new Map>() + for (const doc of result) { + if (doc.person === personId) continue + next.set(doc._id, doc.person) + } + presenceMap = next + onPresence(presenceMap) + }) } - unsubscribe = subscribePresence(params.objectClass, params.objectId, handlePresenceInfo) + runQuery() return { update: (params: PresenceActionParams) => { - if (objectId !== params.objectId || objectClass !== params.objectClass) { - personId = params.personId - objectId = params.objectId - objectClass = params.objectClass - onPresence = params.onPresence - - void unsubscribe?.then((unsub) => { - void unsub() - }) - - presence = new Map>() - unsubscribe = subscribePresence(params.objectClass, params.objectId, handlePresenceInfo) - - onPresence(presence) + const needResubscribe = + objectId !== params.objectId || objectClass !== params.objectClass || personId !== params.personId + personId = params.personId + objectId = params.objectId + objectClass = params.objectClass + onPresence = params.onPresence + + if (needResubscribe) { + presenceMap = new Map>() + onPresence(presenceMap) + runQuery() } }, destroy: () => { - void unsubscribe?.then((unsub) => { - void unsub() - }) + liveQuery.unsubscribe() } } } -export async function subscribePresence ( - objectClass: Ref>, - objectId: string, - callback: Callback -): Promise { - const client = await createPulseClient() - - if (client !== undefined) { - const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' - return await client.subscribe(`${workspace}/presence/${objectId}/`, callback) - } - - return async () => false -} - -export async function updatePresence (presence: PresenceInfo, presenceTtlSeconds: number): Promise { - const client = await createPulseClient() - - if (client !== undefined) { - const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' - const { personId, objectId } = presence - try { - await client.put(`${workspace}/presence/${objectId}/${personId}`, presence, presenceTtlSeconds) - } catch (error) { - console.warn('failed to put presence info:', error) +export async function updatePresence (info: PresenceInfo): Promise { + try { + const client = getClient() + const id = presenceDocId(info.objectId, info.personId) + const existing = await client.findOne(pulse.class.DocumentPresence, { _id: id }) + const now = Date.now() + if (existing !== undefined) { + await client.diffUpdate(existing, { lastActive: now }) + return } + await client.createDoc( + pulse.class.DocumentPresence, + info.space, + { + objectId: info.objectId as Ref, + objectClass: info.objectClass, + person: info.personId, + lastActive: now + }, + id + ) + } catch (err) { + console.warn('failed to update presence:', err) } } -export async function deletePresence (presence: PresenceInfo): Promise { - const client = await createPulseClient() - - if (client !== undefined) { - const workspace = getMetadata(presentation.metadata.WorkspaceUuid) ?? '' - const { personId, objectId } = presence - try { - await client.delete(`${workspace}/presence/${objectId}/${personId}`) - } catch (error) { - console.warn('failed to delete presence info:', error) +export async function deletePresence (info: PresenceInfo): Promise { + try { + const client = getClient() + const id = presenceDocId(info.objectId, info.personId) + const existing = await client.findOne(pulse.class.DocumentPresence, { _id: id }) + if (existing !== undefined) { + await client.removeDoc(pulse.class.DocumentPresence, existing.space, existing._id) } + } catch (err) { + console.warn('failed to delete presence:', err) } } diff --git a/plugins/presence-resources/src/typing.ts b/plugins/presence-resources/src/typing.ts index 07ca6bb776..ee53c9db5a 100644 --- a/plugins/presence-resources/src/typing.ts +++ b/plugins/presence-resources/src/typing.ts @@ -1,4 +1,6 @@ +// // Copyright © 2025 Hardcore Engineering Inc. +// Copyright © 2026 Intabia Fusion. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -11,16 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License -import { type UnsubscribeCallback, type Callback } from '@hcengineering/hulypulse-client' -import { type IntlString, getMetadata } from '@hcengineering/platform' -import presentation, { createPulseClient } from '@hcengineering/presentation' -import { type PersonId } from '@hcengineering/core' - -const typingDelaySeconds = 2 - -function getWorkspace (): string { - return getMetadata(presentation.metadata.WorkspaceUuid) ?? '' -} +import { type PersonId, type Ref, type Space } from '@hcengineering/core' +import { type IntlString } from '@hcengineering/platform' +import { createQuery, getClient } from '@hcengineering/presentation' +import pulse, { type TypingIndicator } from '@hcengineering/pulse' export interface TypingInfo { socialId: PersonId @@ -31,98 +27,83 @@ export interface TypingInfo { export interface TypingActionParams { socialId: PersonId objectId: string - onTyping: (presence: Map) => void + onTyping: (typing: Map) => void +} + +function typingDocId (objectId: string, socialId: PersonId): Ref { + return `typing:${objectId}:${socialId}` as Ref } export function typing (node: HTMLElement, params: TypingActionParams): any { - let unsubscribe: Promise | undefined - let typing = new Map() + const liveQuery = createQuery(true) + let state = new Map() let socialId = params.socialId let objectId = params.objectId let onTyping = params.onTyping - function handleTypingInfo (key: string, value: TypingInfo | undefined): void { - if (value?.socialId === socialId) { - return - } - - if (value === undefined) { - typing.delete(key) - } else { - typing.set(key, value) - } - - onTyping(typing) + function runQuery (): void { + liveQuery.query(pulse.class.TypingIndicator, { objectId }, (result) => { + const next = new Map() + for (const doc of result) { + if (doc.socialId === socialId) continue + next.set(doc._id, { socialId: doc.socialId, objectId: doc.objectId, status: doc.status }) + } + state = next + onTyping(state) + }) } - unsubscribe = subscribeTyping(params.objectId, handleTypingInfo) + runQuery() return { update: (params: TypingActionParams) => { - if (objectId !== params.objectId) { - socialId = params.socialId - objectId = params.objectId - onTyping = params.onTyping - - void unsubscribe?.then((unsub) => { - void unsub() - }) - - typing = new Map() - unsubscribe = subscribeTyping(params.objectId, handleTypingInfo) - - onTyping(typing) + const needResubscribe = objectId !== params.objectId || socialId !== params.socialId + socialId = params.socialId + objectId = params.objectId + onTyping = params.onTyping + + if (needResubscribe) { + state = new Map() + onTyping(state) + runQuery() } }, destroy: () => { - void unsubscribe?.then((unsub) => { - void unsub() - }) + liveQuery.unsubscribe() } } } -export async function subscribeTyping ( +export async function setTyping ( + socialId: PersonId, objectId: string, - callback: Callback -): Promise { - const client = await createPulseClient() - if (client !== undefined) { - const workspace = getWorkspace() - try { - return await client.subscribe(`${workspace}/typing/${objectId}/`, callback) - } catch (error) { - console.warn('failed to subscribe typing info:', error) - } - } - - return async () => false -} - -export async function setTyping (socialId: PersonId, objectId: string, status?: IntlString): Promise { - const client = await createPulseClient() - - if (client !== undefined) { - const workspace = getWorkspace() - const typingInfo: TypingInfo = { socialId, objectId, status } - try { - await client.put(`${workspace}/typing/${objectId}/${socialId}`, typingInfo, typingDelaySeconds) - } catch (error) { - console.warn('failed to put typing info:', error) + space: Ref, + status?: IntlString +): Promise { + try { + const client = getClient() + const id = typingDocId(objectId, socialId) + const existing = await client.findOne(pulse.class.TypingIndicator, { _id: id }) + if (existing !== undefined) { + await client.diffUpdate(existing, { status }) + return } + await client.createDoc(pulse.class.TypingIndicator, space, { objectId, socialId, status }, id) + } catch (err) { + console.warn('failed to set typing:', err) } } export async function clearTyping (socialId: PersonId, objectId: string): Promise { - const client = await createPulseClient() - - if (client !== undefined) { - const workspace = getWorkspace() - try { - await client.delete(`${workspace}/typing/${objectId}/${socialId}`) - } catch (error) { - console.warn('failed to delete typing info:', error) + try { + const client = getClient() + const id = typingDocId(objectId, socialId) + const existing = await client.findOne(pulse.class.TypingIndicator, { _id: id }) + if (existing !== undefined) { + await client.removeDoc(pulse.class.TypingIndicator, existing.space, existing._id) } + } catch (err) { + console.warn('failed to clear typing:', err) } } diff --git a/packages/hulypulse-client/.eslintrc.js b/plugins/pulse/.eslintrc.js similarity index 100% rename from packages/hulypulse-client/.eslintrc.js rename to plugins/pulse/.eslintrc.js diff --git a/packages/hulypulse-client/config/rig.json b/plugins/pulse/config/rig.json similarity index 97% rename from packages/hulypulse-client/config/rig.json rename to plugins/pulse/config/rig.json index 0110930f55..06a2a2e17a 100644 --- a/packages/hulypulse-client/config/rig.json +++ b/plugins/pulse/config/rig.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", "rigPackageName": "@hcengineering/platform-rig" } diff --git a/plugins/pulse/jest.config.js b/plugins/pulse/jest.config.js new file mode 100644 index 0000000000..2cfd408b67 --- /dev/null +++ b/plugins/pulse/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/packages/hulypulse-client/package.json b/plugins/pulse/package.json similarity index 72% rename from packages/hulypulse-client/package.json rename to plugins/pulse/package.json index a9fa6a8cdb..90ac5de9cf 100644 --- a/packages/hulypulse-client/package.json +++ b/plugins/pulse/package.json @@ -1,5 +1,5 @@ { - "name": "@hcengineering/hulypulse-client", + "name": "@hcengineering/pulse", "version": "0.7.0", "main": "lib/index.js", "svelte": "src/index.ts", @@ -9,6 +9,7 @@ "types/**/*", "tsconfig.json" ], + "author": "Intabia Fusion", "scripts": { "build": "compile", "build:watch": "compile", @@ -20,36 +21,23 @@ "_phase:validate": "compile validate" }, "devDependencies": { - "cross-env": "~7.0.3", "@hcengineering/platform-rig": "workspace:^0.7.21", - "@types/node": "^22.18.1", "@typescript-eslint/eslint-plugin": "^6.21.0", "eslint-plugin-import": "^2.26.0", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-n": "^15.4.0", "eslint": "^8.54.0", - "esbuild": "^0.25.10", "@typescript-eslint/parser": "^6.21.0", "eslint-config-standard-with-typescript": "^40.0.0", "prettier": "^3.6.2", "typescript": "^5.9.3", "jest": "^29.7.0", - "jest-fetch-mock": "^3.0.3", "ts-jest": "^29.1.1", "@types/jest": "^29.5.5" }, "dependencies": { "@hcengineering/core": "workspace:^0.7.26", - "@hcengineering/platform": "workspace:^0.7.20" - }, - "exports": { - ".": { - "types": "./types/index.d.ts", - "require": "./lib/index.js", - "import": "./lib/index.js" - } - }, - "publishConfig": { - "access": "public" + "@hcengineering/platform": "workspace:^0.7.20", + "@hcengineering/contact": "workspace:^0.7.0" } } diff --git a/packages/hulypulse-client/src/index.ts b/plugins/pulse/src/index.ts similarity index 78% rename from packages/hulypulse-client/src/index.ts rename to plugins/pulse/src/index.ts index 6187835396..194a5a2ff2 100644 --- a/packages/hulypulse-client/src/index.ts +++ b/plugins/pulse/src/index.ts @@ -1,5 +1,5 @@ // -// Copyright © 2025 Hardcore Engineering Inc. +// Copyright © 2026 Intabia Fusion. // // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may @@ -13,4 +13,8 @@ // limitations under the License. // -export * from './client' +import pulsePlugin, { pulseId } from './plugin' + +export * from './types' +export { pulseId } +export default pulsePlugin diff --git a/plugins/pulse/src/plugin.ts b/plugins/pulse/src/plugin.ts new file mode 100644 index 0000000000..1193cc838b --- /dev/null +++ b/plugins/pulse/src/plugin.ts @@ -0,0 +1,32 @@ +// +// Copyright © 2026 Intabia Fusion. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Ref } from '@hcengineering/core' +import type { Plugin } from '@hcengineering/platform' +import { plugin } from '@hcengineering/platform' +import type { DocumentPresence, TypingIndicator } from './types' + +/** @public */ +export const pulseId = 'pulse' as Plugin + +/** @public */ +const pulsePlugin = plugin(pulseId, { + class: { + DocumentPresence: '' as Ref>, + TypingIndicator: '' as Ref> + } +}) + +export default pulsePlugin diff --git a/plugins/pulse/src/types.ts b/plugins/pulse/src/types.ts new file mode 100644 index 0000000000..d6bd3a000e --- /dev/null +++ b/plugins/pulse/src/types.ts @@ -0,0 +1,33 @@ +// +// Copyright © 2026 Intabia Fusion. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { Class, Doc, PersonId, Ref, Timestamp } from '@hcengineering/core' +import type { Person } from '@hcengineering/contact' +import type { IntlString } from '@hcengineering/platform' + +/** @public */ +export interface DocumentPresence extends Doc { + objectId: Ref + objectClass: Ref> + person: Ref + lastActive: Timestamp +} + +/** @public */ +export interface TypingIndicator extends Doc { + objectId: string + socialId: PersonId + status?: IntlString +} diff --git a/packages/hulypulse-client/tsconfig.json b/plugins/pulse/tsconfig.json similarity index 99% rename from packages/hulypulse-client/tsconfig.json rename to plugins/pulse/tsconfig.json index b5ae22f6e4..7d78e05abb 100644 --- a/packages/hulypulse-client/tsconfig.json +++ b/plugins/pulse/tsconfig.json @@ -9,4 +9,4 @@ }, "include": ["src/**/*"], "exclude": ["node_modules", "lib", "dist", "types", "bundle"] -} \ No newline at end of file +} diff --git a/pods/front/src/__start.ts b/pods/front/src/__start.ts index 27d9bc9d8e..fc8cd76377 100644 --- a/pods/front/src/__start.ts +++ b/pods/front/src/__start.ts @@ -51,7 +51,6 @@ startFront(metricsContext, { PUBLIC_SCHEDULE_URL: process.env.PUBLIC_SCHEDULE_URL, CALDAV_SERVER_URL: process.env.CALDAV_SERVER_URL, EXPORT_URL: process.env.EXPORT_URL, - PULSE_URL: process.env.PULSE_URL, COMMUNICATION_API_ENABLED: process.env.COMMUNICATION_API_ENABLED, EXCLUDED_APPLICATIONS_FOR_ANONYMOUS: process.env.EXCLUDED_APPLICATIONS_FOR_ANONYMOUS, DISABLED_FEATURES: process.env.DISABLED_FEATURES ?? '', diff --git a/rush.json b/rush.json index c04e21467c..647ddc5303 100644 --- a/rush.json +++ b/rush.json @@ -2201,6 +2201,16 @@ "projectFolder": "plugins/desktop-downloads", "shouldPublish": false }, + { + "packageName": "@hcengineering/pulse", + "projectFolder": "plugins/pulse", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/model-pulse", + "projectFolder": "models/pulse", + "shouldPublish": false + }, { "packageName": "@hcengineering/desktop-downloads-assets", "projectFolder": "plugins/desktop-downloads-assets", @@ -2576,11 +2586,6 @@ "projectFolder": "packages/kvs-client", "shouldPublish": false }, - { - "packageName": "@hcengineering/hulypulse-client", - "projectFolder": "packages/hulypulse-client", - "shouldPublish": true - }, { "packageName": "@hcengineering/communication", "projectFolder": "plugins/communication", diff --git a/scripts/takeUpstream.sh b/scripts/takeUpstream.sh index b7676eb5da..bb32fa3eeb 100755 --- a/scripts/takeUpstream.sh +++ b/scripts/takeUpstream.sh @@ -6,5 +6,4 @@ git subtree pull --prefix=foundations/server git@github.com:hcengineering/huly.s git subtree pull --prefix=foundations/net git@github.com:hcengineering/huly.net.git main git subtree pull --prefix=foundations/hulylake git@github.com:hcengineering/hulylake.git master -git subtree pull --prefix=foundations/hulypulse git@github.com:hcengineering/hulypulse.git main git subtree pull --prefix=foundations/communication git@github.com:hcengineering/communication.git main diff --git a/server/front/src/index.ts b/server/front/src/index.ts index 550d3660e9..188826670b 100644 --- a/server/front/src/index.ts +++ b/server/front/src/index.ts @@ -275,7 +275,6 @@ export function start ( mailUrl?: string billingUrl?: string paymentUrl?: string - pulseUrl?: string hulylakeUrl?: string datalakeUrl?: string }, @@ -354,7 +353,6 @@ export function start ( MAIL_URL: config.mailUrl, BILLING_URL: config.billingUrl, PAYMENT_URL: config.paymentUrl, - PULSE_URL: config.pulseUrl, HULYLAKE_URL: config.hulylakeUrl, DATALAKE_URL: config.datalakeUrl, ...(extraConfig ?? {}) diff --git a/server/front/src/starter.ts b/server/front/src/starter.ts index 47a289d880..61d7f8ea47 100644 --- a/server/front/src/starter.ts +++ b/server/front/src/starter.ts @@ -98,11 +98,6 @@ export function startFront (ctx: MeasureContext, extraConfig?: Record { + let leftSideMenuPage: LeftSideMenuPage + let channelPage: ChannelPage + let loginPage: LoginPage + let api: ApiEndpoint + let newUser2: SignUpData + let data: { workspaceName: string, userName: string, firstName: string, lastName: string, channelName: string } + + test.beforeEach(async ({ page, request }) => { + data = generateTestData() + newUser2 = generateUser() + + leftSideMenuPage = new LeftSideMenuPage(page) + channelPage = new ChannelPage(page) + loginPage = new LoginPage(page) + api = new ApiEndpoint(request) + await api.createAccount(data.userName, '1234', data.firstName, data.lastName) + await api.createWorkspaceWithLogin(data.workspaceName, data.userName, '1234') + await (await page.goto(`${PlatformURI}`))?.finished() + await loginPage.login(data.userName, '1234') + const swp = new SelectWorkspacePage(page) + await swp.selectWorkspace(data.workspaceName) + }) + + test('Second user sees typing indicator while first user types in general channel', async ({ + browser, + page, + request + }) => { + const linkText = await getInviteLink(page) + await createAccount(request, newUser2) + using _page2 = await getSecondPageByInvite(browser, linkText, newUser2) + const page2 = _page2.page + + const channelPageSecond = new ChannelPage(page2) + const leftSideMenuPageSecond = new LeftSideMenuPage(page2) + + await leftSideMenuPage.clickChunter() + await channelPage.clickChooseChannel('general') + + await leftSideMenuPageSecond.clickChunter() + await channelPageSecond.clickChooseChannel('general') + + await channelPage.inputMessage().click() + await channelPage.inputMessage().pressSequentially('hello there', { delay: 80 }) + + const typingInfo = page2.locator('span[data-id="channel-typing-info"]') + await expect(typingInfo).toContainText(data.firstName, { timeout: 8000 }) + + await channelPage.buttonSendMessage().click() + await expect(typingInfo).not.toContainText(data.firstName, { timeout: 10000 }) + }) + + test('First user sees second user as document presence viewer in general channel', async ({ + browser, + page, + request + }) => { + const linkText = await getInviteLink(page) + await createAccount(request, newUser2) + + await leftSideMenuPage.clickChunter() + await channelPage.clickChooseChannel('general') + + // Presence avatars on page1 should be empty (only self — filtered out) + const presenceFirst = page.locator('[data-id="document-presence"]') + await expect(presenceFirst).toHaveCount(1) + await expect(presenceFirst.locator('.hulyCombineAvatar, .avatar-button')).toHaveCount(0) + + // Second user joins and opens the same channel + using _page2 = await getSecondPageByInvite(browser, linkText, newUser2) + const page2 = _page2.page + const channelPageSecond = new ChannelPage(page2) + const leftSideMenuPageSecond = new LeftSideMenuPage(page2) + await leftSideMenuPageSecond.clickChunter() + await channelPageSecond.clickChooseChannel('general') + + // First user should now see second user avatar via DocumentPresence + await expect(presenceFirst.locator('.hulyCombineAvatar, .avatar-button')).toHaveCount(1, { timeout: 10000 }) + + // When second user leaves the channel (switches to another), TTL expires and avatar should disappear + await channelPageSecond.clickChooseChannel('random') + await expect(presenceFirst.locator('.hulyCombineAvatar, .avatar-button')).toHaveCount(0, { timeout: 20000 }) + }) +})