mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-15 05:42:31 +00:00
Document replicator capability and lifecycle contracts
This commit is contained in:
@@ -0,0 +1,516 @@
|
|||||||
|
---
|
||||||
|
date: 2026-08-27
|
||||||
|
commonlib-version: "0.1.19"
|
||||||
|
self-hosted-livesync-version: "1.0.21"
|
||||||
|
status: proposed
|
||||||
|
series: replicator-capabilities-and-lifecycle
|
||||||
|
part: 1 of 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architectural Decision Record: Replicator Capabilities and Lifecycle Orchestration — Part 1: Core Contract
|
||||||
|
|
||||||
|
Series navigation: this is Part 1 of 3. Continue with [Part 2: P2P service and
|
||||||
|
session lifecycle](2026_08_replicator_capabilities_02_p2p_service_lifecycle.md),
|
||||||
|
then [Part 3: migration plan and verification](2026_08_replicator_capabilities_03_migration_plan.md).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed. This record defines the provider, capability, lifecycle, interaction,
|
||||||
|
ownership, and probe boundaries required by current Self-hosted LiveSync
|
||||||
|
consumers. It is the generic part of the series; the P2P-specific ownership
|
||||||
|
rules live in Part 2, and implementation sequencing lives in Part 3.
|
||||||
|
|
||||||
|
The accepted P2P room and transport lifecycle record remains authoritative for
|
||||||
|
the current P2P implementation until Stage 3 in Part 3 is complete. The
|
||||||
|
supersession boundary for that record is stated in Part 2 and is not repeated
|
||||||
|
here.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Self-hosted LiveSync currently represents CouchDB, Object Storage, and P2P with
|
||||||
|
one `LiveSyncAbstractReplicator` base class. That base class requires finite
|
||||||
|
and continuous replication, full upload and download, remote creation and
|
||||||
|
reset, lock administration, preferred-tweak Metadata, on-demand Chunk reads,
|
||||||
|
remote status, integrity inspection, and connected-device inspection.
|
||||||
|
|
||||||
|
These operations do not share one support boundary:
|
||||||
|
|
||||||
|
- CouchDB supports unattended OneShot Sync, Continuous replication, central
|
||||||
|
remote administration, and CouchDB-specific inspection and maintenance.
|
||||||
|
- Object Storage supports finite journal synchronisation, central reset and
|
||||||
|
lock Metadata, full upload and download, and storage-size inspection. It has
|
||||||
|
no continuous changes feed, CouchDB Chunk source, CouchDB integrity
|
||||||
|
inspection, or CouchDB device registry.
|
||||||
|
- P2P supports peer-targeted finite transfer and an independently owned room,
|
||||||
|
signalling, watch, and broadcast lifecycle. It has no central database to
|
||||||
|
create, reset, lock, inspect for size, or upload during first-device setup.
|
||||||
|
It also has peer-driven AutoSync and AutoWatch paths when the room is open.
|
||||||
|
Those paths are detailed in Part 2.
|
||||||
|
|
||||||
|
The abstract class makes absent capabilities look like operations. Current
|
||||||
|
implementations express absence through thrown errors, `false`, empty arrays,
|
||||||
|
zero counts, silent success, and a dummy all-zero Security Seed. Callers
|
||||||
|
cannot tell whether an operation was performed, was inapplicable, or could not
|
||||||
|
be performed.
|
||||||
|
|
||||||
|
A neutral value is correct only when it is the documented identity for every
|
||||||
|
caller. For example, Object Storage has no remote-Chunk role, whereas a
|
||||||
|
supported CouchDB Chunk request may legitimately return an empty array. A
|
||||||
|
zero integrity count cannot safely stand for an inspection which did not run.
|
||||||
|
|
||||||
|
Issue 1140 exposes the lifecycle consequence. In version 1.0.21,
|
||||||
|
`ModuleReplicatorCouchDB` owns the application resume callback and excludes
|
||||||
|
Object Storage and P2P by `remoteType`. Object Storage accepts `syncOnStart`,
|
||||||
|
but no journal synchronisation starts at resume. Periodic, event-driven, and
|
||||||
|
manual calls later reach the active Replicator successfully.
|
||||||
|
|
||||||
|
The construction boundary is also overloaded. `getNewReplicator()` is an
|
||||||
|
order-dependent first-result handler which ignores false results and catches
|
||||||
|
handler errors. It is used for active Replicator acquisition, trial settings,
|
||||||
|
and temporary command instances. P2P construction can replace and close a
|
||||||
|
service-owned current transport, so an apparently temporary request can
|
||||||
|
disturb an active or adjunct transport.
|
||||||
|
|
||||||
|
Fast Setup is a separate boundary. Streaming Fetch is a CouchDB-specific
|
||||||
|
initial transfer which uses CouchDB HTTP settings and a Security Seed supplier;
|
||||||
|
it does not need a full Replicator or an owned PouchDB connection. It must not
|
||||||
|
be made a generic Replicator capability merely because the current code obtains
|
||||||
|
one as a supplier.
|
||||||
|
|
||||||
|
Finally, current operation results can hide failure. Object Storage can discard
|
||||||
|
a failed or stopped journal result and report success. A headless P2P path can
|
||||||
|
complete but return `undefined`, which the caller interprets as failure. Remote
|
||||||
|
mutations can be caught or ignored before Rebuilder continuation, and an
|
||||||
|
offline integrity inspection can be represented as zero. The contract must
|
||||||
|
make those outcomes truthful.
|
||||||
|
|
||||||
|
## Decision drivers
|
||||||
|
|
||||||
|
The design must:
|
||||||
|
|
||||||
|
1. cover every operation used by the plug-in, CLI, WebApp, WebPeer, Setup,
|
||||||
|
Rebuilder, and maintenance flows;
|
||||||
|
2. keep lifecycle policy independent of provider class names;
|
||||||
|
3. make an omitted support decision a compile-time error when a current
|
||||||
|
provider or capability is added;
|
||||||
|
4. carry trigger and interaction policy through `ReplicationService`, so an
|
||||||
|
automatic trigger cannot open a dialogue;
|
||||||
|
5. distinguish capability absence, unavailable observation, and an observed
|
||||||
|
empty value where that difference affects safety;
|
||||||
|
6. retain simple neutral results where they are safe identities for every
|
||||||
|
caller;
|
||||||
|
7. give active Replicator, flow-specific probe, and transport resources one
|
||||||
|
explicit owner and disposal boundary; and
|
||||||
|
8. report replication, central-remote mutation, and maintenance outcomes
|
||||||
|
truthfully while preserving source compatibility during migration.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Use precise ownership terms
|
||||||
|
|
||||||
|
- A **provider definition** is the host-composed, exhaustive declaration for
|
||||||
|
one current remote kind.
|
||||||
|
- The **active Replicator** is the selected main-remote handle owned by
|
||||||
|
`ReplicatorService`.
|
||||||
|
- A **probe** is a short-lived, flow-specific validation resource whose caller
|
||||||
|
owns disposal.
|
||||||
|
- The **P2P service** is the stable Commonlib implementation which supplies
|
||||||
|
narrow P2P contract views. Its room-session ownership is defined in Part 2.
|
||||||
|
- A **room session** and **session epoch** are P2P terms defined in Part 2; an
|
||||||
|
epoch is an internal fence, not a public capability or logical room name.
|
||||||
|
|
||||||
|
An active Replicator is a handle, not necessarily the owner of every transport
|
||||||
|
which it uses. In particular, the active P2P Replicator is a non-owning adapter
|
||||||
|
over the P2P service. Its ownership consequences are specified once in Part 2.
|
||||||
|
|
||||||
|
### Separate policy, provider selection, and the active Replicator
|
||||||
|
|
||||||
|
Application lifecycle policy decides **when** synchronisation is requested.
|
||||||
|
The selected provider and its active Replicator decide **how**, and whether,
|
||||||
|
that request can be performed. A provider module must not subscribe to the
|
||||||
|
application resume lifecycle merely because it can construct a transport.
|
||||||
|
|
||||||
|
Self-hosted LiveSync will have one LiveSync-owned core ServiceModule as the
|
||||||
|
replication lifecycle coordinator. It uses `AppLifecycleService`, persisted
|
||||||
|
settings, `ReplicationService`, and the active support declaration. It does
|
||||||
|
not branch on `remoteType` or use `instanceof` as a capability test.
|
||||||
|
Commonlib owns the trigger-aware replication contract; the host owns
|
||||||
|
application lifecycle wiring.
|
||||||
|
|
||||||
|
The existing `onResumed` event remains the eligible-resume boundary after
|
||||||
|
initial readiness, settings application, and visibility recovery. It is not
|
||||||
|
redefined as a once-per-process event. The coordinator coalesces duplicate work
|
||||||
|
within one lifecycle generation and preserves readiness and suspension gates:
|
||||||
|
|
||||||
|
- configured Continuous replication starts only through an active Continuous
|
||||||
|
role;
|
||||||
|
- otherwise, configured `syncOnStart` runs through an unattended OneShot role
|
||||||
|
when that role is supported;
|
||||||
|
- an unsupported configured policy produces an explicit unsupported or
|
||||||
|
not-implemented result; and
|
||||||
|
- automatic start-up, periodic, file-event, and merge triggers never open a
|
||||||
|
dialogue. A target-requiring operation is available to an explicit user
|
||||||
|
action or to a flow with a configured target.
|
||||||
|
|
||||||
|
`ReplicationService` remains responsible for readiness checks, bounded finite
|
||||||
|
activity, failure processing, and replication timing. It exposes distinct
|
||||||
|
user-initiated and unattended entry points, or a typed request which carries
|
||||||
|
interaction authority. The coordinator never calls a concrete Replicator's
|
||||||
|
`openReplication()` directly.
|
||||||
|
|
||||||
|
`P2P_AutoStart` remains a separate P2P room policy. It is not central
|
||||||
|
Continuous replication and is not `syncOnStart`; its service lifecycle is
|
||||||
|
specified in Part 2. Reopening after `EVENT_DATABASE_REBUILT` is a
|
||||||
|
flow-authorised continuation requested by the Rebuilder, not evidence that
|
||||||
|
AutoStart is enabled.
|
||||||
|
|
||||||
|
The CLI daemon owns its initial finite convergence before its mirror scan.
|
||||||
|
Restored settings mark that convergence as satisfied for the current lifecycle
|
||||||
|
generation, so `syncOnStart` does not repeat it. In `--interval` mode, the
|
||||||
|
daemon poller is the sole recurring remote-poll scheduler. In changes-feed mode,
|
||||||
|
the coordinator starts one configured Continuous session when supported;
|
||||||
|
otherwise it may enable the configured generic periodic timer. Continuous has
|
||||||
|
precedence when both are configured.
|
||||||
|
|
||||||
|
### Use a fixed current-provider definition
|
||||||
|
|
||||||
|
Commonlib defines the canonical current remote kinds, provider contract,
|
||||||
|
capability catalogue, support-decision type, and typed provider builder. Each
|
||||||
|
host composition explicitly declares the provider kinds which it includes and
|
||||||
|
supplies an exhaustive definition table for that set. The current catalogue is
|
||||||
|
CouchDB, Object Storage, and P2P; it is not a public third-party registration
|
||||||
|
API.
|
||||||
|
|
||||||
|
CouchDB is part of every current host composition. Object Storage and P2P are
|
||||||
|
compile-time composition choices and may be included or omitted without
|
||||||
|
changing the generic lifecycle coordinator. Adding another current provider
|
||||||
|
requires a Commonlib kind and support declaration, host composition,
|
||||||
|
Setup/profile schema handling, and provider-specific tests. It does not require
|
||||||
|
a runtime plug-in registry or behaviour for unknown provider kinds.
|
||||||
|
|
||||||
|
Each provider definition supplies:
|
||||||
|
|
||||||
|
- canonical kind and diagnostic name;
|
||||||
|
- complete support metadata for the current catalogue;
|
||||||
|
- active Replicator construction;
|
||||||
|
- provider configuration identity and an explicit same-kind rebind-or-replace
|
||||||
|
policy;
|
||||||
|
- flow-specific probe and initial-transfer dependency factories; and
|
||||||
|
- provider-specific entry points required by existing current flows.
|
||||||
|
|
||||||
|
The host composes CouchDB and Object Storage definitions directly. A module is
|
||||||
|
retained only when it owns separate state or behaviour; a factory-registration-
|
||||||
|
only module instance is not required. The stateful P2P service is composed
|
||||||
|
independently and may be an adjunct beside a different selected main provider.
|
||||||
|
|
||||||
|
The definition table uses `satisfies` against required record keys. Adding a
|
||||||
|
current provider or catalogue entry without a support decision is a compile-time
|
||||||
|
error. A typed builder correlates each `supported` decision with its required
|
||||||
|
role and rejects a role for an absent capability.
|
||||||
|
|
||||||
|
Support has three stable states:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type CapabilitySupport =
|
||||||
|
| { readonly kind: "supported" }
|
||||||
|
| { readonly kind: "not-implemented"; readonly reason: CapabilityReason }
|
||||||
|
| { readonly kind: "not-applicable"; readonly reason: CapabilityReason };
|
||||||
|
```
|
||||||
|
|
||||||
|
`not-implemented` means that the provider model exists but the current
|
||||||
|
implementation does not supply it. `not-applicable` means that the model does
|
||||||
|
not exist for that provider, such as central database locking for P2P. Reasons
|
||||||
|
are stable codes, not arbitrary user-facing strings.
|
||||||
|
|
||||||
|
Historical empty `remoteType` is resolved explicitly as CouchDB at the
|
||||||
|
persistence/profile boundary. Capability selection then uses that canonical
|
||||||
|
kind; it never infers CouchDB by truthiness or by a negative test such as
|
||||||
|
'neither Object Storage nor P2P'.
|
||||||
|
|
||||||
|
If a provider caches effective connection settings, its declared rebind or
|
||||||
|
replacement policy keeps the active adapter current after a same-kind profile
|
||||||
|
change. Reconciliation is serialised with active work and leaves no old
|
||||||
|
credentials, Security Seed state, journal checkpoint, adapter cache, or
|
||||||
|
diagnostic connection reachable from the reconciled handle.
|
||||||
|
|
||||||
|
### Separate exhaustive support metadata from narrow runtime roles
|
||||||
|
|
||||||
|
Every provider definition contains a required support record over the complete
|
||||||
|
current catalogue. An active Replicator exposes only the narrow roles marked as
|
||||||
|
supported by its definition. This gives compile-time completeness without a
|
||||||
|
giant runtime facade or a collection of Boolean flags.
|
||||||
|
|
||||||
|
The generic catalogue covers user-initiated and unattended OneShot Sync,
|
||||||
|
ordinary long-lived Continuous replication, full upload and download, central
|
||||||
|
reset and lock administration, preferred-tweak Metadata read and write,
|
||||||
|
on-demand remote Chunk reads, remote storage status, compromised-Chunk
|
||||||
|
inspection, central Security Seed, and a request to stop active transfer.
|
||||||
|
|
||||||
|
Full upload/download, reset/lock, and Metadata read/write remain separate roles.
|
||||||
|
Provider initialisation remains flow-specific: CouchDB may create a database,
|
||||||
|
whereas Object Storage prepares its Security Seed and does not provision a
|
||||||
|
bucket. Provider-specific CouchDB maintenance, Object Storage journal
|
||||||
|
checkpoint maintenance, and P2P room operations are narrowed facets, not
|
||||||
|
generic feature tests. P2P facets are defined in Part 2.
|
||||||
|
|
||||||
|
Local node identity initialisation is a database and Replicator lifecycle
|
||||||
|
concern, not a remote capability. Replication statistics remain a
|
||||||
|
`ReplicatorService` telemetry sink.
|
||||||
|
|
||||||
|
### Make unattended work explicit and truthful
|
||||||
|
|
||||||
|
User-initiated and unattended OneShot Sync are separate roles. Interaction
|
||||||
|
authority is an upper bound on local interaction, not an instruction to display
|
||||||
|
a dialogue. An operation may apply a stricter veto, but cannot request an
|
||||||
|
interaction which its caller did not permit. Remote refusal and incoming-peer
|
||||||
|
consent remain independent decisions.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type UnattendedTrigger = "resume" | "periodic" | "database-event" | "editor-save" | "file-open" | "merge" | "daemon";
|
||||||
|
|
||||||
|
interface InteractionPermissions {
|
||||||
|
readonly peerSelection: boolean;
|
||||||
|
readonly localPeerAdmission: boolean;
|
||||||
|
readonly configurationExchange: boolean;
|
||||||
|
readonly failureRecovery: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermittedInteractionPermissions =
|
||||||
|
| (InteractionPermissions & { readonly peerSelection: true })
|
||||||
|
| (InteractionPermissions & { readonly localPeerAdmission: true })
|
||||||
|
| (InteractionPermissions & { readonly configurationExchange: true })
|
||||||
|
| (InteractionPermissions & { readonly failureRecovery: true });
|
||||||
|
|
||||||
|
type InteractionAuthority =
|
||||||
|
| typeof NO_INTERACTION
|
||||||
|
| { readonly kind: "permitted"; readonly permissions: PermittedInteractionPermissions };
|
||||||
|
|
||||||
|
const NO_INTERACTION = { kind: "forbidden" } as const;
|
||||||
|
|
||||||
|
interface UserInitiatedOneShotRequest {
|
||||||
|
readonly trigger: "manual";
|
||||||
|
readonly interaction: InteractionAuthority;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserInitiatedOneShot {
|
||||||
|
run(request: UserInitiatedOneShotRequest): Promise<ReplicationOutcome>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnattendedOneShot {
|
||||||
|
run(request: {
|
||||||
|
readonly trigger: UnattendedTrigger;
|
||||||
|
readonly interaction: typeof NO_INTERACTION;
|
||||||
|
}): Promise<ReplicationOutcome>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`NO_INTERACTION` is the only all-false authority. Shared immutable authority
|
||||||
|
values are reused by hosts, avoiding a permission allocation per operation.
|
||||||
|
Operation-specific requests, including P2P configuration exchange, carry the
|
||||||
|
same upper bound and expose only relevant permissions. An unattended path may
|
||||||
|
use persisted or automatic acceptance policy, but cannot obtain local
|
||||||
|
interaction authority implicitly.
|
||||||
|
|
||||||
|
Outcomes do not collapse failure into `void` or `boolean`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const REPLICATION_COMPLETED = { status: "completed" } as const;
|
||||||
|
const REPLICATION_CANCELLED = { status: "cancelled" } as const;
|
||||||
|
|
||||||
|
type ReplicationOutcome =
|
||||||
|
| typeof REPLICATION_COMPLETED
|
||||||
|
| typeof REPLICATION_CANCELLED
|
||||||
|
| { readonly status: "blocked"; readonly reason: ReplicationBlockReason }
|
||||||
|
| { readonly status: "partial"; readonly detail: PartialReplicationDetail }
|
||||||
|
| { readonly status: "failed"; readonly error: unknown };
|
||||||
|
```
|
||||||
|
|
||||||
|
Completed and cancelled values are shared singletons or literals. Blocked,
|
||||||
|
partial, and failed results may carry diagnostic detail. Central provider
|
||||||
|
initialisation, reset, lock, unlock, and resolution settle only after their
|
||||||
|
defined remote write succeeds; a Rebuilder must not continue after an ignored
|
||||||
|
mutation failure.
|
||||||
|
|
||||||
|
### Distinguish observation from an identity result only when required
|
||||||
|
|
||||||
|
Capability availability and operation results are separate. A supported
|
||||||
|
operation may fail, while an inapplicable operation must not be called or
|
||||||
|
reported as a network attempt.
|
||||||
|
|
||||||
|
For an observation where an empty value changes a safety decision, use a tagged
|
||||||
|
result:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type RemoteObservation<T> =
|
||||||
|
| { readonly kind: "observed"; readonly value: T }
|
||||||
|
| { readonly kind: "unavailable"; readonly error?: unknown };
|
||||||
|
```
|
||||||
|
|
||||||
|
Compromised-Chunk inspection is the current example. `observed: 0` proves that
|
||||||
|
the supported inspection found no matching entries; `unavailable` says that it
|
||||||
|
did not complete, including while offline. A caller must handle every result
|
||||||
|
before declaring the remote clean.
|
||||||
|
|
||||||
|
Do not wrap every result. When an empty array is the safe, documented identity
|
||||||
|
for every current caller, retain it. High-frequency Chunk, document,
|
||||||
|
changes-feed, and queue paths keep arrays, iterators, and scalars unless a
|
||||||
|
measurement and a safety distinction justify a tag. No wrapper is added per
|
||||||
|
Chunk, document, changes-feed row, or queue item.
|
||||||
|
|
||||||
|
### Give active ownership and probes explicit boundaries
|
||||||
|
|
||||||
|
`ReplicatorService` is the sole owner of the active Replicator. Replacement is
|
||||||
|
an atomic transition under its transition lock:
|
||||||
|
|
||||||
|
```text
|
||||||
|
active -> retiring -> disposed -> replacement published
|
||||||
|
```
|
||||||
|
|
||||||
|
Acquisitions wait for the transition and receive only the replacement. A fenced
|
||||||
|
old handle cannot start work. Disposal stops Continuous activity, awaits work
|
||||||
|
which cannot be cancelled, and settles or reports every operation owned by the
|
||||||
|
active adapter before publication. If bounded retirement cannot settle,
|
||||||
|
replacement fails visibly and no new handle is published; the old handle is
|
||||||
|
disposed when late work settles.
|
||||||
|
|
||||||
|
The generic stop role is an idempotent request to stop a provider transfer after
|
||||||
|
transport work begins. It does not promise cancellation of readiness checks,
|
||||||
|
Security Seed acquisition, or external storage calls which do not consume a
|
||||||
|
cancellation signal. P2P declares this role `not-implemented` until its lower
|
||||||
|
level transfer and RPC work have a real stop path.
|
||||||
|
|
||||||
|
`getNewReplicator()` is not a general temporary-instance API. Setup and settings
|
||||||
|
flows request narrow probes, such as connection, preferred-tweak, or isolated
|
||||||
|
P2P signalling validation. A resource-returning factory returns an owned
|
||||||
|
resource with idempotent asynchronous `dispose()`. Trial settings are passed
|
||||||
|
to the probe itself and cannot silently read active settings. A probe cannot
|
||||||
|
replace the active Replicator or the P2P service.
|
||||||
|
|
||||||
|
Streaming Fetch receives an owned CouchDB HTTP configuration and
|
||||||
|
`RemoteSecuritySeed` supplier directly. The supplier is bound to the selected
|
||||||
|
configuration identity, invalidates cached seed state on reconciliation, and
|
||||||
|
is disposed by the initial-transfer flow. It does not construct a temporary
|
||||||
|
full Replicator.
|
||||||
|
|
||||||
|
### Keep initialisation workflows explicit
|
||||||
|
|
||||||
|
- CouchDB and Object Storage first-device rebuilds reset and lock their central
|
||||||
|
remote, then perform the established convergence upload.
|
||||||
|
- A P2P first-device rebuild prepares the local database without pretending to
|
||||||
|
reset, lock, or upload to a central remote.
|
||||||
|
- CouchDB and Object Storage Fetch use their central full-download flow.
|
||||||
|
- A P2P additional-device Fetch selects a peer and performs one full download.
|
||||||
|
- CouchDB Streaming Fetch remains the separate initial-transfer service above.
|
||||||
|
|
||||||
|
The Rebuilder requests capabilities and does not cast a generic Replicator to a
|
||||||
|
concrete class. Its `EVENT_DATABASE_REBUILT` continuation is separately
|
||||||
|
authorised and does not imply `syncOnStart` or P2P AutoStart.
|
||||||
|
|
||||||
|
## Target capability matrix for current providers
|
||||||
|
|
||||||
|
`S` means supported, `NI` means not implemented, and `NA` means not applicable.
|
||||||
|
Configuration and reachability are request preconditions or outcomes, not
|
||||||
|
support states.
|
||||||
|
|
||||||
|
| Generic role | CouchDB | Object Storage | P2P |
|
||||||
|
| --------------------------------------- | ------- | -------------- | --- |
|
||||||
|
| User-initiated OneShot Sync | S | S | S |
|
||||||
|
| Unattended OneShot Sync without UI | S | S | S |
|
||||||
|
| Ordinary long-lived Continuous session | S | NA | NA |
|
||||||
|
| Full upload to a central remote | S | S | NA |
|
||||||
|
| Full download | S | S | S |
|
||||||
|
| Central remote reset | S | S | NA |
|
||||||
|
| Central remote lock and resolution | S | S | NA |
|
||||||
|
| Preferred-tweak Metadata read and write | S | S | NA |
|
||||||
|
| On-demand remote Chunk source | S | NA | NA |
|
||||||
|
| Remote storage status | S | S | NA |
|
||||||
|
| Compromised-Chunk inspection | S | NA | NA |
|
||||||
|
| Central-remote Security Seed | S | S | NA |
|
||||||
|
| Request to stop active transfer | S | S | NI |
|
||||||
|
|
||||||
|
P2P unattended OneShot means a role exists which uses configured target names
|
||||||
|
without opening a dialogue. Peer-room, watch, acceptance, and broadcast roles
|
||||||
|
are provider-specific facets, not the ordinary Continuous role; their trigger
|
||||||
|
matrix and de-duplication rules are owned by Part 2.
|
||||||
|
|
||||||
|
## Alternatives rejected
|
||||||
|
|
||||||
|
### Move the resume handler and retain a `remoteType` switch
|
||||||
|
|
||||||
|
This would fix issue 1140 narrowly, but would leave future providers and
|
||||||
|
triggers subject to the same omission. It would not stop automatic P2P calls
|
||||||
|
from reaching an interactive method, and capability semantics would remain
|
||||||
|
implicit.
|
||||||
|
|
||||||
|
### Use only optional methods or Boolean support flags
|
||||||
|
|
||||||
|
Optional runtime roles are useful, but they do not force a support decision when
|
||||||
|
a provider or catalogue entry is added. Exhaustive support metadata plus a
|
||||||
|
typed builder keeps runtime interfaces small while requiring the decision at
|
||||||
|
compile time.
|
||||||
|
|
||||||
|
### Use only a provider-discriminated union
|
||||||
|
|
||||||
|
A provider union is appropriate for provider-specific maintenance after
|
||||||
|
explicit narrowing. It cannot model adjunct P2P beside another selected main
|
||||||
|
provider and is not a generic feature test.
|
||||||
|
|
||||||
|
### Tag every value and hot-path return
|
||||||
|
|
||||||
|
Uniform wrappers would add allocation and noise to Chunk, document,
|
||||||
|
changes-feed, and queue paths without improving semantics where an identity is
|
||||||
|
safe. Tags remain for low-frequency observations and outcomes which affect
|
||||||
|
control flow.
|
||||||
|
|
||||||
|
### Retain `getNewReplicator()` as the trial and command factory
|
||||||
|
|
||||||
|
The handler is order-dependent, can suppress construction errors, and can
|
||||||
|
replace a feature-owned P2P transport. Flow-specific probes and the stable P2P
|
||||||
|
service make ownership explicit.
|
||||||
|
|
||||||
|
### Treat neutral compatibility results as supported operations
|
||||||
|
|
||||||
|
Dummy zero counts, empty Security Seeds, false values, and silent mutations
|
||||||
|
lose distinctions required for safety and recovery. A neutral value remains
|
||||||
|
only when every caller proves it to be the operation's identity.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `syncOnStart` becomes an application policy for every unattended finite
|
||||||
|
provider which supports it, rather than a CouchDB module behaviour.
|
||||||
|
- Adding a current provider or capability requires an explicit compile-time
|
||||||
|
support decision.
|
||||||
|
- Unsupported central operations are no longer represented as successful P2P
|
||||||
|
no-ops or transport errors.
|
||||||
|
- Safe identities remain simple, while safety-sensitive observations are
|
||||||
|
explicit.
|
||||||
|
- Setup and trial configuration cannot replace an active or adjunct transport.
|
||||||
|
- Central mutations and integrity checks cannot silently turn failure or
|
||||||
|
unavailability into success.
|
||||||
|
- P2P remains first-class without being mislabelled as central Continuous
|
||||||
|
replication.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Do not introduce third-party remote-provider registration.
|
||||||
|
- Do not redesign every replication result or user-facing message in one
|
||||||
|
change.
|
||||||
|
- Do not make Streaming Fetch a generic Replicator capability.
|
||||||
|
- Do not make P2P pretend to own a central remote database.
|
||||||
|
- Do not infer support from `remoteType`, constructor identity, a falsy result,
|
||||||
|
or a neutral value.
|
||||||
|
- Do not redefine `syncOnStart` as a once-per-process setting.
|
||||||
|
- Do not change established profile persistence or Setup flag-file restart
|
||||||
|
ordering as part of this capability split.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Part 2: P2P service and session lifecycle](2026_08_replicator_capabilities_02_p2p_service_lifecycle.md)
|
||||||
|
- [Part 3: migration plan and verification](2026_08_replicator_capabilities_03_migration_plan.md)
|
||||||
|
- [Bounded Remote Activity](2026_07_bounded_remote_activity.md)
|
||||||
|
- [Make Onboarding Profile-Aware](2026_07_multiple_remote_onboarding.md)
|
||||||
|
- [P2P Room and Transport Lifecycle](2026_07_p2p_transport_lifecycle.md)
|
||||||
|
- [P2P Transport Compatibility Controls](2026_08_p2p_transport_compatibility.md)
|
||||||
|
- [CouchDB Remote Connection Ownership](2026_08_couchdb_remote_connection_ownership.md)
|
||||||
|
- [Package the Common Library Behind Explicit Host Boundaries](2026_07_common_library_package_boundary.md)
|
||||||
|
- [Self-hosted LiveSync issue 1140](https://github.com/vrtmrz/obsidian-livesync/issues/1140)
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
---
|
||||||
|
date: 2026-08-27
|
||||||
|
commonlib-version: "0.1.19"
|
||||||
|
self-hosted-livesync-version: "1.0.21"
|
||||||
|
status: proposed
|
||||||
|
series: replicator-capabilities-and-lifecycle
|
||||||
|
part: 2 of 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architectural Decision Record: Replicator Capabilities and Lifecycle Orchestration — Part 2: P2P Service and Session Lifecycle
|
||||||
|
|
||||||
|
Series navigation: this is Part 2 of 3. Read [Part 1: core contract](2026_08_replicator_capabilities_01_core_contract.md)
|
||||||
|
first, then continue with [Part 3: migration plan and verification](2026_08_replicator_capabilities_03_migration_plan.md).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed. This record defines the P2P service owner, room-session boundary,
|
||||||
|
narrow contract views, automation demands, replacement fencing, and trigger
|
||||||
|
semantics. Generic provider and capability rules are owned by Part 1; the
|
||||||
|
implementation and verification order is owned by Part 3.
|
||||||
|
|
||||||
|
The accepted [P2P Room and Transport Lifecycle](2026_07_p2p_transport_lifecycle.md)
|
||||||
|
record remains the description of the current implementation until Stage 3 in
|
||||||
|
Part 3 is complete. Stage 3 then supersedes only that record's replaceable
|
||||||
|
LiveSync P2P Replicator and current-result ownership. Its decisions about
|
||||||
|
serialised room operations, `room.leave()`, Trystero-owned physical peers, and
|
||||||
|
relay reconnection remain in force.
|
||||||
|
|
||||||
|
## Scope and context
|
||||||
|
|
||||||
|
The Commonlib P2P feature currently spans `TrysteroReplicatorP2PServer`,
|
||||||
|
`TrysteroReplicator`, and the LiveSync-specific `LiveSyncTrysteroReplicator`.
|
||||||
|
The current result resolves a replaceable Replicator while the room, signalling,
|
||||||
|
watch, broadcast, diagnostics, platform-event subscriptions, and database
|
||||||
|
feeds have longer-lived relationships. Installing that replaceable object as
|
||||||
|
the active Replicator makes ordinary active-handle disposal capable of closing
|
||||||
|
an adjunct or policy-owned room.
|
||||||
|
|
||||||
|
The same room membership serves several independent behaviours:
|
||||||
|
|
||||||
|
- P2P AutoStart opens the room and signalling service;
|
||||||
|
- AutoSync reacts to an advertised and accepted peer with one finite transfer;
|
||||||
|
- AutoWatch follows later changes broadcast by a selected peer;
|
||||||
|
- AutoBroadcast publishes local database changes;
|
||||||
|
- explicit commands pull, push, or synchronise against a selected peer;
|
||||||
|
- incoming `reqSync` requests pull from an accepted peer; and
|
||||||
|
- diagnostics and platform events observe the transport.
|
||||||
|
|
||||||
|
These behaviours must share one room owner, but they must not share one
|
||||||
|
implicit policy. A setting change may replace the room session while another
|
||||||
|
provider remains the selected main remote. A setup probe must not close that
|
||||||
|
session. A finite operation may need a room even when AutoStart is disabled.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### One stable P2P service owns each room session
|
||||||
|
|
||||||
|
The host composes one stable P2P service independently of the selected main
|
||||||
|
provider. It may be present as an adjunct beside CouchDB or Object Storage, or
|
||||||
|
its non-owning adapter may serve as the selected main P2P Replicator. The host
|
||||||
|
owns the service lifetime; the service owns all LiveSync-specific room
|
||||||
|
resources.
|
||||||
|
|
||||||
|
A **P2P room session** is one active room membership and every resource whose
|
||||||
|
validity depends on that membership:
|
||||||
|
|
||||||
|
- the Trystero room, RPC actions, and `RpcRoom`;
|
||||||
|
- its internal session epoch and cancellation signal;
|
||||||
|
- advertisement state and temporary peer decisions;
|
||||||
|
- peer-bound RPC clients and remote database proxies;
|
||||||
|
- connection, diagnostic, and platform-event subscriptions; and
|
||||||
|
- RPC publication, the watch set, database and broadcast change feeds, and
|
||||||
|
in-flight finite-transfer de-duplication bound to the local database.
|
||||||
|
|
||||||
|
A **session epoch** is an internal fence for one room session. It is not a
|
||||||
|
public capability, a persisted profile identifier, or a synonym for the
|
||||||
|
logical room. Every callback, snapshot, and operation token carrying
|
||||||
|
session-bound state is checked against the current epoch.
|
||||||
|
|
||||||
|
Closing or replacing a session fences its epoch, stops new operations, settles
|
||||||
|
or fails in-flight work, closes RPC and client resources, and leaves the room
|
||||||
|
in the transport-owned order. Persisted peer acceptance decisions survive;
|
||||||
|
temporary decisions, advertisements, clients, listeners, and feeds do not.
|
||||||
|
The underlying WebRTC peer remains under Trystero's shared-peer ownership as
|
||||||
|
specified by the accepted lifecycle record.
|
||||||
|
|
||||||
|
### Expose seven narrow contract views
|
||||||
|
|
||||||
|
The service does not expose a room session, raw host, or concrete Replicator to
|
||||||
|
ordinary consumers. It supplies these seven views over the same owner:
|
||||||
|
|
||||||
|
1. `P2PTransportLifecycle` observes room state and accepts explicit,
|
||||||
|
user-owned connect or disconnect requests.
|
||||||
|
2. `P2PPeerDirectory` supplies peer snapshots and peer arrival or departure.
|
||||||
|
3. `P2PPeerAdmission` evaluates incoming peers and administers temporary or
|
||||||
|
persisted acceptance decisions.
|
||||||
|
4. `P2PTargetedTransfer` performs pull, requested push, and bidirectional
|
||||||
|
finite synchronisation against an explicit peer.
|
||||||
|
5. `P2PChangeRelay` administers peer watch and local-change broadcast.
|
||||||
|
6. `P2PConfigurationExchange` performs peer configuration exchange under its
|
||||||
|
declared interaction authority.
|
||||||
|
7. `P2PDiagnostics` supplies status and RTC diagnostics without exposing raw
|
||||||
|
room or peer connections.
|
||||||
|
|
||||||
|
These are stable service-level contract views, not seven wrapper allocations or
|
||||||
|
independent state owners. One implementation may satisfy several views.
|
||||||
|
Advertisement and admission state remain under one peer-access owner, while
|
||||||
|
pull, push, and bidirectional transfer remain under one transfer owner. A
|
||||||
|
consumer which needs more than one view receives those views explicitly; it
|
||||||
|
does not receive a general P2P context or service locator.
|
||||||
|
|
||||||
|
The views resolve the current published session at invocation. An operation
|
||||||
|
which carries an old epoch returns a stale or blocked result rather than
|
||||||
|
dispatching into a replacement. The epoch remains internal. The active P2P
|
||||||
|
Replicator is a non-owning adapter over the views, and its disposal cannot
|
||||||
|
implicitly leave the service-owned room.
|
||||||
|
|
||||||
|
### Make explicit disconnect a veto, not another policy demand
|
||||||
|
|
||||||
|
`P2PTransportLifecycle` distinguishes user intent from automation:
|
||||||
|
|
||||||
|
- explicit connect resumes relay reconnection and establishes a user-owned
|
||||||
|
room demand;
|
||||||
|
- explicit disconnect is the sole force-close path, retires the current
|
||||||
|
session, invalidates all demands under the retirement contract, pauses relay
|
||||||
|
reconnection, and establishes a service-lifetime veto against AutoStart; and
|
||||||
|
- only a later explicit connect clears that veto.
|
||||||
|
|
||||||
|
Automated policies and finite operations acquire or release only their own
|
||||||
|
demands. They cannot close a room held by another demand and cannot override an
|
||||||
|
explicit disconnect veto. This is a lifecycle veto, not the opposite of
|
||||||
|
`InteractionAuthority`: local interaction authority is the upper bound used by
|
||||||
|
operations, as specified in Part 1. An operation may impose a stricter veto,
|
||||||
|
but an unattended operation cannot gain permission to open a dialogue.
|
||||||
|
|
||||||
|
### Separate automation policy from room ownership
|
||||||
|
|
||||||
|
P2P automation is a composed service feature. It owns `P2P_AutoStart`,
|
||||||
|
AutoSync, AutoWatch, and AutoBroadcast policy, including delayed work and
|
||||||
|
automatic-trigger coalescing. It consumes the transport, peer, admission,
|
||||||
|
transfer, and change-relay views but does not own their mutable state.
|
||||||
|
|
||||||
|
`P2PChangeRelay` owns the actual watch set and database changes feed.
|
||||||
|
`P2PTargetedTransfer` owns in-flight transfer de-duplication. Incoming-peer
|
||||||
|
consent is distinct from a local caller's authority to select a peer or open a
|
||||||
|
dialogue. Persisted or automatic acceptance may authorise an unattended path;
|
||||||
|
it cannot create local interaction authority implicitly.
|
||||||
|
|
||||||
|
Every finite operation which needs a room obtains its own internal,
|
||||||
|
session-epoch-bound demand. Acquiring another demand does not open another
|
||||||
|
session. Releasing it never closes a session still required by AutoStart,
|
||||||
|
another finite operation, or another host consumer. Demand bookkeeping is
|
||||||
|
internal to the service and is not a general consumer contract; it cannot turn
|
||||||
|
a finite transfer into persistent transport policy.
|
||||||
|
|
||||||
|
### Reconcile session settings atomically
|
||||||
|
|
||||||
|
The effective P2P session binding is derived from the selected profile, all
|
||||||
|
settings which affect transport or session-bound automation, and the current
|
||||||
|
local database identity. It is not a new persisted profile identifier or a
|
||||||
|
device-local override. The service reconciles this binding independently of
|
||||||
|
the selected main provider, so an adjunct room can be replaced while CouchDB
|
||||||
|
or Object Storage remains active.
|
||||||
|
|
||||||
|
A change to any binding input retires the whole room session and opens a
|
||||||
|
replacement when policy still requires one. Replacing a policy-only setting
|
||||||
|
may cause a temporary disconnect, but it preserves one atomic listener and
|
||||||
|
policy boundary: advertisements and temporary peer decisions are reacquired,
|
||||||
|
while persisted peer decisions survive. AutoStart reconnects when it remains
|
||||||
|
enabled and no explicit-disconnect veto is active. No old listener, credential,
|
||||||
|
client, or policy demand remains reachable after replacement.
|
||||||
|
|
||||||
|
Reconciliation is serialised with room lifecycle operations:
|
||||||
|
|
||||||
|
1. fence new session work;
|
||||||
|
2. allow already-started finite transfers to settle, because the current lower
|
||||||
|
level transfer may not yet have a real cancellation contract;
|
||||||
|
3. close and leave the old room in transport-owned order;
|
||||||
|
4. open and validate the candidate session; and
|
||||||
|
5. publish the replacement only after it has opened successfully.
|
||||||
|
|
||||||
|
The service never exposes a partly initialised candidate or silently interrupts
|
||||||
|
a transfer without a cancellation contract. If bounded settlement or candidate
|
||||||
|
opening fails, reconciliation reports the failure and publishes no mixed old
|
||||||
|
and new session. A candidate-open failure leaves one observable disconnected
|
||||||
|
state with policy demands unsatisfied; it does not revive the fenced session or
|
||||||
|
start an unbounded retry loop. A later lifecycle trigger or explicit connect may
|
||||||
|
retry.
|
||||||
|
|
||||||
|
### Order local database replacement across both owners
|
||||||
|
|
||||||
|
The database lifecycle transition owns ordering above `ReplicatorService` and
|
||||||
|
the P2P service:
|
||||||
|
|
||||||
|
1. fence acquisition of active Replicator and P2P room work;
|
||||||
|
2. retire the active adapter;
|
||||||
|
3. settle and retire P2P database-bound feeds and publication;
|
||||||
|
4. publish the replacement local database identity only after both boundaries
|
||||||
|
have settled;
|
||||||
|
5. rebind or replace the active provider;
|
||||||
|
6. reconcile P2P against the new database identity; and
|
||||||
|
7. let the Rebuilder request its separately authorised reopen.
|
||||||
|
|
||||||
|
Each service serialises its own resources. The database transition owns the
|
||||||
|
cross-service ordering rather than introducing one transport-wide lock.
|
||||||
|
|
||||||
|
### De-duplicate by logical lifecycle, not by room epoch
|
||||||
|
|
||||||
|
Baseline AutoSync transfers are de-duplicated by peer and application lifecycle
|
||||||
|
generation. Trigger provenance and session epoch are not part of the key, so a
|
||||||
|
reconnect alone cannot repeat an in-flight or completed baseline transfer.
|
||||||
|
|
||||||
|
In-flight de-duplication belongs to the current room session. Completed baseline
|
||||||
|
history belongs to the stable automation owner and survives transport-only or
|
||||||
|
policy-only session replacement. Only completed per-peer baselines are settled;
|
||||||
|
partial requests record completed peers only, while blocked, cancelled, failed,
|
||||||
|
and incomplete peers remain eligible for a later bounded retry.
|
||||||
|
|
||||||
|
The automation owner clears settled records when the logical peer namespace or
|
||||||
|
local database identity changes. It retains them across transport-only and
|
||||||
|
policy-only replacement. Watch may follow a later advertised change, but does
|
||||||
|
not immediately repeat a completed baseline transfer.
|
||||||
|
|
||||||
|
### Define the P2P trigger matrix
|
||||||
|
|
||||||
|
| Trigger | Required preconditions | Effect |
|
||||||
|
| ----------------------- | --------------------------------------------------------------------- | -------------------------------------------- |
|
||||||
|
| `P2P_AutoStart` | P2P enabled, active lifecycle generation, and no user disconnect veto | Open room; do not itself transfer files |
|
||||||
|
| `P2P_AutoSyncPeers` | Open room, matching advertisement, and accepted peer policy | Run one bidirectional finite synchronisation |
|
||||||
|
| `P2P_AutoWatchPeers` | Open room, matching accepted advertisement, and remote broadcasting | Pull later announced updates |
|
||||||
|
| `P2P_AutoBroadcast` | Open room and local broadcasting enabled | Announce later local database changes |
|
||||||
|
| `P2P_SyncOnReplication` | Configured names and advertisements received within a bounded wait | Run target-aware unattended OneShot Sync |
|
||||||
|
| Explicit peer command | Supplied peer target and accepted connection | Run user-owned finite synchronisation |
|
||||||
|
| Incoming `reqSync` | Accepted peer and ordinary readiness | Pull from requesting peer |
|
||||||
|
|
||||||
|
`P2P_AutoStart` is a transport policy, not central Continuous replication and
|
||||||
|
not `syncOnStart`. AutoSync, AutoWatch, and accepted incoming requests remain
|
||||||
|
unattended when their persisted policies permit them. A configured-target
|
||||||
|
request waits for advertisement for a bounded period; it does not inspect a
|
||||||
|
possibly stale snapshot immediately after opening. Missing, undiscovered,
|
||||||
|
unaccepted, or partly successful targets are explicit operation results. An
|
||||||
|
unknown peer never opens an acceptance dialogue on an unattended path.
|
||||||
|
|
||||||
|
Delayed opens belong to the lifecycle generation which scheduled them.
|
||||||
|
Suspension cancels them or makes them harmless, and the callback rechecks
|
||||||
|
current settings and suspension state before opening. Ordinary automation uses
|
||||||
|
the trigger-aware readiness policy rather than bypassing readiness, pending-file
|
||||||
|
settlement, clean-up, or version gates. Fetch and Rebuild retain their
|
||||||
|
separately authorised bypasses.
|
||||||
|
|
||||||
|
### Keep probes isolated from the active service
|
||||||
|
|
||||||
|
Setup and settings use a separately owned `P2PConnectionProbe`. It does not
|
||||||
|
borrow or replace the current room session, and its `dispose()` cannot close the
|
||||||
|
active service. Because the current implementation may use process-global
|
||||||
|
relay sockets, a probe must either allocate isolated transport resources or the
|
||||||
|
host must reject concurrent probing while the active service uses those
|
||||||
|
sockets. It must not pause or close active relay sockets as a side effect of a
|
||||||
|
short-lived check.
|
||||||
|
|
||||||
|
### Keep provider composition explicit
|
||||||
|
|
||||||
|
The stable P2P service can be composed even when P2P is not the selected main
|
||||||
|
provider, while the generic provider table in Part 1 can include or omit the
|
||||||
|
P2P provider at compile time. No runtime unknown-provider registry is implied.
|
||||||
|
When P2P is selected as main, its active adapter delegates to this same service
|
||||||
|
and does not publish a second P2P lifecycle owner.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- P2P room, peer, watch, acceptance, transfer, configuration, and diagnostics
|
||||||
|
have one explicit owner and seven focused contracts.
|
||||||
|
- Disposing an active adapter cannot close a policy-owned or adjunct room.
|
||||||
|
- Explicit user disconnect has a clear veto boundary and cannot be undone by
|
||||||
|
AutoStart or a finite operation.
|
||||||
|
- Finite operations can request a room without changing persistent room policy.
|
||||||
|
- Settings and local database replacement cannot publish mixed-session state.
|
||||||
|
- Reconnects do not repeat completed baseline transfers merely because the room
|
||||||
|
epoch changed.
|
||||||
|
- Setup probes can validate P2P without mutating the active transport.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Do not make the P2P service a general service locator.
|
||||||
|
- Do not expose room, raw host, peer connection, or concrete Replicator state
|
||||||
|
to ordinary consumers.
|
||||||
|
- Do not make P2P own a central remote database.
|
||||||
|
- Do not reinterpret P2P AutoStart as `syncOnStart` or central Continuous
|
||||||
|
replication.
|
||||||
|
- Do not replace the accepted Trystero physical-peer and relay ownership
|
||||||
|
decisions.
|
||||||
|
- Do not add unbounded retry loops or pretend that a missing lower-level stop
|
||||||
|
operation is end-to-end cancellation.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Part 1: core contract](2026_08_replicator_capabilities_01_core_contract.md)
|
||||||
|
- [Part 3: migration plan and verification](2026_08_replicator_capabilities_03_migration_plan.md)
|
||||||
|
- [P2P Room and Transport Lifecycle](2026_07_p2p_transport_lifecycle.md)
|
||||||
|
- [P2P Transport Compatibility Controls](2026_08_p2p_transport_compatibility.md)
|
||||||
|
- [Bounded Remote Activity](2026_07_bounded_remote_activity.md)
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
---
|
||||||
|
date: 2026-08-27
|
||||||
|
commonlib-version: "0.1.19"
|
||||||
|
self-hosted-livesync-version: "1.0.21"
|
||||||
|
status: proposed
|
||||||
|
series: replicator-capabilities-and-lifecycle
|
||||||
|
part: 3 of 3
|
||||||
|
---
|
||||||
|
|
||||||
|
# Architectural Decision Record: Replicator Capabilities and Lifecycle Orchestration — Part 3: Migration Plan and Verification
|
||||||
|
|
||||||
|
Series navigation: this is Part 3 of 3. Start with [Part 1: core contract](2026_08_replicator_capabilities_01_core_contract.md),
|
||||||
|
then [Part 2: P2P service and session lifecycle](2026_08_replicator_capabilities_02_p2p_service_lifecycle.md).
|
||||||
|
This part owns implementation sequencing and verification; it does not add
|
||||||
|
another runtime contract.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed. The stages below are an implementation and verification order, not
|
||||||
|
independently releasable states. Commonlib and Self-hosted LiveSync must not
|
||||||
|
publish temporary support boundaries described by an incomplete stage. A
|
||||||
|
release follows only after the target matrix, ownership boundaries, and all
|
||||||
|
production-consumer migrations in Parts 1 and 2 are complete.
|
||||||
|
|
||||||
|
## Migration rules
|
||||||
|
|
||||||
|
Each Commonlib change first runs its focused unit and type-contract tests,
|
||||||
|
builds and validates the packed artefact, installs that exact artefact in
|
||||||
|
Self-hosted LiveSync, and runs focused downstream tests before either
|
||||||
|
repository advances. Compatibility methods remain until every production
|
||||||
|
consumer has migrated.
|
||||||
|
|
||||||
|
The smallest vertical contract which fixes issue 1140 takes priority over
|
||||||
|
unrelated probe, Fast Fetch, maintenance, integrity, and facade work. A stage
|
||||||
|
must not claim completion when it only changes a type declaration while a
|
||||||
|
production caller still uses the old ownership or interaction path.
|
||||||
|
|
||||||
|
## Stage 1: reproduce the current boundary failures
|
||||||
|
|
||||||
|
Add the smallest regression which configures an Object Storage active provider,
|
||||||
|
enables `syncOnStart`, invokes the resume lifecycle after readiness, and
|
||||||
|
expects one unattended finite synchronisation. Run it against 1.0.21 and
|
||||||
|
confirm the expected failure: the CouchDB-owned lifecycle handler excludes
|
||||||
|
Object Storage. Cover both an ordinary Object Storage profile and a migrated
|
||||||
|
profile which retains `liveSync: true`; unsupported Continuous must not suppress
|
||||||
|
the supported `syncOnStart` OneShot policy.
|
||||||
|
|
||||||
|
Add passing characterisation tests which inventory automatic P2P periodic,
|
||||||
|
database-save, editor-save, file-open, and merge triggers, together with P2P
|
||||||
|
AutoSync, AutoWatch, incoming-request, and nested finite-activity paths. These
|
||||||
|
tests record current ownership and dialogue behaviour so that later stages do
|
||||||
|
not accidentally remove a valid automatic path.
|
||||||
|
|
||||||
|
## Stage 2: fix issue 1140 through the minimum vertical contract
|
||||||
|
|
||||||
|
Add the fixed provider definitions and the minimum user-initiated, unattended,
|
||||||
|
and Continuous roles to Commonlib. Give `ReplicationService` typed entry points
|
||||||
|
which carry trigger and interaction policy. Immediately before changing
|
||||||
|
Object Storage handling, add a failing test in which a stopped or failed
|
||||||
|
journal transfer must not produce a completed outcome; then propagate the
|
||||||
|
actual journal result.
|
||||||
|
|
||||||
|
Add the LiveSync-owned lifecycle coordinator, remove the resume handler from
|
||||||
|
`ModuleReplicatorCouchDB`, and route CouchDB Continuous and OneShot Sync plus
|
||||||
|
Object Storage `syncOnStart` through `ReplicationService`. Migrate every
|
||||||
|
automatic caller to the unattended entry point in this stage, so periodic and
|
||||||
|
event calls cannot fall back to an interactive P2P role. Migrate manual commands
|
||||||
|
to the user-initiated entry point.
|
||||||
|
|
||||||
|
Replace the factory-registration-only responsibilities of
|
||||||
|
`ModuleReplicatorCouchDB` and `ModuleReplicatorMinIO` with composed provider
|
||||||
|
definitions. Retain a module only for separately identified stateful
|
||||||
|
behaviour; do not retain an instance merely to add a construction handler.
|
||||||
|
|
||||||
|
At this boundary, existing P2P AutoSync, AutoWatch, and incoming-request
|
||||||
|
entry points receive the same non-interactive readiness and accepted-peer gate.
|
||||||
|
The no-interaction authority reaches counterpart RPC authorisation and
|
||||||
|
broadcast progress notifications. An unknown peer is blocked rather than
|
||||||
|
prompting.
|
||||||
|
|
||||||
|
Until Stage 4 supplies target-aware unattended P2P, each host composition
|
||||||
|
declares generic `P2P_SyncOnReplication` as `not-implemented`. Its automatic
|
||||||
|
request settles without UI with an explicit blocked result. Existing AutoSync,
|
||||||
|
AutoWatch, and accepted incoming-request paths continue with the Stage 2 gate.
|
||||||
|
This is a temporary migration state, not the target matrix in Part 1.
|
||||||
|
|
||||||
|
Apply and test the CLI scheduling precedence defined in Part 1, so the daemon
|
||||||
|
and lifecycle coordinator cannot schedule duplicate initial or recurring work.
|
||||||
|
|
||||||
|
## Stage 3: make P2P transport ownership truthful
|
||||||
|
|
||||||
|
Introduce the stable P2P service and its narrow contract views around the
|
||||||
|
existing implementation while preserving transfer semantics and changing the
|
||||||
|
necessary ownership and lifecycle behaviour. Make the active P2P Replicator a
|
||||||
|
non-owning adapter. Give the service exclusive ownership of room sessions,
|
||||||
|
session-epoch fencing, effective session binding, room open/close/replacement,
|
||||||
|
and settings reconciliation. Consolidate overlapping resume and
|
||||||
|
settings-event handlers.
|
||||||
|
|
||||||
|
Migrate Obsidian panes and commands to lifecycle, peer, admission, transfer,
|
||||||
|
change-relay, configuration-exchange, and diagnostic views as required. Migrate
|
||||||
|
CLI and WebPeer away from concrete-class checks and raw host or room access.
|
||||||
|
Preserve RTC diagnostics through `P2PDiagnostics`, rather than retaining
|
||||||
|
`rawHost`. The compatibility facade may delegate during this stage, but new
|
||||||
|
consumers cannot receive it.
|
||||||
|
|
||||||
|
Separate active-adapter release, room-session leave, and the stop request. Keep
|
||||||
|
the P2P stop role `not-implemented` until a lower-level operation exists. Add
|
||||||
|
internal session-demand ownership for finite operations and policy-held
|
||||||
|
AutoStart without exposing that bookkeeping as a general consumer API.
|
||||||
|
|
||||||
|
Add ownership regressions immediately before implementation:
|
||||||
|
|
||||||
|
- replacing or disposing the active main adapter does not close a policy-owned
|
||||||
|
or adjunct room;
|
||||||
|
- a disposed session fences late callbacks and clients;
|
||||||
|
- a P2P setting change replaces an adjunct room while another provider remains
|
||||||
|
the active main remote;
|
||||||
|
- persisted peer decisions survive replacement, while temporary decisions and
|
||||||
|
advertisements do not;
|
||||||
|
- local database replacement retires database-bound feeds and publication;
|
||||||
|
- repeated replacement does not retain platform-event subscriptions;
|
||||||
|
- policy-only change waits for an already-started finite transfer to settle;
|
||||||
|
- explicit disconnect suppresses AutoStart and relay reconnection until
|
||||||
|
explicit connect; and
|
||||||
|
- database replacement fences both active-provider and P2P work before
|
||||||
|
publishing the new database identity, while a failed candidate leaves one
|
||||||
|
observable disconnected state without reviving the fenced session.
|
||||||
|
|
||||||
|
When this stage lands, add a supersession note to the accepted P2P lifecycle
|
||||||
|
record and update `devs.md` from the replaceable concrete Replicator getter to
|
||||||
|
the stable contract views. Preserve the accepted Trystero peer and relay
|
||||||
|
ownership rules rather than rewriting their historical verification.
|
||||||
|
|
||||||
|
## Stage 4: add target-aware unattended P2P orchestration
|
||||||
|
|
||||||
|
Before implementation, add failing regressions for the headless result-loss
|
||||||
|
path, delayed advertisement, an unaccepted peer, overlapping configured-target
|
||||||
|
and AutoSync requests, suspension before delayed open, and a finite-operation
|
||||||
|
demand beside policy-owned AutoStart demand.
|
||||||
|
|
||||||
|
Implement target-aware unattended P2P work without UI. Add bounded
|
||||||
|
advertisement waiting, peer-acceptance outcomes, finite-operation demands beside
|
||||||
|
policy demands, lifecycle-generation cancellation for delayed opens, and
|
||||||
|
de-duplication across AutoSync, AutoWatch, and configured-target requests.
|
||||||
|
Route P2P automation through trigger-aware readiness without central-remote
|
||||||
|
Security Seed preflight. Add the missing finite-activity boundary to direct
|
||||||
|
shared-pane synchronisation.
|
||||||
|
|
||||||
|
Keep the detailed wait, session-demand, de-duplication, and session-epoch state
|
||||||
|
machine in Part 2 rather than expanding the generic provider contract. If
|
||||||
|
implementation evidence requires a refinement, amend Part 2 before completing
|
||||||
|
this stage. After Stage 3 is complete, Part 2 supersedes the
|
||||||
|
replaceable-Replicator and current-result ownership portions of the accepted
|
||||||
|
July 2026 record; its Trystero peer and relay decisions remain unchanged.
|
||||||
|
|
||||||
|
## Stage 5: separate active construction and flow-specific probes
|
||||||
|
|
||||||
|
Make active creation a private, exhaustive `ReplicatorService` operation with
|
||||||
|
an explicit same-kind rebind-or-replace policy. Migrate every non-active caller
|
||||||
|
before restricting `getNewReplicator()`: CouchDB connection and passphrase
|
||||||
|
checks, Object Storage connection and preferred-tweak trials, isolated P2P
|
||||||
|
Setup, CLI commands, and other host compositions. Prove that every probe leaves
|
||||||
|
the active main Replicator and adjunct P2P transport unchanged.
|
||||||
|
|
||||||
|
Migrate Streaming Fetch to the owned CouchDB initial-transfer dependencies
|
||||||
|
defined in Part 1. Add replacement-fence, late-settlement,
|
||||||
|
configuration-identity, cache-invalidation, and probe-disposal tests before
|
||||||
|
making active construction private.
|
||||||
|
|
||||||
|
## Stage 6: migrate safety-sensitive and provider-specific consumers
|
||||||
|
|
||||||
|
Migrate Setup, Rebuilder, tweak review, remote-size inspection, on-demand Chunk
|
||||||
|
retrieval, migration inspection, maintenance, and abort commands. Central
|
||||||
|
mutations settle truthfully before Rebuilder continuation. Compromised-Chunk
|
||||||
|
inspection uses `observed` or `unavailable`; an offline check is never zero.
|
||||||
|
Object Storage and P2P no longer supply dummy integrity counts, remote Chunk
|
||||||
|
results, or Security Seeds through the compatibility facade.
|
||||||
|
|
||||||
|
Move local node identity initialisation out of the provider facade. Add
|
||||||
|
idempotent asynchronous disposal and settlement tests, same-kind profile
|
||||||
|
rebind-or-replace tests, and bounded activity around the host-owned garbage-
|
||||||
|
collection workflow's CouchDB OneShots. Each consumer migration removes the
|
||||||
|
corresponding generic `remoteType` or `instanceof` feature branch and adds a
|
||||||
|
focused test.
|
||||||
|
|
||||||
|
Transport-specific settings may continue to display provider kind. Provider
|
||||||
|
identity is valid for labels and provider-specific configuration; it is not a
|
||||||
|
generic feature test.
|
||||||
|
|
||||||
|
## Stage 7: retire the giant compatibility facade
|
||||||
|
|
||||||
|
After every current consumer has migrated, stop requiring unsupported methods
|
||||||
|
on `LiveSyncAbstractReplicator`. Retain or remove compatibility exports at the
|
||||||
|
next Commonlib compatibility boundary. Do not retain dummy methods only because
|
||||||
|
the former abstract base declared them.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Commonlib unit and type-contract tests
|
||||||
|
|
||||||
|
Cover:
|
||||||
|
|
||||||
|
- exhaustive host-composed definitions for CouchDB, Object Storage, and P2P;
|
||||||
|
- complete support declarations and matching runtime roles;
|
||||||
|
- user-initiated, unattended, blocked, partial, cancelled, and failed OneShot
|
||||||
|
outcomes;
|
||||||
|
- truthful Object Storage stop or transfer failure and headless P2P outcomes;
|
||||||
|
- observed and unavailable compromised-Chunk results, including offline;
|
||||||
|
- active, retiring, disposed, and replacement-published states, including
|
||||||
|
rejection of new work during retirement and late settlement;
|
||||||
|
- same-kind rebind or replacement, configuration-identity invalidation,
|
||||||
|
idempotent asynchronous disposal, and settlement ordering;
|
||||||
|
- probes which cannot replace the active Replicator or P2P service and dispose
|
||||||
|
owned resources;
|
||||||
|
- the deliberately narrow active-transfer stop request, including work it
|
||||||
|
does not claim to cancel;
|
||||||
|
- central full-transfer and remote-administration boundaries; and
|
||||||
|
- provider-specific P2P, CouchDB, and journal facets.
|
||||||
|
|
||||||
|
### Self-hosted LiveSync unit tests
|
||||||
|
|
||||||
|
Cover:
|
||||||
|
|
||||||
|
- Object Storage `syncOnStart` through resume, including a migrated profile
|
||||||
|
which retains `liveSync: true`;
|
||||||
|
- existing CouchDB Continuous and OneShot paths;
|
||||||
|
- periodic, database-save, editor-save, file-open, merge, and daemon triggers
|
||||||
|
remaining free of dialogues;
|
||||||
|
- manual P2P and configured peer-targeted flows remaining available;
|
||||||
|
- P2P AutoStart cancellation across suspension, bounded advertisement waiting,
|
||||||
|
accepted peers without unattended dialogues, remote-broadcast prerequisites,
|
||||||
|
session-demand reference counts, and overlapping peer policies;
|
||||||
|
- the seven P2P service views sharing one room owner without exposing a raw
|
||||||
|
host, room, or concrete Replicator;
|
||||||
|
- session replacement fencing callbacks, clients, temporary decisions,
|
||||||
|
advertisements, and database-bound feeds while retaining persisted decisions;
|
||||||
|
- counterpart RPC authorisation and broadcast progress preserving no-dialogue
|
||||||
|
authority;
|
||||||
|
- Setup and settings validation through probes;
|
||||||
|
- first-device and additional-device initialisation for each current provider;
|
||||||
|
- P2P as the main remote and as an adjunct transport; and
|
||||||
|
- CLI, WebApp, and WebPeer composition against the same contracts, including
|
||||||
|
daemon scheduling after settings restoration and P2P AutoStart reconciliation
|
||||||
|
after a settings change.
|
||||||
|
|
||||||
|
### Focused integration and real-Obsidian verification
|
||||||
|
|
||||||
|
Cover an Object Storage change arriving immediately after start-up without
|
||||||
|
waiting for the periodic interval, ordinary CouchDB start-up, and P2P start-up
|
||||||
|
without an unexpected selection dialogue or transport replacement. P2P
|
||||||
|
validation also covers an accepted configured peer advertising after room open,
|
||||||
|
a watched peer whose remote side broadcasts, and suspension before a delayed
|
||||||
|
AutoStart callback.
|
||||||
|
|
||||||
|
No temporary stage is a release candidate. Release readiness requires the
|
||||||
|
target capability matrix, production-consumer migration, focused downstream
|
||||||
|
checks with the exact packed Commonlib artefact, and the real-runtime checks
|
||||||
|
appropriate to the changed boundary.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Part 1: core contract](2026_08_replicator_capabilities_01_core_contract.md)
|
||||||
|
- [Part 2: P2P service and session lifecycle](2026_08_replicator_capabilities_02_p2p_service_lifecycle.md)
|
||||||
|
- [P2P Room and Transport Lifecycle](2026_07_p2p_transport_lifecycle.md)
|
||||||
|
- [P2P Transport Compatibility Controls](2026_08_p2p_transport_compatibility.md)
|
||||||
|
- [Bounded Remote Activity](2026_07_bounded_remote_activity.md)
|
||||||
|
- [Self-hosted LiveSync issue 1140](https://github.com/vrtmrz/obsidian-livesync/issues/1140)
|
||||||
Reference in New Issue
Block a user