Files
gopeed/ui/flutter/docs/window-capability-rpc.md
T

8.5 KiB

Window Capability RPC Architecture

This document defines the internal communication contract between the main window and desktop child windows.

1. Ownership

  • The main window is the only owner of the Gopeed runtime, Gopeed API connection, and Hive database.
  • Child windows must never initialize libgopeed, initialize the Gopeed HTTP client, or open a Hive box.
  • Child windows access application capabilities through AppCapabilities only.
  • Immutable window launch input, such as the window type and an initial create-task request, may use AppWindowPayload.
  • API addresses, API tokens, appearance state, locale, and persisted data must not be placed in launch payloads.

2. Layers

Presentation / Riverpod controllers
              |
        AppCapabilities
          /         \
 GopeedService   AppStorageService
          \         /
       CapabilityInvoker
          /         \
 LocalCapability   WindowCapability
     Invoker           Invoker
          |               |
 CapabilityRegistry  WindowMethodChannel
          |               |
 Gopeed HTTP/FFI + Hive in the main window

The presentation layer must not branch on whether it is running in a main or child window. The root ProviderScope selects the local or remote capability implementation.

3. RPC Contract

Every operation is declared once as a typed RpcMethod<P, R>:

static const resolve = RpcMethod<ResolveTask, ResolveResult>('gopeed.resolve');

Rules:

  • Operation names are stable protocol identifiers and must be declared in a capability method catalog.
  • Do not duplicate operation strings in pages, controllers, host handlers, or clients.
  • Do not create one MethodChannel per operation.
  • Do not expose Gopeed HTTP paths through the internal capability API.
  • Multi-argument operations should use a JSON map or a dedicated request DTO.
  • Prefer a dedicated request DTO when parameters have domain meaning or are likely to evolve.

RpcCodecRegistry owns serialization. Encoding is generic for JSON primitives, collections, enums, and models with toJson(). Each non-primitive response or request model registers its fromJson decoder once. Individual operations must not repeat request and response codec closures.

The local invoker calls the typed handler directly and does not perform a JSON round trip. Serialization occurs only at a window boundary.

4. Transport

Child-to-main requests use one unidirectional WindowMethodChannel:

gopeed.app.capabilities.v1

The transport uses one method, capability.call, with an operation name and JSON payload. Results use a common success/error envelope. MethodChannel provides request-response correlation, so the protocol does not add request IDs for ordinary calls.

The existing HTTP HostRpcService is an external browser-extension integration and is not the internal window capability transport. Its /forward endpoint must not be reused by child windows.

5. Events And State Synchronization

Main-to-child events use each child's WindowController channel. A child registers its method handler before subscribing.

Initialization order:

  1. The child registers its window event handler.
  2. The child requests the current appearance snapshot.
  3. The child applies the snapshot.
  4. The child subscribes with its window ID.
  5. The main window stores the controller and immediately sends the current snapshot again.
  6. Later state changes are broadcast to every subscribed child.

Events contain complete state snapshots, not patches. No revision field is required. Event names are declared centrally in AppWindowRpcProtocol.

Appearance synchronization uses appearance.changed and currently contains:

  • Theme mode
  • Theme color
  • Locale

State broadcasting must observe the owning Riverpod state centrally. Do not broadcast directly from a settings button callback, because updates can originate from configuration loading, system changes, or future entry points.

6. Storage

  • Database remains the low-level Hive implementation and is main-window infrastructure.
  • Product code used by child windows depends on AppStorageService.
  • Storage RPC methods expose business capabilities such as create-history operations, not raw box.get or box.put access.
  • Prefer batch mutations across the window boundary. For example, save all parsed URLs in one saveCreateHistory(List<String>) call.
  • New child-window storage requirements must be added to StorageMethods and bound in the main registry.
  • Config reads, full config saves, and create-history mutations share one main-window queue. Each history mutation reads, changes, and writes the config within that queue. History reads wait for earlier saves.
  • StorageMethods owns extra.createHistory; full config saves preserve its latest value inside the queue so stale settings or child-window snapshots cannot overwrite history.

7. Gopeed Backend Migration

All structured task, configuration, extension, and webhook operations are exposed through GopeedService. The local registry currently binds them to lib/api/api.dart.

When the backend moves to FFI:

  • Replace the main-window local bindings or their underlying implementation.
  • Keep GopeedMethods, GopeedService, child-window transport, pages, and controllers unchanged.
  • Keep raw HTTP proxy functionality separate unless an explicit capability contract is introduced for it.

8. Adding A Capability

  1. Add one typed RpcMethod to the appropriate method catalog.
  2. Register a model decoder once if a new non-primitive type crosses the boundary.
  3. Add the strongly typed facade method to the capability service.
  4. Bind the method to the main-window implementation in LocalAppCapabilities.
  5. Use the facade from a Riverpod controller or page.
  6. Add tests for local typed dispatch and serialized dispatch.
  7. For events, add one centralized event name and broadcast a complete snapshot.

9. Prohibited Patterns

Child-window code must not:

  • Import lib/api/api.dart.
  • Call api.init or access the Gopeed socket/address/token.
  • Import or access Database.instance.
  • Open Hive boxes.
  • Receive API configuration or persisted data through AppWindowPayload.
  • Add feature-specific MethodChannels.
  • Parse RPC JSON directly inside a page or widget.

These constraints keep all backend and persistence ownership in the main window and allow multiple child windows to operate concurrently without database locks or duplicated Gopeed runtimes.

10. Batch Task Creation

GopeedMethods.createTaskBatch carries CreateTaskBatch through the existing codec registry. Each member accepts either rid from resolve or req, plus creation opts. A nonempty rid takes priority. BT selections retain their original file indexes in opts.selectFiles.

Both ordinary and grouped creation return CreateTaskBatchResult, an object with taskIds and an optional parentId. A successful response completes the batch, including partial success. The preview tracks completed batches so a failed batch can be retried without repeating successful batches. Creation does not accept a group ID to append members to an existing group.

11. Opening Child Surfaces

Use AppRouter.openWindow(AppWindowPayload) for child-window destinations. AppRouter.openCreateTask(createTask: ...) is the create-task convenience API. All entry points, including deep links, browser capture and the tray, use this API rather than selecting a presentation themselves:

  • Desktop launches an independent native child window through AppWindowLauncher.open(payload) and the existing capability transport.
  • Web pushes an AppWindowPage on the root Navigator. This routed shadcn modal keeps the current section, filters and scroll state mounted underneath it.
  • Native mobile retains the full-page flow, including later share requests.
  • If native child-window creation fails, the main window is brought forward and the same in-app modal is used as a fallback.

AppWindowContent owns the destination paths and content builders shared with ChildWindowApp. For another child surface, add its payload type, register its path and content, and use AppWindowPage for its in-app route. The modal's preferred size can be supplied per destination. Keep backend and storage access on the existing capability API in either presentation.

Create-task content reports cancellation or completion through onClose when hosted in-app. Cancellation pops only the child surface. Completion selects the downloading list and returns home. The route payload uses a serializable string so browser history can restore a modal and its initial request.