chore: move to independent docker events (#1334)

- update dockerode to v4
- remove docker-event-emitter dependency

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced a real-time Docker event monitoring service that improves
container status updates.
- Consolidated Docker functionalities within a dedicated module for
enhanced management.

- **Refactor**
- Streamlined Docker event handling to boost stability and simplify
operations.
- Updated the structure of the resolvers module to encapsulate
Docker-related functionality.
  - Made certain methods public for easier access and interaction.

- **Chores**
- Upgraded Docker-related dependencies and removed deprecated packages
to improve performance.
  - Removed unused imports to clean up the codebase.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Eli Bosley
2025-04-07 10:47:13 -04:00
committed by GitHub
parent 1bbe7d27b0
commit 23c60dad0c
11 changed files with 682 additions and 380 deletions
+1 -2
View File
@@ -87,8 +87,7 @@
"cron": "3.5.0",
"cross-fetch": "^4.0.0",
"diff": "^7.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dockerode": "^4.0.5",
"dotenv": "^16.4.5",
"execa": "^9.5.1",
"exit-hook": "^4.0.0",
-1
View File
@@ -7,7 +7,6 @@ import { LoggerModule } from 'nestjs-pino';
import { apiLogger } from '@app/core/log.js';
import { LOG_LEVEL } from '@app/environment.js';
import { AuthInterceptor } from '@app/unraid-api/auth/auth.interceptor.js';
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
import { AuthenticationGuard } from '@app/unraid-api/auth/authentication.guard.js';
import { CronModule } from '@app/unraid-api/cron/cron.module.js';
+3 -5
View File
@@ -1,9 +1,8 @@
import type { ApolloDriverConfig } from '@nestjs/apollo';
import { ApolloDriver } from '@nestjs/apollo';
import { Module, UnauthorizedException } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ApolloServerPlugin } from '@apollo/server';
import { NoUnusedVariablesRule, print } from 'graphql';
import {
DateTimeResolver,
@@ -12,7 +11,6 @@ import {
URLResolver,
UUIDResolver,
} from 'graphql-scalars';
import { AuthZService } from 'nest-authz';
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long.js';
import { loadTypeDefs } from '@app/graphql/schema/loadTypesDefs.js';
@@ -31,8 +29,8 @@ import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
GraphQLModule.forRootAsync<ApolloDriverConfig>({
driver: ApolloDriver,
imports: [PluginModule, AuthModule],
inject: [PluginService, AuthZService],
useFactory: async (pluginService: PluginService, authZService: AuthZService) => {
inject: [PluginService],
useFactory: async (pluginService: PluginService) => {
const plugins = await pluginService.getGraphQLConfiguration();
const authEnumTypeDefs = getAuthEnumTypeDefs();
const typeDefs = print(await loadTypeDefs([plugins.typeDefs, authEnumTypeDefs]));
@@ -0,0 +1,254 @@
import { Logger } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { PassThrough, Readable } from 'stream';
import Docker from 'dockerode';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
// Mock chokidar
vi.mock('chokidar', () => ({
watch: vi.fn().mockReturnValue({
on: vi.fn().mockReturnThis(),
}),
}));
// Mock @nestjs/common
vi.mock('@nestjs/common', async () => {
const actual = await vi.importActual('@nestjs/common');
return {
...actual,
Injectable: () => vi.fn(),
Logger: vi.fn().mockImplementation(() => ({
debug: vi.fn(),
error: vi.fn(),
log: vi.fn(),
})),
};
});
// Mock store getters
vi.mock('@app/store/index.js', () => ({
getters: {
paths: vi.fn().mockReturnValue({
'var-run': '/var/run',
'docker-socket': '/var/run/docker.sock',
}),
},
}));
// Mock DockerService
vi.mock('./docker.service.js', () => ({
DockerService: vi.fn().mockImplementation(() => ({
getDockerClient: vi.fn(),
debouncedContainerCacheUpdate: vi.fn(),
})),
}));
describe('DockerEventService', () => {
let service: DockerEventService;
let dockerService: DockerService;
let mockDockerClient: Docker;
let mockEventStream: PassThrough;
let mockLogger: Logger;
let module: TestingModule;
beforeEach(async () => {
// Create a mock Docker client
mockDockerClient = {
getEvents: vi.fn(),
} as unknown as Docker;
// Create a mock Docker service *instance*
const mockDockerServiceImpl = {
getDockerClient: vi.fn().mockReturnValue(mockDockerClient),
debouncedContainerCacheUpdate: vi.fn(),
};
// Create a mock event stream
mockEventStream = new PassThrough();
// Set up the mock Docker client to return our mock event stream
vi.spyOn(mockDockerClient, 'getEvents').mockResolvedValue(
mockEventStream as unknown as Readable
);
// Create a mock logger
mockLogger = new Logger(DockerEventService.name) as Logger;
// Use the mock implementation in the testing module
module = await Test.createTestingModule({
providers: [
DockerEventService,
{
provide: DockerService,
useValue: mockDockerServiceImpl,
},
],
}).compile();
service = module.get<DockerEventService>(DockerEventService);
dockerService = module.get<DockerService>(DockerService);
});
afterEach(() => {
vi.clearAllMocks();
if (service['dockerEventStream']) {
service.stopEventStream();
}
module.close();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
const waitForEventProcessing = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));
it('should process Docker events correctly', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const event = {
Type: 'container',
Action: 'start',
id: '123',
from: 'test-image',
time: Date.now(),
timeNano: Date.now() * 1000000,
};
mockEventStream.write(JSON.stringify(event) + '\n');
await waitForEventProcessing();
expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalled();
});
it('should ignore non-watched actions', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const event = {
Type: 'container',
Action: 'unknown',
id: '123',
from: 'test-image',
time: Date.now(),
timeNano: Date.now() * 1000000,
};
mockEventStream.write(JSON.stringify(event) + '\n');
await waitForEventProcessing();
expect(dockerService.debouncedContainerCacheUpdate).not.toHaveBeenCalled();
});
it('should handle malformed JSON gracefully', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const malformedJson = '{malformed json}\n';
mockEventStream.write(malformedJson);
const validEvent = { Type: 'container', Action: 'start', id: '456' };
mockEventStream.write(JSON.stringify(validEvent) + '\n');
await waitForEventProcessing();
expect(service.isActive()).toBe(true);
expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
});
it('should handle multiple JSON bodies in a single chunk', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const events = [
{ Type: 'container', Action: 'start', id: '123', from: 'test-image-1' },
{ Type: 'container', Action: 'stop', id: '456', from: 'test-image-2' },
];
mockEventStream.write(events.map((event) => JSON.stringify(event)).join('\n') + '\n');
await waitForEventProcessing();
expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(2);
});
it('should handle mixed valid and invalid JSON in a single chunk', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const validEvent = { Type: 'container', Action: 'start', id: '123', from: 'test-image' };
const invalidJson = '{malformed json}';
mockEventStream.write(JSON.stringify(validEvent) + '\n' + invalidJson + '\n');
await waitForEventProcessing();
expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
expect(service.isActive()).toBe(true);
});
it('should handle empty lines in a chunk', async () => {
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const event = { Type: 'container', Action: 'start', id: '123', from: 'test-image' };
mockEventStream.write('\n\n' + JSON.stringify(event) + '\n\n');
await waitForEventProcessing();
expect(dockerService.debouncedContainerCacheUpdate).toHaveBeenCalledTimes(1);
expect(service.isActive()).toBe(true);
});
it('should handle stream errors gracefully', async () => {
const stopSpy = vi.spyOn(service, 'stopEventStream');
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true);
const testError = new Error('Stream error');
mockEventStream.emit('error', testError);
await waitForEventProcessing();
expect(service.isActive()).toBe(false);
expect(stopSpy).toHaveBeenCalled();
});
it('should clean up resources when stopped', async () => {
// Start the event stream
await service['setupDockerWatch']();
expect(service.isActive()).toBe(true); // Ensure it started
// Check if the stream exists before spying
const stream = service['dockerEventStream'];
let removeListenersSpy: any, destroySpy: any;
if (stream) {
removeListenersSpy = vi.spyOn(stream, 'removeAllListeners');
destroySpy = vi.spyOn(stream, 'destroy');
}
// Stop the event stream
service.stopEventStream();
// Verify that the service has stopped
expect(service.isActive()).toBe(false);
// Verify stream methods were called if the stream existed
if (removeListenersSpy) {
expect(removeListenersSpy).toHaveBeenCalled();
}
if (destroySpy) {
expect(destroySpy).toHaveBeenCalled();
}
});
});
@@ -0,0 +1,198 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { Readable } from 'stream';
import { watch } from 'chokidar';
import Docker from 'dockerode';
import { getters } from '@app/store/index.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
enum DockerEventAction {
DIE = 'die',
KILL = 'kill',
OOM = 'oom',
PAUSE = 'pause',
RESTART = 'restart',
START = 'start',
STOP = 'stop',
UNPAUSE = 'unpause',
EXEC_CREATE = 'exec_create',
EXEC_START = 'exec_start',
EXEC_DIE = 'exec_die',
}
enum DockerEventType {
CONTAINER = 'container',
}
interface DockerEvent {
Action?: string;
status?: string;
from?: string;
Type?: string;
[key: string]: unknown;
}
@Injectable()
export class DockerEventService implements OnModuleDestroy, OnModuleInit {
private client: Docker;
private dockerEventStream: Readable | null = null;
private readonly logger = new Logger(DockerEventService.name);
private watchedActions = [
DockerEventAction.DIE,
DockerEventAction.KILL,
DockerEventAction.OOM,
DockerEventAction.PAUSE,
DockerEventAction.RESTART,
DockerEventAction.START,
DockerEventAction.STOP,
DockerEventAction.UNPAUSE,
DockerEventAction.EXEC_CREATE,
DockerEventAction.EXEC_START,
DockerEventAction.EXEC_DIE,
];
private containerActions = [
DockerEventAction.DIE,
DockerEventAction.KILL,
DockerEventAction.OOM,
DockerEventAction.PAUSE,
DockerEventAction.RESTART,
DockerEventAction.START,
DockerEventAction.STOP,
DockerEventAction.UNPAUSE,
];
constructor(private readonly dockerService: DockerService) {
this.client = this.dockerService.getDockerClient();
}
async onModuleInit() {
this.setupVarRunWatch();
}
onModuleDestroy() {
this.stopEventStream();
}
private setupVarRunWatch() {
const paths = getters.paths();
watch(paths['var-run'], { ignoreInitial: false })
.on('add', async (path) => {
if (path === paths['docker-socket']) {
this.logger.debug('Starting docker event watch');
await this.setupDockerWatch();
}
})
.on('unlink', (path) => {
if (path === paths['docker-socket']) {
this.stopEventStream();
}
});
}
/**
* Stop the Docker event stream
*/
public stopEventStream(): void {
if (this.dockerEventStream) {
this.logger.debug('Stopping docker event stream');
this.dockerEventStream.removeAllListeners();
this.dockerEventStream.destroy();
this.dockerEventStream = null;
}
}
private async handleDockerEvent(event: unknown): Promise<void> {
if (typeof event !== 'object' || event === null) {
this.logger.error('Received non-object event', event);
return;
}
// Type assertion to DockerEvent
const dockerEvent = event as DockerEvent;
// Check if this is an action we're watching
const actionName = dockerEvent.Action || dockerEvent.status;
const shouldProcess = this.watchedActions.some(
(action) => typeof actionName === 'string' && actionName.startsWith(action)
);
if (shouldProcess) {
this.logger.debug(`[${dockerEvent.from}] ${dockerEvent.Type}->${actionName}`);
// For container lifecycle events, update the container cache
if (
dockerEvent.Type === DockerEventType.CONTAINER &&
typeof actionName === 'string' &&
this.containerActions.includes(actionName as DockerEventAction)
) {
await this.dockerService.debouncedContainerCacheUpdate();
}
}
}
private async setupDockerWatch(): Promise<void> {
this.logger.debug('Setting up Docker event stream');
try {
const eventStream = await this.client.getEvents();
this.dockerEventStream = eventStream as unknown as Readable;
if (this.dockerEventStream) {
// Add error handlers to raw stream to prevent uncaught errors
this.dockerEventStream.on('error', (error) => {
this.logger.error('Docker event stream error', error);
this.stopEventStream();
});
this.dockerEventStream.on('end', () => {
this.logger.debug('Docker event stream closed');
this.stopEventStream();
});
// Set up data handler for line-by-line JSON parsing
this.dockerEventStream.on('data', async (chunk) => {
try {
// Split the chunk by newlines to handle multiple JSON bodies
const jsonStrings = chunk
.toString()
.split('\n')
.filter((line) => line.trim() !== '');
for (const jsonString of jsonStrings) {
try {
const event = JSON.parse(jsonString);
await this.handleDockerEvent(event);
} catch (parseError) {
this.logger
.error(`Failed to parse individual Docker event: ${parseError instanceof Error ? parseError.message : String(parseError)}
Event data: ${jsonString}`);
}
}
} catch (error) {
this.logger.error(
`Failed to process Docker event chunk: ${error instanceof Error ? error.message : String(error)}`
);
this.logger.verbose(`Full chunk: ${chunk.toString()}`);
}
});
this.logger.debug('Docker event stream active');
}
} catch (error) {
this.logger.error(
`Failed to set up Docker event stream - ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Check if the Docker event service is currently running
* @returns True if the event stream is active
*/
public isActive(): boolean {
return this.dockerEventStream !== null;
}
}
@@ -0,0 +1,70 @@
import { Test, TestingModule } from '@nestjs/testing';
import { describe, expect, it, vi } from 'vitest';
import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
describe('DockerModule', () => {
it('should compile the module', async () => {
const module = await Test.createTestingModule({
imports: [DockerModule],
})
.overrideProvider(DockerService)
.useValue({ getDockerClient: vi.fn() })
.compile();
expect(module).toBeDefined();
});
it('should provide DockerService', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: DockerService,
useValue: {
getDockerClient: vi.fn(),
debouncedContainerCacheUpdate: vi.fn(),
},
},
],
}).compile();
const service = module.get<DockerService>(DockerService);
expect(service).toBeDefined();
expect(service).toHaveProperty('getDockerClient');
});
it('should provide DockerEventService', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
DockerEventService,
{ provide: DockerService, useValue: { getDockerClient: vi.fn() } },
],
}).compile();
const service = module.get<DockerEventService>(DockerEventService);
expect(service).toBeInstanceOf(DockerEventService);
});
it('should provide DockerResolver', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerResolver, { provide: DockerService, useValue: {} }],
}).compile();
const resolver = module.get<DockerResolver>(DockerResolver);
expect(resolver).toBeInstanceOf(DockerResolver);
});
it('should provide DockerMutationsResolver', async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerMutationsResolver, { provide: DockerService, useValue: {} }],
}).compile();
const resolver = module.get<DockerMutationsResolver>(DockerMutationsResolver);
expect(resolver).toBeInstanceOf(DockerMutationsResolver);
});
});
@@ -0,0 +1,20 @@
import { Module } from '@nestjs/common';
import { DockerEventService } from '@app/unraid-api/graph/resolvers/docker/docker-event.service.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
@Module({
providers: [
// Services
DockerService,
DockerEventService,
// Resolvers
DockerResolver,
DockerMutationsResolver,
],
exports: [DockerService, DockerEventService],
})
export class DockerModule {}
@@ -4,27 +4,9 @@ import { Test } from '@nestjs/testing';
import Docker from 'dockerode';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
import { ContainerState } from '@app/graphql/generated/api/types.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
// Mock chokidar
vi.mock('chokidar', () => ({
watch: vi.fn().mockReturnValue({
on: vi.fn().mockReturnValue({
on: vi.fn(),
}),
}),
}));
// Mock docker-event-emitter
vi.mock('docker-event-emitter', () => ({
default: vi.fn().mockReturnValue({
on: vi.fn(),
start: vi.fn().mockResolvedValue(undefined),
}),
}));
// Mock pubsub
vi.mock('@app/core/pubsub.js', () => ({
pubsub: {
@@ -522,225 +504,4 @@ describe('DockerService', () => {
});
});
});
describe('watchers', () => {
it('should setup docker watcher when docker socket is added', async () => {
// Mock the setupDockerWatch method
const setupDockerWatchSpy = vi.spyOn(service as any, 'setupDockerWatch');
setupDockerWatchSpy.mockResolvedValue({} as any);
// Get the watch function from chokidar
const { watch } = await import('chokidar');
// Mock the on method to simulate the add event
const mockOn = vi.fn().mockImplementation((event, callback) => {
if (event === 'add') {
// Simulate the add event with the docker socket path
callback('/var/run/docker.sock');
}
return { on: vi.fn() };
});
// Replace the watch function's on method
(watch as any).mockReturnValue({
on: mockOn,
});
// Call the setupVarRunWatch method
await (service as any).setupVarRunWatch();
// Verify that setupDockerWatch was called
expect(setupDockerWatchSpy).toHaveBeenCalled();
});
it('should stop docker watcher when docker socket is removed', async () => {
// Get the watch function from chokidar
const { watch } = await import('chokidar');
// Create a mock stop function
const mockStop = vi.fn();
// Set up the dockerWatcher before calling setupVarRunWatch
(service as any).dockerWatcher = { stop: mockStop };
// Mock the on method to simulate the unlink event
let unlinkCallback: (path: string) => void = () => {};
const mockOn = vi.fn().mockImplementation((event, callback) => {
if (event === 'unlink') {
unlinkCallback = callback;
}
return { on: mockOn };
});
// Replace the watch function's on method
(watch as any).mockReturnValue({
on: mockOn,
});
// Call the setupVarRunWatch method
(service as any).setupVarRunWatch();
// Verify that the on method was called with 'unlink'
expect(mockOn).toHaveBeenCalledWith('unlink', expect.any(Function));
expect(unlinkCallback).toBeDefined();
// Trigger the unlink event
unlinkCallback('/var/run/docker.sock');
// Verify that the stop method was called
expect(mockStop).toHaveBeenCalled();
expect((service as any).dockerWatcher).toBeNull();
expect((service as any).containerCache).toEqual([]);
});
it('should setup docker watch correctly', async () => {
// Get the DockerEE import
const DockerEE = (await import('docker-event-emitter')).default;
// Mock the debouncedContainerCacheUpdate method
const debouncedContainerCacheUpdateSpy = vi.spyOn(
service as any,
'debouncedContainerCacheUpdate'
);
debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
// Call the setupDockerWatch method
const result = await (service as any).setupDockerWatch();
// Verify that DockerEE was instantiated with the client
expect(DockerEE).toHaveBeenCalledWith(mockDockerInstance);
// Verify that the on method was called with the correct arguments
const dockerEEInstance = DockerEE();
expect(dockerEEInstance.on).toHaveBeenCalledWith('container', expect.any(Function));
// Verify that the start method was called
expect(dockerEEInstance.start).toHaveBeenCalled();
// Verify that debouncedContainerCacheUpdate was called
expect(debouncedContainerCacheUpdateSpy).toHaveBeenCalled();
// Verify that the result is the DockerEE instance
expect(result).toBe(dockerEEInstance);
});
it('should call debouncedContainerCacheUpdate when container event is received', async () => {
// Get the DockerEE import
const DockerEE = (await import('docker-event-emitter')).default;
// Mock the on method to capture the callback
const mockOnCallback = vi.fn();
const mockOn = vi.fn().mockImplementation((event, callback) => {
if (event === 'container') {
mockOnCallback(callback);
}
return { on: vi.fn() };
});
// Replace the DockerEE constructor's on method
(DockerEE as any).mockReturnValue({
on: mockOn,
start: vi.fn().mockResolvedValue(undefined),
});
// Mock the debouncedContainerCacheUpdate method
const debouncedContainerCacheUpdateSpy = vi.spyOn(
service as any,
'debouncedContainerCacheUpdate'
);
debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
// Call the setupDockerWatch method
await (service as any).setupDockerWatch();
// Get the callback function that was passed to the on method
const containerEventCallback = mockOnCallback.mock.calls[0][0];
// Call the callback with a container event
await containerEventCallback({
Type: 'container',
Action: 'start',
from: 'test-container',
});
// Verify that debouncedContainerCacheUpdate was called
expect(debouncedContainerCacheUpdateSpy).toHaveBeenCalled();
});
it('should not call debouncedContainerCacheUpdate for non-watched container events', async () => {
// Get the DockerEE import
const DockerEE = (await import('docker-event-emitter')).default;
// Mock the debouncedContainerCacheUpdate method
const debouncedContainerCacheUpdateSpy = vi.spyOn(
service as any,
'debouncedContainerCacheUpdate'
);
debouncedContainerCacheUpdateSpy.mockResolvedValue(undefined);
// Create a mock on function that captures the callback
let containerCallback: (data: {
Type: string;
Action: string;
from: string;
}) => void = () => {};
const mockOn = vi.fn().mockImplementation((event, callback) => {
if (event === 'container') {
containerCallback = callback;
}
return { on: vi.fn() };
});
// Replace the DockerEE constructor's on method
(DockerEE as any).mockReturnValue({
on: mockOn,
start: vi.fn().mockResolvedValue(undefined),
});
// Call the setupDockerWatch method
await (service as any).setupDockerWatch();
// Reset the spy after setup
debouncedContainerCacheUpdateSpy.mockReset();
// Call the callback with a non-watched container event
await containerCallback({
Type: 'container',
Action: 'create',
from: 'test-container',
});
// Verify that debouncedContainerCacheUpdate was not called
expect(debouncedContainerCacheUpdateSpy).not.toHaveBeenCalled();
});
it('should call getContainers and publish appUpdateEvent in debouncedContainerCacheUpdate', async () => {
// Mock the client's listContainers method
const mockListContainers = vi.fn().mockResolvedValue([]);
(service as any).client = {
listContainers: mockListContainers,
};
// Mock the getContainers method
const getContainersSpy = vi.spyOn(service, 'getContainers');
// Get the pubsub import
const { pubsub, PUBSUB_CHANNEL } = await import('@app/core/pubsub.js');
// Call the debouncedContainerCacheUpdate method directly and wait for the debounce
service['debouncedContainerCacheUpdate']();
// Force the debounced function to execute immediately
await new Promise((resolve) => setTimeout(resolve, 600));
// Verify that getContainers was called with useCache: false
expect(getContainersSpy).toHaveBeenCalledWith({ useCache: false });
// Verify that pubsub.publish was called with the correct arguments
expect(pubsub.publish).toHaveBeenCalledWith('info', {
info: {
apps: { installed: 0, running: 0 },
},
});
});
});
});
@@ -2,13 +2,10 @@ import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { readFile } from 'fs/promises';
import camelCaseKeys from 'camelcase-keys';
import { watch } from 'chokidar';
import DockerEE from 'docker-event-emitter';
import Docker from 'dockerode';
import { debounce } from 'lodash-es';
import type { ContainerPort, DockerContainer, DockerNetwork } from '@app/graphql/generated/api/types.js';
import { dockerLogger } from '@app/core/log.js';
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
import { sleep } from '@app/core/utils/misc/sleep.js';
@@ -28,38 +25,18 @@ export class DockerService implements OnModuleInit {
private client: Docker;
private containerCache: Array<DockerContainer> = [];
private autoStarts: string[] = [];
private dockerWatcher: null | typeof DockerEE = null;
private readonly logger = new Logger(DockerService.name);
constructor() {
this.client = this.getDockerClient();
}
private getDockerClient() {
public getDockerClient() {
return new Docker({
socketPath: '/var/run/docker.sock',
});
}
private setupVarRunWatch() {
const paths = getters.paths();
watch(paths['var-run'], { ignoreInitial: false })
.on('add', async (path) => {
if (path === paths['docker-socket']) {
dockerLogger.debug('Starting docker watch');
this.dockerWatcher = await this.setupDockerWatch();
}
})
.on('unlink', (path) => {
if (path === paths['docker-socket'] && this.dockerWatcher) {
dockerLogger.debug('Stopping docker watch');
this.dockerWatcher?.stop?.();
this.dockerWatcher = null;
this.containerCache = [];
}
});
}
get installed() {
return this.containerCache.length;
}
@@ -77,51 +54,24 @@ export class DockerService implements OnModuleInit {
};
}
private async setupDockerWatch(): Promise<DockerEE> {
// Only watch container events equal to start/stop
const watchedActions = ['die', 'kill', 'oom', 'pause', 'restart', 'start', 'stop', 'unpause'];
// Create docker event emitter instance
dockerLogger.debug('Creating docker event emitter instance');
const dee = new DockerEE(this.client);
// On Docker event update info with { apps: { installed, started } }
dee.on(
'container',
async (data: { Type: 'container'; Action: 'start' | 'stop'; from: string }) => {
// Only listen to container events
if (!watchedActions.includes(data.Action)) {
return;
}
dockerLogger.debug(`[${data.from}] ${data.Type}->${data.Action}`);
await this.debouncedContainerCacheUpdate();
}
);
// Get docker container count on first start
await this.debouncedContainerCacheUpdate();
await dee.start();
dockerLogger.debug('Binding to docker events');
return dee;
}
public async onModuleInit() {
this.setupVarRunWatch();
await this.debouncedContainerCacheUpdate();
}
/**
* Docker auto start file
*
* @note Doesn't exist if array is offline.
* @see https://github.com/limetech/webgui/issues/502#issue-480992547
*/
public async getAutoStarts(): Promise<string[]> {
/**
* Docker auto start file
*
* @note Doesn't exist if array is offline.
* @see https://github.com/limetech/webgui/issues/502#issue-480992547
*/
const autoStartFile = await readFile(getters.paths()['docker-autostart'], 'utf8')
.then((file) => file.toString())
.catch(() => '');
return autoStartFile.split('\n');
}
private debouncedContainerCacheUpdate = debounce(async () => {
public debouncedContainerCacheUpdate = debounce(async () => {
await this.getContainers({ useCache: false });
await pubsub.publish(PUBSUB_CHANNEL.INFO, this.appUpdateEvent);
}, 500);
@@ -13,9 +13,7 @@ import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resol
import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js';
import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js';
import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js';
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
import { DockerModule } from '@app/unraid-api/graph/resolvers/docker/docker.module.js';
import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js';
import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js';
import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
@@ -36,7 +34,7 @@ import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolv
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
@Module({
imports: [AuthModule],
imports: [AuthModule, DockerModule],
providers: [
ApiKeyResolver,
ArrayMutationsResolver,
@@ -49,9 +47,6 @@ import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js'
ConnectSettingsService,
DisksResolver,
DisplayResolver,
DockerMutationsResolver,
DockerResolver,
DockerService,
FlashResolver,
InfoResolver,
LogsResolver,
+125 -67
View File
@@ -137,12 +137,9 @@ importers:
diff:
specifier: ^7.0.0
version: 7.0.0
docker-event-emitter:
specifier: ^0.3.0
version: 0.3.0(dockerode@3.3.5)
dockerode:
specifier: ^3.3.5
version: 3.3.5
specifier: ^4.0.5
version: 4.0.5
dotenv:
specifier: ^16.4.5
version: 16.4.7
@@ -2298,6 +2295,15 @@ packages:
peerDependencies:
graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
'@grpc/grpc-js@1.13.2':
resolution: {integrity: sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==}
engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.7.13':
resolution: {integrity: sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==}
engines: {node: '>=6'}
hasBin: true
'@headlessui/vue@1.7.23':
resolution: {integrity: sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==}
engines: {node: '>=10'}
@@ -2400,6 +2406,9 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
'@jsdevtools/ono@7.1.3':
resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==}
@@ -5865,17 +5874,12 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
docker-event-emitter@0.3.0:
resolution: {integrity: sha512-QWpJsTOcLOiOctbCTH3T+w34Aw+zK6JzTh8xOqD/5/dDEhPhnCFmR8VzsCvTYAlTmkgxMUkRMTlBz1sGNZB5vg==}
peerDependencies:
dockerode: ^3.0.2
docker-modem@3.0.8:
resolution: {integrity: sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==}
docker-modem@5.0.6:
resolution: {integrity: sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ==}
engines: {node: '>= 8.0'}
dockerode@3.3.5:
resolution: {integrity: sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==}
dockerode@4.0.5:
resolution: {integrity: sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==}
engines: {node: '>= 8.0'}
doctrine@2.1.0:
@@ -7993,6 +7997,9 @@ packages:
lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.castarray@4.4.0:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
@@ -8066,6 +8073,9 @@ packages:
long@4.0.0:
resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==}
long@5.3.1:
resolution: {integrity: sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==}
longest@2.0.1:
resolution: {integrity: sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==}
engines: {node: '>=0.10.0'}
@@ -9448,6 +9458,10 @@ packages:
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
protobufjs@7.4.0:
resolution: {integrity: sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==}
engines: {node: '>=12.0.0'}
protocols@2.0.2:
resolution: {integrity: sha512-hHVTzba3wboROl0/aWRRG9dMytgH6ow//STBZh43l/wQgmMhYhOFi0EHWAPtoCz9IAUymsyP0TSBHkhgMEGNnQ==}
@@ -9516,6 +9530,7 @@ packages:
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
deprecated: |-
You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.
(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)
qs@6.13.0:
@@ -10485,8 +10500,8 @@ packages:
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
engines: {node: '>=6'}
tar-fs@2.0.1:
resolution: {integrity: sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==}
tar-fs@2.1.2:
resolution: {integrity: sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==}
tar-stream@2.2.0:
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
@@ -11046,6 +11061,10 @@ packages:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
uuid@10.0.0:
resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
hasBin: true
uuid@11.1.0:
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
hasBin: true
@@ -11988,7 +12007,7 @@ snapshots:
'@babel/traverse': 7.26.10
'@babel/types': 7.26.10
convert-source-map: 2.0.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -12359,7 +12378,7 @@ snapshots:
'@babel/parser': 7.27.0
'@babel/template': 7.26.9
'@babel/types': 7.27.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -12371,7 +12390,7 @@ snapshots:
'@babel/parser': 7.26.8
'@babel/template': 7.26.8
'@babel/types': 7.26.8
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -12689,7 +12708,7 @@ snapshots:
'@eslint/config-array@0.19.2':
dependencies:
'@eslint/object-schema': 2.1.6
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -12726,7 +12745,7 @@ snapshots:
'@eslint/eslintrc@3.3.1':
dependencies:
ajv: 6.12.6
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
espree: 10.3.0
globals: 14.0.0
ignore: 5.3.2
@@ -13325,12 +13344,12 @@ snapshots:
'@types/js-yaml': 4.0.9
'@whatwg-node/fetch': 0.10.3
chalk: 4.1.2
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
dotenv: 16.4.7
graphql: 16.10.0
graphql-request: 6.1.0(graphql@16.10.0)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6(supports-color@9.4.0)
https-proxy-agent: 7.0.6
jose: 5.10.0
js-yaml: 4.1.0
lodash: 4.17.21
@@ -13442,6 +13461,18 @@ snapshots:
dependencies:
graphql: 16.10.0
'@grpc/grpc-js@1.13.2':
dependencies:
'@grpc/proto-loader': 0.7.13
'@js-sdsl/ordered-map': 4.4.2
'@grpc/proto-loader@0.7.13':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.1
protobufjs: 7.4.0
yargs: 17.7.2
'@headlessui/vue@1.7.23(vue@3.5.13(typescript@5.8.2))':
dependencies:
'@tanstack/vue-virtual': 3.13.0(vue@3.5.13(typescript@5.8.2))
@@ -13540,6 +13571,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@js-sdsl/ordered-map@4.4.2': {}
'@jsdevtools/ono@7.1.3': {}
'@jsonforms/core@3.5.1':
@@ -13626,7 +13659,7 @@ snapshots:
dependencies:
consola: 3.4.2
detect-libc: 2.0.3
https-proxy-agent: 7.0.6(supports-color@9.4.0)
https-proxy-agent: 7.0.6
node-fetch: 2.7.0
nopt: 8.1.0
semver: 7.7.1
@@ -14408,7 +14441,7 @@ snapshots:
'@pm2/pm2-version-check@1.0.4':
dependencies:
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -15058,7 +15091,7 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 22.13.13
'@types/node': 22.14.0
'@types/bytes@3.1.5': {}
@@ -15068,11 +15101,11 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
'@types/node': 22.13.13
'@types/node': 22.14.0
'@types/conventional-commits-parser@5.0.1':
dependencies:
'@types/node': 22.13.13
'@types/node': 22.14.0
optional: true
'@types/cors@2.8.17':
@@ -15219,14 +15252,14 @@ snapshots:
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 22.13.13
'@types/node': 22.14.0
'@types/sendmail@1.4.7': {}
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 22.13.13
'@types/node': 22.14.0
'@types/send': 0.17.4
'@types/ssh2@1.15.4':
@@ -15309,7 +15342,7 @@ snapshots:
'@typescript-eslint/types': 8.28.0
'@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
'@typescript-eslint/visitor-keys': 8.28.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.23.0(jiti@2.4.2)
typescript: 5.8.2
transitivePeerDependencies:
@@ -15329,7 +15362,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 8.28.0(typescript@5.8.2)
'@typescript-eslint/utils': 8.28.0(eslint@9.23.0(jiti@2.4.2))(typescript@5.8.2)
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
eslint: 9.23.0(jiti@2.4.2)
ts-api-utils: 2.1.0(typescript@5.8.2)
typescript: 5.8.2
@@ -15357,7 +15390,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 8.28.0
'@typescript-eslint/visitor-keys': 8.28.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
@@ -15529,7 +15562,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.6
@@ -17626,27 +17659,24 @@ snapshots:
dlv@1.1.3: {}
docker-event-emitter@0.3.0(dockerode@3.3.5):
docker-modem@5.0.6:
dependencies:
debug: 4.4.0(supports-color@9.4.0)
dockerode: 3.3.5
transitivePeerDependencies:
- supports-color
docker-modem@3.0.8:
dependencies:
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
readable-stream: 3.6.2
split-ca: 1.0.1
ssh2: 1.16.0
transitivePeerDependencies:
- supports-color
dockerode@3.3.5:
dockerode@4.0.5:
dependencies:
'@balena/dockerignore': 1.0.2
docker-modem: 3.0.8
tar-fs: 2.0.1
'@grpc/grpc-js': 1.13.2
'@grpc/proto-loader': 0.7.13
docker-modem: 5.0.6
protobufjs: 7.4.0
tar-fs: 2.1.2
uuid: 10.0.0
transitivePeerDependencies:
- supports-color
@@ -18283,7 +18313,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.6
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
escape-string-regexp: 4.0.0
eslint-scope: 8.3.0
eslint-visitor-keys: 4.2.0
@@ -18827,7 +18857,7 @@ snapshots:
dependencies:
basic-ftp: 5.0.5
data-uri-to-buffer: 6.0.2
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -19294,7 +19324,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -19338,6 +19368,13 @@ snapshots:
quick-lru: 5.1.1
resolve-alpn: 1.2.1
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6(supports-color@9.4.0):
dependencies:
agent-base: 7.1.3
@@ -19769,7 +19806,7 @@ snapshots:
istanbul-lib-source-maps@5.0.6:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -19856,7 +19893,7 @@ snapshots:
form-data: 4.0.2
html-encoding-sniffer: 4.0.0
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6(supports-color@9.4.0)
https-proxy-agent: 7.0.6
is-potential-custom-element-name: 1.0.1
nwsapi: 2.2.16
parse5: 7.2.1
@@ -20126,6 +20163,8 @@ snapshots:
lodash-es@4.17.21: {}
lodash.camelcase@4.3.0: {}
lodash.castarray@4.4.0: {}
lodash.defaults@4.2.0: {}
@@ -20182,6 +20221,8 @@ snapshots:
long@4.0.0: {}
long@5.3.1: {}
longest@2.0.1: {}
loose-envify@1.4.0:
@@ -21106,10 +21147,10 @@ snapshots:
dependencies:
'@tootallnate/quickjs-emscripten': 0.23.0
agent-base: 7.1.3
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
get-uri: 6.0.4
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6(supports-color@9.4.0)
https-proxy-agent: 7.0.6
pac-resolver: 7.0.1
socks-proxy-agent: 8.0.5
transitivePeerDependencies:
@@ -21373,7 +21414,7 @@ snapshots:
pm2-axon-rpc@0.7.1:
dependencies:
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@@ -21381,7 +21422,7 @@ snapshots:
dependencies:
amp: 0.3.1
amp-message: 0.1.2
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
escape-string-regexp: 4.0.0
transitivePeerDependencies:
- supports-color
@@ -21398,7 +21439,7 @@ snapshots:
pm2-sysmonit@1.2.8:
dependencies:
async: 3.2.6
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
pidusage: 2.0.21
systeminformation: 5.25.11
tx2: 1.0.5
@@ -21420,7 +21461,7 @@ snapshots:
commander: 2.15.1
croner: 4.1.97
dayjs: 1.11.13
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
enquirer: 2.3.6
eventemitter2: 5.0.1
fclone: 1.0.11
@@ -21749,6 +21790,21 @@ snapshots:
proto-list@1.2.4: {}
protobufjs@7.4.0:
dependencies:
'@protobufjs/aspromise': 1.1.2
'@protobufjs/base64': 1.1.2
'@protobufjs/codegen': 2.0.4
'@protobufjs/eventemitter': 1.1.0
'@protobufjs/fetch': 1.1.0
'@protobufjs/float': 1.0.2
'@protobufjs/inquire': 1.1.0
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 22.14.0
long: 5.3.1
protocols@2.0.2: {}
proxy-addr@2.0.7:
@@ -21759,9 +21815,9 @@ snapshots:
proxy-agent@6.4.0:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6(supports-color@9.4.0)
https-proxy-agent: 7.0.6
lru-cache: 7.18.3
pac-proxy-agent: 7.1.0
proxy-from-env: 1.1.0
@@ -22148,7 +22204,7 @@ snapshots:
require-in-the-middle@5.2.0:
dependencies:
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
module-details-from-path: 1.0.3
resolve: 1.22.10
transitivePeerDependencies:
@@ -22615,7 +22671,7 @@ snapshots:
socks-proxy-agent@8.0.5:
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
socks: 2.8.4
transitivePeerDependencies:
- supports-color
@@ -22871,7 +22927,7 @@ snapshots:
stylus@0.57.0:
dependencies:
css: 3.0.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
glob: 7.2.3
safer-buffer: 2.1.2
sax: 1.2.4
@@ -23022,7 +23078,7 @@ snapshots:
tapable@2.2.1: {}
tar-fs@2.0.1:
tar-fs@2.1.2:
dependencies:
chownr: 1.1.4
mkdirp-classic: 0.5.3
@@ -23560,6 +23616,8 @@ snapshots:
utils-merge@1.0.1: {}
uuid@10.0.0: {}
uuid@11.1.0: {}
uuid@3.4.0: {}
@@ -23602,7 +23660,7 @@ snapshots:
vite-node@3.0.9(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0):
dependencies:
cac: 6.7.14
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
es-module-lexer: 1.6.0
pathe: 2.0.3
vite: 6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
@@ -23735,7 +23793,7 @@ snapshots:
dependencies:
'@rollup/pluginutils': 4.2.1
chalk: 4.1.2
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
vite: 6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)
optionalDependencies:
'@swc/core': 1.11.13(@swc/helpers@0.5.15)
@@ -23798,7 +23856,7 @@ snapshots:
vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.3(@types/node@22.13.13)(jiti@2.4.2)(stylus@0.57.0)(terser@5.39.0)(tsx@4.19.3)(yaml@2.7.0)):
dependencies:
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
globrex: 0.1.2
tsconfck: 3.1.5(typescript@5.8.2)
optionalDependencies:
@@ -23873,7 +23931,7 @@ snapshots:
'@vitest/spy': 3.0.9
'@vitest/utils': 3.0.9
chai: 5.2.0
debug: 4.4.0(supports-color@9.4.0)
debug: 4.4.0(supports-color@5.5.0)
expect-type: 1.2.0
magic-string: 0.30.17
pathe: 2.0.3