Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ PATHS_LOGS_FILE=./dev/log/graphql-api.log
PATHS_CONNECT_STATUS_FILE_PATH=./dev/connectStatus.json # Connect plugin status file
PATHS_OIDC_JSON=./dev/configs/oidc.local.json
PATHS_LOCAL_SESSION_FILE=./dev/local-session
PATHS_CONNECT_STATUS=./dev/states/connectStatus.json # Connect status file for development
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add quotes around the value on line 22.

The .env file format requires quoted values for consistent parsing by dotenv libraries.

Apply this fix:

-PATHS_CONNECT_STATUS=./dev/states/connectStatus.json # Connect status file for development
+PATHS_CONNECT_STATUS="./dev/states/connectStatus.json" # Connect status file for development
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PATHS_CONNECT_STATUS=./dev/states/connectStatus.json # Connect status file for development
PATHS_CONNECT_STATUS="./dev/states/connectStatus.json" # Connect status file for development
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 22-22: [UnorderedKey] The PATHS_CONNECT_STATUS key should go before the PATHS_CONNECT_STATUS_FILE_PATH key

(UnorderedKey)


[warning] 22-22: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In api/.env.development around line 22, the PATHS_CONNECT_STATUS value is
unquoted; update the line to wrap the value in quotes so dotenv parsers handle
it consistently (e.g., change
PATHS_CONNECT_STATUS=./dev/states/connectStatus.json to
PATHS_CONNECT_STATUS="./dev/states/connectStatus.json").

ENVIRONMENT="development"
NODE_ENV="development"
PORT="3001"
PLAYGROUND=true
INTROSPECTION=true
MOTHERSHIP_GRAPHQL_LINK="http://authenticator:3000/graphql"
MOTHERSHIP_GRAPHQL_LINK="wss://preview.mothership2.unraid.net"
MOTHERSHIP_BASE_URL="https://preview.mothership2.unraid.net"
NODE_TLS_REJECT_UNAUTHORIZED=0
BYPASS_PERMISSION_CHECKS=false
BYPASS_CORS_CHECKS=true
Expand Down
2 changes: 1 addition & 1 deletion api/dev/configs/api.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "4.25.3",
"version": "4.27.2",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],
Expand Down
2 changes: 1 addition & 1 deletion api/dev/configs/connect.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"wanaccess": true,
"wanport": 8443,
"upnpEnabled": false,
"apikey": "",
"apikey": "_______________________LOCAL_API_KEY_HERE_________________________",
"localApiKey": "_______________________LOCAL_API_KEY_HERE_________________________",
"email": "test@example.com",
"username": "zspearmint",
Expand Down
7 changes: 7 additions & 0 deletions api/dev/states/connectStatus.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"connectionStatus": "PRE_INIT",
"error": null,
"lastPing": null,
"allowedOrigins": "",
"timestamp": 1764601989840
}
2 changes: 1 addition & 1 deletion api/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { exit } from 'process';
import type { PackageJson } from 'type-fest';
import { $, cd } from 'zx';

import { getDeploymentVersion } from './get-deployment-version.js';
import { getDeploymentVersion } from '@app/../scripts/get-deployment-version.js';

type ApiPackageJson = PackageJson & {
version: string;
Expand Down
9 changes: 9 additions & 0 deletions api/src/unraid-api/unraid-file-modifier/file-modification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { applyPatch, createPatch, parsePatch, reversePatch } from 'diff';
import { coerce, compare, gte, lte } from 'semver';

import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version.js';
import { NODE_ENV } from '@app/environment.js';

export type ModificationEffect = 'nginx:reload';

Expand Down Expand Up @@ -225,6 +226,14 @@ export abstract class FileModification {
throw new Error('Invalid file modification configuration');
}

// Skip file modifications in development mode
if (NODE_ENV === 'development') {
return {
shouldApply: false,
reason: 'File modifications are disabled in development mode',
};
}

const fileExists = await access(this.filePath, constants.R_OK | constants.W_OK)
.then(() => true)
.catch(() => false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { ConfigService } from '@nestjs/config';

import type { ModificationEffect } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
import { NODE_ENV } from '@app/environment.js';
import { FileModificationEffectService } from '@app/unraid-api/unraid-file-modifier/file-modification-effect.service.js';
import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js';

Expand All @@ -29,6 +30,11 @@ export class UnraidFileModificationService
*/
async onModuleInit() {
try {
if (NODE_ENV === 'development') {
this.logger.log('Skipping file modifications in development mode');
return;
}

this.logger.log('Loading file modifications...');
const mods = await this.loadModifications();
await this.applyModifications(mods);
Expand Down
21 changes: 1 addition & 20 deletions packages/unraid-api-plugin-connect/codegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,7 @@ const config: CodegenConfig = {
},
},
generates: {
// Generate Types for Mothership GraphQL Client
'src/graphql/generated/client/': {
documents: './src/graphql/**/*.ts',
schema: {
[process.env.MOTHERSHIP_GRAPHQL_LINK ?? 'https://staging.mothership.unraid.net/ws']: {
headers: {
origin: 'https://forums.unraid.net',
},
},
},
preset: 'client',
presetConfig: {
gqlTagName: 'graphql',
},
config: {
useTypeImports: true,
withObjectType: true,
},
plugins: [{ add: { content: '/* eslint-disable */' } }],
},
// No longer generating mothership GraphQL types since we switched to WebSocket-based UnraidServerClient
},
};

Expand Down
3 changes: 2 additions & 1 deletion packages/unraid-api-plugin-connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"build": "tsc",
"prepare": "npm run build",
"format": "prettier --write \"src/**/*.{ts,js,json}\"",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.ts"
"codegen": "graphql-codegen --config codegen.ts"
},
"keywords": [
"unraid",
Expand Down Expand Up @@ -57,6 +57,7 @@
"jose": "6.0.13",
"lodash-es": "4.17.21",
"nest-authz": "2.17.0",
"pify": "^6.1.0",
"prettier": "3.6.2",
"rimraf": "6.0.1",
"rxjs": "7.8.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class CloudService {
}

private async hardCheckDns() {
const mothershipGqlUri = this.configService.getOrThrow<string>('MOTHERSHIP_GRAPHQL_LINK');
const mothershipGqlUri = this.configService.getOrThrow<string>('MOTHERSHIP_BASE_URL');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for all references to MOTHERSHIP_BASE_URL and MOTHERSHIP_GRAPHQL_LINK
echo "=== MOTHERSHIP_BASE_URL references ==="
rg -n "MOTHERSHIP_BASE_URL" --type ts --type tsx --type js --type jsx

echo -e "\n=== MOTHERSHIP_GRAPHQL_LINK references ==="
rg -n "MOTHERSHIP_GRAPHQL_LINK" --type ts --type tsx --type js --type jsx

echo -e "\n=== Environment configuration files ==="
fd -e env -e env.example --type f

Repository: unraid/api

Length of output: 250


🏁 Script executed:

# Retry search without problematic file types
echo "=== MOTHERSHIP_BASE_URL references ==="
rg -n "MOTHERSHIP_BASE_URL"

echo -e "\n=== MOTHERSHIP_GRAPHQL_LINK references ==="
rg -n "MOTHERSHIP_GRAPHQL_LINK"

echo -e "\n=== Environment files ==="
find . -name ".env*" -type f | grep -v node_modules | head -20

Repository: unraid/api

Length of output: 1773


🏁 Script executed:

# Read the cloud.service.ts file to understand the context
cat -n packages/unraid-api-plugin-connect/src/connection-status/cloud.service.ts | sed -n '90,110p'

echo -e "\n=== Context around line 207 ==="
cat -n packages/unraid-api-plugin-connect/src/connection-status/cloud.service.ts | sed -n '200,220p'

echo -e "\n=== Check environment.ts for MOTHERSHIP_BASE_URL ==="
rg -n "MOTHERSHIP_BASE_URL" api/src/environment.ts

Repository: unraid/api

Length of output: 2233


🏁 Script executed:

# Check environment.ts for configuration definitions
cat -n api/src/environment.ts | sed -n '90,105p'

echo -e "\n=== Full search for MOTHERSHIP_BASE_URL in entire repo ==="
find . -type f \( -name "*.ts" -o -name "*.js" -o -name ".env*" \) ! -path "*/node_modules/*" -exec grep -l "MOTHERSHIP_BASE_URL" {} \;

Repository: unraid/api

Length of output: 1184


🏁 Script executed:

# Check .env.development for MOTHERSHIP_BASE_URL
cat -n api/.env.development | grep -A2 -B2 "MOTHERSHIP"

echo -e "\n=== Full content of .env.development ==="
cat api/.env.development

Repository: unraid/api

Length of output: 2102


🏁 Script executed:

# Check other .env files for MOTHERSHIP_BASE_URL
echo "=== .env.staging ==="
grep "MOTHERSHIP" api/.env.staging

echo -e "\n=== .env.production ==="
grep "MOTHERSHIP" api/.env.production

echo -e "\n=== .env.test ==="
grep "MOTHERSHIP" api/.env.test

echo -e "\n=== plugin/.env.example ==="
grep "MOTHERSHIP" plugin/.env.example

echo -e "\n=== Check if MOTHERSHIP_BASE_URL should be in environment.ts ==="
rg -n "export const MOTHERSHIP" api/src/environment.ts

Repository: unraid/api

Length of output: 417


🏁 Script executed:

# Verify configService type and how it accesses config
rg -n "configService.getOrThrow" packages/unraid-api-plugin-connect/src/connection-status/cloud.service.ts | head -5

echo -e "\n=== Check ConfigService implementation ==="
fd -n "config.service" packages/unraid-api-plugin-connect/src --type f

echo -e "\n=== Check if there's a NestJS ConfigModule setup ==="
rg -n "ConfigModule" packages/unraid-api-plugin-connect/src --type ts | head -10

Repository: unraid/api

Length of output: 1415


Critical: Incomplete refactoring with missing environment configuration for MOTHERSHIP_BASE_URL.

Line 207 uses MOTHERSHIP_BASE_URL, but this config key is only defined in .env.development and missing from .env.staging, .env.production, and .env.test. Additionally, it is not exported from environment.ts like MOTHERSHIP_GRAPHQL_LINK is, causing configService.getOrThrow() to fail at runtime in non-development environments.

Furthermore, hardCheckCloud (line 96) still uses MOTHERSHIP_GRAPHQL_LINK, creating an inconsistency where DNS resolution (hardCheckDns, line 207) validates a different configuration than what canReachMothership and checkMothershipAuthentication use (line 98–106). The variable name mothershipGqlUri on line 207 is misleading when assigned a base URL instead of a GraphQL URL.

Required fixes:

  1. Add MOTHERSHIP_BASE_URL to all .env.* files (staging, production, test) with appropriate values
  2. Export MOTHERSHIP_BASE_URL from api/src/environment.ts with defaults, matching the pattern used for MOTHERSHIP_GRAPHQL_LINK
  3. Decide on the intended behavior:
    • If both URLs should be identical, update line 96 to use MOTHERSHIP_BASE_URL and rename variables consistently
    • If URLs intentionally differ, document why DNS checks validate a different hostname than connection attempts
  4. Rename mothershipGqlUri to mothershipBaseUrl on line 207 for clarity
🤖 Prompt for AI Agents
In packages/unraid-api-plugin-connect/src/connection-status/cloud.service.ts
around line 207 (and related usages at lines ~96 and 98–106), the code reads
MOTHERSHIP_BASE_URL via configService.getOrThrow while that key is missing from
non-development .env files and not exported from api/src/environment.ts, and
there is an inconsistency between hardCheckCloud (using MOTHERSHIP_GRAPHQL_LINK)
and DNS checks (using MOTHERSHIP_BASE_URL) plus a misleading variable name
mothershipGqlUri; fix by (1) adding MOTHERSHIP_BASE_URL to all
.env.staging/.env.production/.env.test with appropriate values, (2) export
MOTHERSHIP_BASE_URL from api/src/environment.ts with a sensible default
following the existing MOTHERSHIP_GRAPHQL_LINK pattern, (3) decide whether the
base URL and GraphQL link should be the same—if yes, change hardCheckCloud to
use MOTHERSHIP_BASE_URL and align variable names; if they must differ, add
comments documenting why DNS checks validate a different host than connection
attempts, and (4) rename mothershipGqlUri to mothershipBaseUrl at line 207 and
update any usages to reflect the consistent naming.

const hostname = new URL(mothershipGqlUri).host;
const lookup = promisify(lookupDNS);
const resolve = promisify(resolveDNS);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Injectable, Logger, OnApplicationBootstrap, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import { unlink, writeFile } from 'fs/promises';
import { mkdir, unlink, writeFile } from 'fs/promises';
import { dirname } from 'path';

import { ConfigType, ConnectionMetadata } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
Expand All @@ -13,8 +14,8 @@ export class ConnectStatusWriterService implements OnApplicationBootstrap, OnMod
private logger = new Logger(ConnectStatusWriterService.name);

get statusFilePath() {
// Use environment variable if provided, otherwise use default path
return process.env.PATHS_CONNECT_STATUS_FILE_PATH ?? '/var/local/emhttp/connectStatus.json';
// Use environment variable if set, otherwise default to /var/local/emhttp/connectStatus.json
return this.configService.get('PATHS_CONNECT_STATUS') || '/var/local/emhttp/connectStatus.json';
}

async onApplicationBootstrap() {
Expand Down Expand Up @@ -59,6 +60,10 @@ export class ConnectStatusWriterService implements OnApplicationBootstrap, OnMod
const data = JSON.stringify(statusData, null, 2);
this.logger.verbose(`Writing connection status: ${data}`);

// Ensure the directory exists before writing
const dir = dirname(this.statusFilePath);
await mkdir(dir, { recursive: true });

await writeFile(this.statusFilePath, data);
this.logger.verbose(`Status written to ${this.statusFilePath}`);
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Import from the generated directory
import { graphql } from '../graphql/generated/client/gql.js';
import { graphql } from './generated/client/gql.js';

export const SEND_REMOTE_QUERY_RESPONSE = graphql(/* GraphQL */ `
mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { gql } from '@apollo/client/core/index.js';
import { parse, print, visit } from 'graphql';

import {
CANONICAL_INTERNAL_CLIENT_TOKEN,
type CanonicalInternalClientService,
} from '@unraid/shared';

interface GraphQLExecutor {
execute(params: {
query: string
variables?: Record<string, any>
operationName?: string
operationType?: 'query' | 'mutation' | 'subscription'
}): Promise<any>
stopSubscription?(operationId: string): Promise<void>
}
Comment on lines +10 to +18
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace any types with proper typing.

The interface uses any in multiple places, violating the coding guideline: "Never use the any type." Define proper types for the GraphQL execution contract.

+interface GraphQLResult<T = Record<string, unknown>> {
+  data?: T;
+  errors?: Array<{
+    message: string;
+    extensions?: Record<string, unknown>;
+  }>;
+}
+
 interface GraphQLExecutor {
   execute(params: {
     query: string
-    variables?: Record<string, any>
+    variables?: Record<string, unknown>
     operationName?: string
     operationType?: 'query' | 'mutation' | 'subscription'
-  }): Promise<any>
+  }): Promise<GraphQLResult>
   stopSubscription?(operationId: string): Promise<void>
 }

As per coding guidelines, avoid any in favor of proper typing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface GraphQLExecutor {
execute(params: {
query: string
variables?: Record<string, any>
operationName?: string
operationType?: 'query' | 'mutation' | 'subscription'
}): Promise<any>
stopSubscription?(operationId: string): Promise<void>
}
interface GraphQLResult<T = Record<string, unknown>> {
data?: T;
errors?: Array<{
message: string;
extensions?: Record<string, unknown>;
}>;
}
interface GraphQLExecutor {
execute(params: {
query: string
variables?: Record<string, unknown>
operationName?: string
operationType?: 'query' | 'mutation' | 'subscription'
}): Promise<GraphQLResult>
stopSubscription?(operationId: string): Promise<void>
}


/**
* Local GraphQL executor that maps remote queries to local API calls
*/
@Injectable()
export class LocalGraphQLExecutor implements GraphQLExecutor {
private readonly logger = new Logger(LocalGraphQLExecutor.name);

constructor(
@Inject(CANONICAL_INTERNAL_CLIENT_TOKEN)
private readonly internalClient: CanonicalInternalClientService,
) {}

async execute(params: {
query: string
variables?: Record<string, any>
operationName?: string
operationType?: 'query' | 'mutation' | 'subscription'
}): Promise<any> {
const { query, variables, operationName, operationType } = params;

try {
this.logger.debug(`Executing ${operationType} operation: ${operationName || 'unnamed'}`);
this.logger.verbose(`Query: ${query}`);
this.logger.verbose(`Variables: ${JSON.stringify(variables)}`);

// Transform remote query to local query by removing "remote" prefixes
const localQuery = this.transformRemoteQueryToLocal(query);

// Execute the transformed query against local API
const client = await this.internalClient.getClient();
const result = await client.query({
query: gql`${localQuery}`,
variables,
});
Comment on lines +48 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Execute method ignores operationType parameter.

The method accepts operationType but always calls client.query(). Mutations should use client.mutate() to ensure proper behavior.

       // Execute the transformed query against local API
       const client = await this.internalClient.getClient();
-      const result = await client.query({
-        query: gql`${localQuery}`,
-        variables,
-      });
+      let result;
+      if (operationType === 'mutation') {
+        result = await client.mutate({
+          mutation: gql`${localQuery}`,
+          variables,
+        });
+      } else {
+        result = await client.query({
+          query: gql`${localQuery}`,
+          variables,
+        });
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Execute the transformed query against local API
const client = await this.internalClient.getClient();
const result = await client.query({
query: gql`${localQuery}`,
variables,
});
// Execute the transformed query against local API
const client = await this.internalClient.getClient();
let result;
if (operationType === 'mutation') {
result = await client.mutate({
mutation: gql`${localQuery}`,
variables,
});
} else {
result = await client.query({
query: gql`${localQuery}`,
variables,
});
}
🤖 Prompt for AI Agents
In
packages/unraid-api-plugin-connect-2/src/mothership-proxy/local-graphql-executor.service.ts
around lines 48 to 53, the method ignores the operationType parameter and always
calls client.query(); change the logic to branch on operationType: if it's
'query' call client.query({ query: gql`${localQuery}`, variables }), if it's
'mutation' call client.mutate({ mutation: gql`${localQuery}`, variables }); keep
using gql for the document, pass the same variables, and return the
mutation/query result as before (no other API changes).


return {
data: result.data,
};
} catch (error: any) {
this.logger.error(`GraphQL execution error: ${error?.message}`);
return {
errors: [
{
message: error?.message || 'Unknown error',
extensions: { code: 'EXECUTION_ERROR' },
},
],
};
}
}

/**
* Transform remote GraphQL query to local query by removing "remote" prefixes
*/
private transformRemoteQueryToLocal(query: string): string {
try {
// Parse the GraphQL query
const document = parse(query);

// Transform the document by removing "remote" prefixes
const transformedDocument = visit(document, {
// Transform operation names (e.g., remoteGetDockerInfo -> getDockerInfo)
OperationDefinition: (node) => {
if (node.name?.value.startsWith('remote')) {
return {
...node,
name: {
...node.name,
value: this.removeRemotePrefix(node.name.value),
},
};
}
return node;
},
// Transform field names (e.g., remoteGetDockerInfo -> docker, remoteGetVms -> vms)
Field: (node) => {
if (node.name.value.startsWith('remote')) {
return {
...node,
name: {
...node.name,
value: this.transformRemoteFieldName(node.name.value),
},
};
}
return node;
},
});

// Convert back to string
return print(transformedDocument);
} catch (error) {
this.logger.error(`Failed to parse/transform GraphQL query: ${error}`);
throw error;
}
}

/**
* Remove "remote" prefix from operation names
*/
private removeRemotePrefix(name: string): string {
if (name.startsWith('remote')) {
// remoteGetDockerInfo -> getDockerInfo
return name.slice(6); // Remove "remote"
}
return name;
}

/**
* Transform remote field names to local equivalents
*/
private transformRemoteFieldName(fieldName: string): string {
// Handle common patterns
if (fieldName === 'remoteGetDockerInfo') {
return 'docker';
}
if (fieldName === 'remoteGetVms') {
return 'vms';
}
if (fieldName === 'remoteGetSystemInfo') {
return 'system';
}

// Generic transformation: remove "remoteGet" and convert to camelCase
if (fieldName.startsWith('remoteGet')) {
const baseName = fieldName.slice(9); // Remove "remoteGet"
return baseName.charAt(0).toLowerCase() + baseName.slice(1);
}

// Remove "remote" prefix as fallback
if (fieldName.startsWith('remote')) {
const baseName = fieldName.slice(6); // Remove "remote"
return baseName.charAt(0).toLowerCase() + baseName.slice(1);
}

return fieldName;
}

async stopSubscription(operationId: string): Promise<void> {
this.logger.debug(`Stopping subscription: ${operationId}`);
// Subscription cleanup logic would go here
}
}
Loading
Loading