refactor: stricter domain: attachments

This commit is contained in:
2026-04-11 13:39:27 +02:00
parent 0b9a9f311e
commit 58e338246f
15 changed files with 60 additions and 57 deletions

View File

@@ -7,25 +7,28 @@ Handles file sharing between peers over WebRTC data channels. Files are announce
``` ```
attachment/ attachment/
├── application/ ├── application/
│ ├── attachment.facade.ts Thin entry point, delegates to manager │ ├── attachment.facade.ts Thin entry point, delegates to manager
── attachment-manager.service.ts Orchestrates lifecycle, auto-download, peer listeners ── services/
│ ├── attachment-transfer.service.ts P2P file transfer protocol (announce/request/chunk/cancel) ├── attachment-manager.service.ts Orchestrates lifecycle, auto-download, peer listeners
│ ├── attachment-transfer-transport.service.ts Base64 encode/decode, chunked streaming ├── attachment-transfer.service.ts P2P file transfer protocol (announce/request/chunk/cancel)
│ ├── attachment-persistence.service.ts DB + filesystem persistence, migration from localStorage ├── attachment-transfer-transport.service.ts Base64 encode/decode, chunked streaming
── attachment-runtime.store.ts In-memory signal-based state (Maps for attachments, chunks, pending) ── attachment-persistence.service.ts DB + filesystem persistence, migration from localStorage
│ └── attachment-runtime.store.ts In-memory signal-based state (Maps for attachments, chunks, pending)
├── domain/ ├── domain/
│ ├── attachment.models.ts Attachment type extending AttachmentMeta with runtime state │ ├── attachment.logic.ts isAttachmentMedia, shouldAutoRequestWhenWatched, shouldPersistDownloadedAttachment
│ ├── attachment.logic.ts isAttachmentMedia, shouldAutoRequestWhenWatched, shouldPersistDownloadedAttachment │ ├── models/
│ ├── attachment.constants.ts MAX_AUTO_SAVE_SIZE_BYTES = 10 MB │ ├── attachment.models.ts Attachment type extending AttachmentMeta with runtime state
── attachment-transfer.models.ts Protocol event types (file-announce, file-chunk, file-request, ...) │ └── attachment-transfer.models.ts Protocol event types (file-announce, file-chunk, file-request, ...)
│ └── attachment-transfer.constants.ts FILE_CHUNK_SIZE_BYTES = 64 KB, EWMA weights, error messages │ └── constants/
│ ├── attachment.constants.ts MAX_AUTO_SAVE_SIZE_BYTES = 10 MB
│ └── attachment-transfer.constants.ts FILE_CHUNK_SIZE_BYTES = 64 KB, EWMA weights, error messages
├── infrastructure/ ├── infrastructure/
│ ├── attachment-storage.service.ts Electron filesystem access (save / read / delete) │ ├── attachment-storage.service.ts Electron filesystem access (save / read / delete)
│ └── attachment-storage.helpers.ts sanitizeAttachmentRoomName, resolveAttachmentStorageBucket │ └── attachment-storage.util.ts sanitizeAttachmentRoomName, resolveAttachmentStorageBucket
└── index.ts Barrel exports └── index.ts Barrel exports
``` ```
## Service composition ## Service composition
@@ -52,16 +55,16 @@ graph TD
Transfer --> Store Transfer --> Store
Persistence --> Storage Persistence --> Storage
Persistence --> Store Persistence --> Store
Storage --> Helpers[attachment-storage.helpers] Storage --> Helpers[attachment-storage.util]
click Facade "application/attachment.facade.ts" "Thin entry point" _blank click Facade "application/attachment.facade.ts" "Thin entry point" _blank
click Manager "application/attachment-manager.service.ts" "Orchestrates lifecycle" _blank click Manager "application/services/attachment-manager.service.ts" "Orchestrates lifecycle" _blank
click Transfer "application/attachment-transfer.service.ts" "P2P file transfer protocol" _blank click Transfer "application/services/attachment-transfer.service.ts" "P2P file transfer protocol" _blank
click Transport "application/attachment-transfer-transport.service.ts" "Base64 encode/decode, chunked streaming" _blank click Transport "application/services/attachment-transfer-transport.service.ts" "Base64 encode/decode, chunked streaming" _blank
click Persistence "application/attachment-persistence.service.ts" "DB + filesystem persistence" _blank click Persistence "application/services/attachment-persistence.service.ts" "DB + filesystem persistence" _blank
click Store "application/attachment-runtime.store.ts" "In-memory signal-based state" _blank click Store "application/services/attachment-runtime.store.ts" "In-memory signal-based state" _blank
click Storage "infrastructure/attachment-storage.service.ts" "Electron filesystem access" _blank click Storage "infrastructure/attachment-storage.service.ts" "Electron filesystem access" _blank
click Helpers "infrastructure/attachment-storage.helpers.ts" "Path helpers" _blank click Helpers "infrastructure/attachment-storage.util.ts" "Path helpers" _blank
click Logic "domain/attachment.logic.ts" "Pure decision functions" _blank click Logic "domain/attachment.logic.ts" "Pure decision functions" _blank
``` ```

View File

@@ -1,5 +1,5 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { AttachmentManagerService } from './attachment-manager.service'; import { AttachmentManagerService } from './services/attachment-manager.service';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttachmentFacade { export class AttachmentFacade {

View File

@@ -4,18 +4,18 @@ import {
inject inject
} from '@angular/core'; } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router'; import { NavigationEnd, Router } from '@angular/router';
import { RealtimeSessionFacade } from '../../../core/realtime'; import { RealtimeSessionFacade } from '../../../../core/realtime';
import { DatabaseService } from '../../../infrastructure/persistence'; import { DatabaseService } from '../../../../infrastructure/persistence';
import { ROOM_URL_PATTERN } from '../../../core/constants'; import { ROOM_URL_PATTERN } from '../../../../core/constants';
import { shouldAutoRequestWhenWatched } from '../domain/attachment.logic'; import { shouldAutoRequestWhenWatched } from '../../domain/attachment.logic';
import type { Attachment, AttachmentMeta } from '../domain/attachment.models'; import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.models';
import type { import type {
FileAnnouncePayload, FileAnnouncePayload,
FileCancelPayload, FileCancelPayload,
FileChunkPayload, FileChunkPayload,
FileNotFoundPayload, FileNotFoundPayload,
FileRequestPayload FileRequestPayload
} from '../domain/attachment-transfer.models'; } from '../../domain/models/attachment-transfer.models';
import { AttachmentPersistenceService } from './attachment-persistence.service'; import { AttachmentPersistenceService } from './attachment-persistence.service';
import { AttachmentRuntimeStore } from './attachment-runtime.store'; import { AttachmentRuntimeStore } from './attachment-runtime.store';
import { AttachmentTransferService } from './attachment-transfer.service'; import { AttachmentTransferService } from './attachment-transfer.service';

View File

@@ -1,12 +1,12 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { take } from 'rxjs'; import { take } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { selectCurrentRoomName } from '../../../store/rooms/rooms.selectors'; import { selectCurrentRoomName } from '../../../../store/rooms/rooms.selectors';
import { DatabaseService } from '../../../infrastructure/persistence'; import { DatabaseService } from '../../../../infrastructure/persistence';
import { AttachmentStorageService } from '../infrastructure/attachment-storage.service'; import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
import type { Attachment, AttachmentMeta } from '../domain/attachment.models'; import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.models';
import { MAX_AUTO_SAVE_SIZE_BYTES } from '../domain/attachment.constants'; import { MAX_AUTO_SAVE_SIZE_BYTES } from '../../domain/constants/attachment.constants';
import { LEGACY_ATTACHMENTS_STORAGE_KEY } from '../domain/attachment-transfer.constants'; import { LEGACY_ATTACHMENTS_STORAGE_KEY } from '../../domain/constants/attachment-transfer.constants';
import { AttachmentRuntimeStore } from './attachment-runtime.store'; import { AttachmentRuntimeStore } from './attachment-runtime.store';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })

View File

@@ -1,5 +1,5 @@
import { Injectable, signal } from '@angular/core'; import { Injectable, signal } from '@angular/core';
import type { Attachment } from '../domain/attachment.models'; import type { Attachment } from '../../domain/models/attachment.models';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttachmentRuntimeStore { export class AttachmentRuntimeStore {

View File

@@ -1,8 +1,8 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { RealtimeSessionFacade } from '../../../core/realtime'; import { RealtimeSessionFacade } from '../../../../core/realtime';
import { AttachmentStorageService } from '../infrastructure/attachment-storage.service'; import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
import { FILE_CHUNK_SIZE_BYTES } from '../domain/attachment-transfer.constants'; import { FILE_CHUNK_SIZE_BYTES } from '../../domain/constants/attachment-transfer.constants';
import { FileChunkEvent } from '../domain/attachment-transfer.models'; import { FileChunkEvent } from '../../domain/models/attachment-transfer.models';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttachmentTransferTransportService { export class AttachmentTransferTransportService {

View File

@@ -1,17 +1,17 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { recordDebugNetworkFileChunk } from '../../../infrastructure/realtime/logging/debug-network-metrics'; import { recordDebugNetworkFileChunk } from '../../../../infrastructure/realtime/logging/debug-network-metrics';
import { RealtimeSessionFacade } from '../../../core/realtime'; import { RealtimeSessionFacade } from '../../../../core/realtime';
import { AttachmentStorageService } from '../infrastructure/attachment-storage.service'; import { AttachmentStorageService } from '../../infrastructure/services/attachment-storage.service';
import { MAX_AUTO_SAVE_SIZE_BYTES } from '../domain/attachment.constants'; import { MAX_AUTO_SAVE_SIZE_BYTES } from '../../domain/constants/attachment.constants';
import { shouldPersistDownloadedAttachment } from '../domain/attachment.logic'; import { shouldPersistDownloadedAttachment } from '../../domain/attachment.logic';
import type { Attachment, AttachmentMeta } from '../domain/attachment.models'; import type { Attachment, AttachmentMeta } from '../../domain/models/attachment.models';
import { import {
ATTACHMENT_TRANSFER_EWMA_CURRENT_WEIGHT, ATTACHMENT_TRANSFER_EWMA_CURRENT_WEIGHT,
ATTACHMENT_TRANSFER_EWMA_PREVIOUS_WEIGHT, ATTACHMENT_TRANSFER_EWMA_PREVIOUS_WEIGHT,
DEFAULT_ATTACHMENT_MIME_TYPE, DEFAULT_ATTACHMENT_MIME_TYPE,
FILE_NOT_FOUND_REQUEST_ERROR, FILE_NOT_FOUND_REQUEST_ERROR,
NO_CONNECTED_PEERS_REQUEST_ERROR NO_CONNECTED_PEERS_REQUEST_ERROR
} from '../domain/attachment-transfer.constants'; } from '../../domain/constants/attachment-transfer.constants';
import { import {
type FileAnnounceEvent, type FileAnnounceEvent,
type FileAnnouncePayload, type FileAnnouncePayload,
@@ -23,7 +23,7 @@ import {
type FileRequestEvent, type FileRequestEvent,
type FileRequestPayload, type FileRequestPayload,
type LocalFileWithPath type LocalFileWithPath
} from '../domain/attachment-transfer.models'; } from '../../domain/models/attachment-transfer.models';
import { AttachmentPersistenceService } from './attachment-persistence.service'; import { AttachmentPersistenceService } from './attachment-persistence.service';
import { AttachmentRuntimeStore } from './attachment-runtime.store'; import { AttachmentRuntimeStore } from './attachment-runtime.store';
import { AttachmentTransferTransportService } from './attachment-transfer-transport.service'; import { AttachmentTransferTransportService } from './attachment-transfer-transport.service';

View File

@@ -1,5 +1,5 @@
import { MAX_AUTO_SAVE_SIZE_BYTES } from './attachment.constants'; import { MAX_AUTO_SAVE_SIZE_BYTES } from './constants/attachment.constants';
import type { Attachment } from './attachment.models'; import type { Attachment } from './models/attachment.models';
export function isAttachmentMedia(attachment: Pick<Attachment, 'mime'>): boolean { export function isAttachmentMedia(attachment: Pick<Attachment, 'mime'>): boolean {
return attachment.mime.startsWith('image/') || return attachment.mime.startsWith('image/') ||

View File

@@ -1,5 +1,5 @@
import type { ChatEvent } from '../../../shared-kernel'; import type { ChatEvent } from '../../../../shared-kernel';
import type { ChatAttachmentAnnouncement } from '../../../shared-kernel'; import type { ChatAttachmentAnnouncement } from '../../../../shared-kernel';
export type FileAnnounceEvent = ChatEvent & { export type FileAnnounceEvent = ChatEvent & {
type: 'file-announce'; type: 'file-announce';

View File

@@ -1,4 +1,4 @@
import type { ChatAttachmentMeta } from '../../../shared-kernel'; import type { ChatAttachmentMeta } from '../../../../shared-kernel';
export type AttachmentMeta = ChatAttachmentMeta; export type AttachmentMeta = ChatAttachmentMeta;

View File

@@ -1,3 +1,3 @@
export * from './application/attachment.facade'; export * from './application/attachment.facade';
export * from './domain/attachment.constants'; export * from './domain/constants/attachment.constants';
export * from './domain/attachment.models'; export * from './domain/models/attachment.models';

View File

@@ -1,11 +1,11 @@
import { Injectable, inject } from '@angular/core'; import { Injectable, inject } from '@angular/core';
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service'; import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
import type { Attachment } from '../domain/attachment.models'; import type { Attachment } from '../../domain/models/attachment.models';
import { import {
resolveAttachmentStorageBucket, resolveAttachmentStorageBucket,
resolveAttachmentStoredFilename, resolveAttachmentStoredFilename,
sanitizeAttachmentRoomName sanitizeAttachmentRoomName
} from './attachment-storage.helpers'; } from '../util/attachment-storage.util';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttachmentStorageService { export class AttachmentStorageService {