mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-22 17:13:20 +00:00
Refactor optional file synchronisation ownership
This commit is contained in:
1 parent
3b2d5aa5af
commit
826413bf84
67 files changed
+6294
-1856
No files matched your search
@@ -0,0 +1,454 @@
|
||||
---
|
||||
date: 2026-09-03
|
||||
commonlib-version: "0.1.21"
|
||||
self-hosted-livesync-version: "1.0.24"
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# Architectural Decision Record: Customisation Sync and Hidden File Sync ownership
|
||||
|
||||
## Status
|
||||
|
||||
Accepted and implemented. The completed structural stages
|
||||
extract the Customisation Sync path and codec operations, characterise the
|
||||
current shared routing behaviour, remove the production UI dependency cycle,
|
||||
move host presentation and Hidden File Sync commands into serviceFeatures,
|
||||
give one optional-file serviceFeature ownership of both runtime contexts and
|
||||
all Service handler registration, and replace both runtimes' complete-core
|
||||
dependencies with narrow host adapters. A pure local-path policy now selects
|
||||
one writer for raw events, scheduled scans, and automatic database-to-local
|
||||
reflection. The constructor-name add-on lookup and the `ConfigSync` and
|
||||
`HiddenFileSync` add-on identities have been retired.
|
||||
|
||||
The Customisation Sync private context owns its catalogue, progress, manifest,
|
||||
queue, and periodic state. The Hidden File Sync private context owns its
|
||||
processed-state caches, reconciliation and conflict queues, activity counts,
|
||||
and periodic state. Each receives live settings and database projections,
|
||||
focused storage, path, and exact-revision capabilities, and explicit host
|
||||
effects rather than `LiveSyncCore`.
|
||||
|
||||
The corresponding implemented topology is documented in
|
||||
[Optional-file synchronisation architecture](../design_docs/optional_file_sync_architecture.md).
|
||||
|
||||
## Context
|
||||
|
||||
Customisation Sync and Hidden File Sync are supported, advanced, opt-in
|
||||
features with different data and interaction contracts:
|
||||
|
||||
- Customisation Sync stores device-scoped snapshots in the `ix:` namespace.
|
||||
It supports grouped V1 documents and per-file V2 documents, presents a
|
||||
catalogue, and applies selected remote state only after an explicit action.
|
||||
- Hidden File Sync stores one mirrored hidden file in each `i:` Metadata
|
||||
document. It scans and reconciles local and database state, applies eligible
|
||||
remote changes automatically, and maintains device-local processed-state
|
||||
records for offline and conflict handling.
|
||||
|
||||
In the baseline implementation, the `ConfigSync` and `HiddenFileSync` add-ons
|
||||
each combined domain operations, mutable state, Service handler registration,
|
||||
lifecycle work, settings policy, commands, and Obsidian presentation. Both
|
||||
received the complete core through `LiveSyncCommands`, and both registered
|
||||
handlers for optional file events, conflicts, settings, application lifecycle,
|
||||
database initialisation, and replication.
|
||||
|
||||
Some of those Commonlib handlers short-circuit at the first successful or
|
||||
non-empty result. The construction order of `ConfigSync` before
|
||||
`HiddenFileSync` is therefore observable behaviour. The Obsidian storage-event
|
||||
manager also asks the Vault Service whether a configuration-directory file is
|
||||
an extra target, while the corresponding predicate is currently registered by
|
||||
`HiddenFileSync`. Local file ownership is consequently distributed across the
|
||||
storage-event manager, both add-ons, settings entries, target and ignore
|
||||
patterns, and handler registration order.
|
||||
|
||||
In the baseline implementation, user-interface, maintenance, and
|
||||
real-Obsidian E2E consumers located the concrete add-ons through `getAddOn()`.
|
||||
Removing either add-on before migrating those consumers would therefore have
|
||||
combined an ownership change, a lifecycle change, and a consumer migration in
|
||||
one step.
|
||||
|
||||
The baseline Customisation Sync presentation also formed a runtime import
|
||||
cycle: `ConfigSync` imported its Obsidian dialogue, the dialogue imported its
|
||||
Svelte pane, and the pane imported `ConfigSync` and `HiddenFileSync` to locate
|
||||
both add-ons. The child row component imported `ConfigSync` again. The settings
|
||||
tab also imported `main.ts` as a runtime binding even though it used the
|
||||
plug-in class only as a type. Bundling survived because those bindings were
|
||||
read after module evaluation, but that timing was not an architectural
|
||||
contract. Presentation therefore had to move off the concrete add-ons before
|
||||
the domain runtimes could be extracted.
|
||||
|
||||
## Decision drivers
|
||||
|
||||
The redesign must:
|
||||
|
||||
1. preserve `ix:` and `i:` data, including V1 and V2 Customisation Sync data;
|
||||
2. give each local path at most one synchronisation owner;
|
||||
3. remove handler registration order as the source of path ownership;
|
||||
4. preserve explicit Customisation Sync application and automatic Hidden File
|
||||
Sync reflection as separate behaviours;
|
||||
5. make mutable state and resource disposal ownership explicit;
|
||||
6. keep Obsidian presentation outside host-neutral operations;
|
||||
7. migrate existing add-on consumers to focused views before retiring add-on
|
||||
identity; and
|
||||
8. permit each migration stage to be verified independently.
|
||||
|
||||
## Decision
|
||||
|
||||
### Retain two domain runtimes
|
||||
|
||||
Customisation Sync and Hidden File Sync will remain separate domain runtimes.
|
||||
They will not be implemented as modes of one generic file-sync engine.
|
||||
|
||||
The Customisation Sync runtime will own:
|
||||
|
||||
- V1 and V2 codecs and document-path compatibility;
|
||||
- the device catalogue and snapshot repository;
|
||||
- scanning, migration, logical deletion, comparison, and explicit apply
|
||||
operations; and
|
||||
- the state required to serialise and report those operations.
|
||||
|
||||
The Hidden File Sync runtime will own:
|
||||
|
||||
- the device-local processed-state records;
|
||||
- one-file storage and database transfer, including exact-revision repair;
|
||||
- start-up, offline, periodic, and pre-replication reconciliation;
|
||||
- conflict queues and automatic or interactive JSON resolution; and
|
||||
- the state required to serialise and report those operations.
|
||||
|
||||
Neither runtime will accept `LiveSyncBaseCore` as its domain dependency.
|
||||
Operations will receive narrow Services, ServiceModules, settings projections,
|
||||
and host effects.
|
||||
|
||||
### Compose one owner for overlapping handlers
|
||||
|
||||
One joint serviceFeature constructs both private runtime contexts and owns
|
||||
registration into the overlapping optional-file, conflict, target, settings,
|
||||
database, replication, and application-lifecycle handlers. A single
|
||||
optional-file callback selects its local owner before invoking either context,
|
||||
and a single conflict callback dispatches the disjoint `ps:`, `ix:`, and `i:`
|
||||
namespaces. Callback order and add-on construction order no longer select the
|
||||
local file owner. Config-before-Hidden order remains explicit only for shared
|
||||
lifecycle handlers whose side effects still require compatibility.
|
||||
|
||||
The composition feature will remain small. Codecs, path functions,
|
||||
repositories, state transitions, and transfer operations are ordinary modules
|
||||
or private contexts rather than separate serviceFeatures. No new
|
||||
ServiceModule is justified while those capabilities have one composition
|
||||
owner and no independent long-lived consumers.
|
||||
|
||||
Internally, operations will use a typed settlement which distinguishes at
|
||||
least handled, skipped, and failed work. The composition boundary will adapt
|
||||
that settlement to the existing Commonlib Boolean or first-result handler
|
||||
contracts. Existing short-circuit and failure behaviour must be characterised
|
||||
before an adapter changes it.
|
||||
|
||||
### Make local path ownership explicit
|
||||
|
||||
A pure routing policy classifies a local path as owned by Customisation
|
||||
Sync, owned by Hidden File Sync, or ignored with a reason. Its inputs will
|
||||
include:
|
||||
|
||||
- the Obsidian configuration directory;
|
||||
- whether each feature is enabled and ready;
|
||||
- the Customisation Sync category and extended mode;
|
||||
- Hidden File Sync target and ignore patterns; and
|
||||
- the ignore-file result where that asynchronous policy applies.
|
||||
|
||||
The maintained ownership intent is:
|
||||
|
||||
| Path and mode | Local owner |
|
||||
| ------------------------------------------------------------ | ------------------ |
|
||||
| Recognised Customisation Sync path in Selective mode | Customisation Sync |
|
||||
| Recognised Customisation Sync path in Flagged Selective mode | Customisation Sync |
|
||||
| Recognised Customisation Sync path in Automatic mode | Hidden File Sync |
|
||||
| Recognised Customisation Sync path in Ignore mode | Neither feature |
|
||||
| Other eligible hidden path | Hidden File Sync |
|
||||
| Excluded, ignored, disabled, or ordinary Vault path | Neither feature |
|
||||
|
||||
This table is now used for raw-event dispatch and is injected into both
|
||||
contexts for scheduled or database-driven local work. Default and persisted
|
||||
Selective entries therefore have the same Customisation Sync owner. Hidden
|
||||
File Sync target patterns and ignore-file results are evaluated only after the
|
||||
policy selects Hidden File Sync, so they cannot prevent a Selective
|
||||
Customisation Sync event. Automatic mode has no local owner while Hidden File
|
||||
Sync is disabled, and Ignore mode has no local owner in either context.
|
||||
|
||||
Commonlib's ordinary event queue rejects dot paths while Hidden File Sync is
|
||||
disabled. The Obsidian raw-event boundary therefore dispatches an event which
|
||||
the policy assigns to Customisation Sync directly after retaining the queue's
|
||||
configuration, suspension, and modification-time gates. Events handled while
|
||||
Hidden File Sync is enabled continue through the ordinary queue.
|
||||
|
||||
This is a local-write routing table, not a database-document acceptance table.
|
||||
Existing `ix:` documents must remain recognisable after a local mode change,
|
||||
and they must not fall through to ordinary Vault reflection. Existing `i:`
|
||||
documents retain their own namespace and selection rules. The implementation
|
||||
therefore keeps namespace-specific database handlers separate from the
|
||||
local-path policy rather than exposing one general `isTargetPath()` predicate.
|
||||
A narrower document decision controls whether a local Customisation Sync scan
|
||||
may mutate an existing `ix:` document; it does not control whether that
|
||||
document is recognised or consumed.
|
||||
|
||||
### Use private contexts and focused views
|
||||
|
||||
Each mutable object will have one named owner. Catalogue stores, manifest
|
||||
caches, processed-state records, recent-event records, locks, semaphores,
|
||||
queues, periodic processors, Notices, and event subscriptions must be created
|
||||
and disposed by their feature context or a focused resource owner.
|
||||
|
||||
The joint composition may return several views backed by those contexts:
|
||||
|
||||
- a Customisation Sync catalogue and operation view for its dialogue;
|
||||
- a Hidden File Sync initialisation view for settings workflows; and
|
||||
- a Hidden File Sync repair view for the Hatch pane.
|
||||
|
||||
Several views over one context do not create several owners. Views expose
|
||||
stable application data and operations rather than PouchDB entries, queue
|
||||
objects, mutable settings records, or the complete core.
|
||||
|
||||
Obsidian commands, ribbon actions, dialogues, Notices, plug-in reloads, and
|
||||
restart scheduling will remain in host-owned composition. The UI will receive
|
||||
focused views instead of locating `ConfigSync` or `HiddenFileSync` and calling
|
||||
one add-on from the other.
|
||||
|
||||
The Customisation Sync dialogue has its own host-owned presentation
|
||||
serviceFeature. It owns command, ribbon, event subscription, dialogue reuse,
|
||||
and dialogue disposal, and consumes the catalogue and Hidden File Sync
|
||||
initialisation views. Neither domain runtime imports its Obsidian dialogue or
|
||||
Svelte components. This presentation feature is separate from the joint
|
||||
synchronisation owner because it registers host UI rather than overlapping
|
||||
file, conflict, or replication handlers.
|
||||
|
||||
Hidden File Sync commands and their setting-change event subscription are
|
||||
owned by a second host serviceFeature. It consumes only the command view and
|
||||
releases its lifecycle and event registrations during application unload.
|
||||
|
||||
### Retire compatibility façades after consumer migration
|
||||
|
||||
The concrete `ConfigSync` and `HiddenFileSync` add-on identities were retained
|
||||
only until their consumers and handler ownership had migrated. They have now
|
||||
been replaced by private `CustomisationSyncContext` and
|
||||
`HiddenFileSyncContext` instances constructed solely by the joint
|
||||
serviceFeature. `LiveSyncBaseCore.getAddOn()` and its constructor-name lookup
|
||||
have been removed.
|
||||
|
||||
Production code consumes focused views and does not use the broad context
|
||||
surface. Maintained real-Obsidian E2E workflows use an explicitly internal
|
||||
test view exposed by the composed feature. This is a transitional test seam,
|
||||
not a production service locator, and it should be narrowed as those workflows
|
||||
move to public operations or commands.
|
||||
|
||||
The retirement was gated on:
|
||||
|
||||
- production UI and maintenance consumers no longer use `getAddOn()` for these
|
||||
features;
|
||||
- maintained E2E helpers use commands or an explicit test view;
|
||||
- no handler depending on add-on construction order; and
|
||||
- unload and replacement tests prove that every owned processor,
|
||||
subscription, queue, dialogue, and Notice is released.
|
||||
|
||||
## Persisted compatibility
|
||||
|
||||
This decision does not authorise a data migration.
|
||||
|
||||
- `ix:` remains the Customisation Sync Metadata namespace.
|
||||
- `i:` remains the Hidden File Sync Metadata namespace.
|
||||
- Metadata continues to reference Chunks rather than embedding raw file
|
||||
content as an ordinary Metadata field.
|
||||
- V1 grouped Customisation Sync documents remain readable.
|
||||
- V2 per-file Customisation Sync paths and the existing V1-to-V2 migration
|
||||
remain readable and idempotent.
|
||||
- Device and Vault terms, logical deletions, revision identifiers, and current
|
||||
path derivation remain compatibility inputs.
|
||||
- Hidden File Sync processed-state keys remain device-local state unless a
|
||||
separately tested migration is introduced.
|
||||
|
||||
Pure extraction must preserve exact case, depth, prefix, delimiter, and
|
||||
fallback behaviour even where a later correction appears desirable.
|
||||
|
||||
## Lifecycle and failure boundaries
|
||||
|
||||
Feature composition will occur after required Services and ServiceModules
|
||||
exist and before lifecycle-driven work begins. Command and Obsidian UI
|
||||
registration will occur at the readiness point required by the host.
|
||||
|
||||
The owner will:
|
||||
|
||||
- start periodic work only while its feature is enabled, ready, and resumed;
|
||||
- coalesce or serialise work within the feature instance rather than through
|
||||
process-global string keys where practical;
|
||||
- stop admission before disposing queues or processors;
|
||||
- unsubscribe every registered local event listener; and
|
||||
- close owned dialogues and Notices during unload.
|
||||
|
||||
A failed operation must not be converted to handled success merely to stop a
|
||||
later handler, unless a characterisation test proves that the existing
|
||||
contract deliberately consumes that failure. Such compatibility adaptations
|
||||
must be visible at the composition boundary.
|
||||
|
||||
## Migration and verification sequence
|
||||
|
||||
### Stage 1: characterise pure and routing contracts — implemented
|
||||
|
||||
- Cover current Customisation Sync category and V1/V2 document-path rules.
|
||||
- Cover codec round trips, legacy JSON and YAML fallbacks, and migration
|
||||
sentinels before extracting the codec.
|
||||
- Record the effective Selective, Automatic, Ignore, and Flagged Selective
|
||||
routing matrix, including feature-disabled and pattern-excluded cases.
|
||||
- Record handler registration, short-circuiting, and teardown behaviour.
|
||||
|
||||
### Stage 2: extract pure operations and the presentation boundary — implemented
|
||||
|
||||
- Move Customisation Sync path and document-key functions behind the existing
|
||||
façade methods.
|
||||
- Move the V1/V2 codec with its hash and YAML dependencies made explicit.
|
||||
- Inject catalogue, initialisation, and repair views into production UI.
|
||||
- Move Customisation Sync command, ribbon, event subscription, and dialogue
|
||||
lifetime into a host-owned presentation serviceFeature.
|
||||
- Prohibit presentation imports of either concrete add-on or the application
|
||||
core, and prohibit the runtime from importing its dialogue.
|
||||
|
||||
### Stage 3: make routing ownership explicit — implemented
|
||||
|
||||
- Introduce the pure routing policy, initially adapting both runtime contexts
|
||||
to it at the joint composition boundary.
|
||||
- Preserve the characterised legacy routing until each discrepancy has its own
|
||||
behavioural decision and regression test.
|
||||
|
||||
One optional-file handler now dispatches exactly one selected context, and one
|
||||
namespace router handles optional conflicts. Both contexts receive the same
|
||||
static ownership projections for raw events and scans. The final raw-event
|
||||
decision additionally includes lifecycle readiness, Hidden File Sync patterns,
|
||||
and the asynchronous ignore-file result. Handler failure does not fall through
|
||||
to the non-owner.
|
||||
|
||||
### Stage 4: extract the Customisation Sync runtime — implemented
|
||||
|
||||
- Move catalogue, snapshot repository, scan, apply, compare, delete, and
|
||||
migration operations into a private context.
|
||||
- Replace module-global mutable state with context-owned state.
|
||||
- Replace the complete-core dependency with narrow dependencies.
|
||||
|
||||
The private context, path module, codec module, focused presentation view, and
|
||||
resource teardown are implemented. Catalogue, enumeration, migration, scan,
|
||||
and manifest state is owned per context instance. The context accepts only
|
||||
narrow, live projections and explicit effects; an Obsidian adapter at the
|
||||
composition edge owns dialogues, Notices, plug-in reload, restart, lifecycle,
|
||||
Vault access, and compatibility scan telemetry.
|
||||
|
||||
### Stage 5: extract the Hidden File Sync runtime — implemented
|
||||
|
||||
- Move processed-state, transfer, reconciliation, conflict, and notification
|
||||
operations into explicit owners.
|
||||
- Preserve exact-revision repair and current initialisation directions.
|
||||
- Replace the complete-core dependency with narrow dependencies.
|
||||
|
||||
The private context, focused initialisation, repair, and command views,
|
||||
host-owned command registration, and processor, cache, subscription, and
|
||||
Notice teardown are implemented. The context owns transfer, reconciliation,
|
||||
conflict, and processed-state behaviour through narrow live dependencies.
|
||||
An Obsidian adapter owns JSON conflict dialogues, progress presentation,
|
||||
grouped Notices, plug-in reload, restart scheduling, Vault enumeration, and
|
||||
compatibility activity publication.
|
||||
|
||||
### Stage 6: move synchronisation composition — implemented
|
||||
|
||||
- Register the overlapping Service handlers once through the joint
|
||||
serviceFeature.
|
||||
- Preserve the characterised lifecycle callback order and Commonlib
|
||||
aggregation semantics through focused tests.
|
||||
|
||||
### Stage 7: retire the façades — implemented
|
||||
|
||||
- Remove add-on identity and constructor-order dependencies.
|
||||
- Migrate maintained E2E workflows to the explicit feature test boundary.
|
||||
- Remove constructor-name service lookup from the core.
|
||||
|
||||
The implemented topology is documented separately from this migration record
|
||||
in [Optional-file synchronisation architecture](../design_docs/optional_file_sync_architecture.md).
|
||||
|
||||
Each stage will run focused unit tests. Changes to Customisation Sync and
|
||||
Hidden File Sync will run their respective real-Obsidian E2E workflows. The
|
||||
composition switch will additionally require a mixed-ownership workflow which
|
||||
proves that one local path is never written by both features.
|
||||
|
||||
## Non-goals
|
||||
|
||||
This migration does not:
|
||||
|
||||
- merge the two persisted namespaces or sync models;
|
||||
- move implementation into Commonlib before another maintained host needs the
|
||||
capability;
|
||||
- change feature maturity, default enablement, setup, or initialisation;
|
||||
- replace current conflict policy with a generic conflict engine;
|
||||
- rename existing setting keys, command identifiers, or user-interface labels;
|
||||
or
|
||||
- correct unrelated suspicious behaviour while extracting code.
|
||||
|
||||
## Characterisation gates
|
||||
|
||||
Stage 3 resolved the routing-specific gates with focused regressions:
|
||||
|
||||
- an unrecognised eligible hidden path is owned by Hidden File Sync;
|
||||
- default and persisted Selective entries are owned by Customisation Sync;
|
||||
- raw Customisation Sync remains available while Hidden File Sync is disabled;
|
||||
- Hidden File Sync patterns and ignore-file results gate only its selected
|
||||
paths;
|
||||
- Automatic mode without an enabled and ready Hidden File Sync owner is
|
||||
ignored rather than falling back to Customisation Sync;
|
||||
- a selected handler which skips or fails does not fall through to the other
|
||||
context; and
|
||||
- Customisation Sync raw admission now invokes the readiness predicate and
|
||||
rejects unrecognised or non-owned paths.
|
||||
|
||||
The extraction preserves the existing V1 and V2 plug-in application paths,
|
||||
exact-revision repair, initial cache conditions, and the selected Hidden File
|
||||
Sync database-processing settlement. These remain compatibility gates for any
|
||||
future behavioural change. Any defect correction requires its own failing
|
||||
regression.
|
||||
|
||||
## Alternatives rejected
|
||||
|
||||
### Convert each extracted file into a serviceFeature
|
||||
|
||||
This would reproduce distributed registration and lifetime ownership under
|
||||
more function names. Pure operations and one private context are the narrower
|
||||
boundary.
|
||||
|
||||
### Merge both features into one generic hidden-file engine
|
||||
|
||||
This would obscure the explicit-apply snapshot contract, the automatic mirror
|
||||
contract, and their incompatible persistence and conflict semantics.
|
||||
|
||||
### Remove both add-ons before characterisation and consumer migration
|
||||
|
||||
This would change identity, ordering, lifecycle, UI, maintenance, and E2E
|
||||
boundaries simultaneously. Retaining identity through the earlier migration
|
||||
stages gave each structural change a smaller failure surface.
|
||||
|
||||
### Move the runtimes to Commonlib first
|
||||
|
||||
There is no second maintained consumer for the complete feature behaviour at
|
||||
present. Moving the monoliths across the package boundary would enlarge the
|
||||
migration without first establishing narrow dependencies.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Local path ownership can become explicit at one composition boundary.
|
||||
- Persisted formats and supported user workflows remain stable during the
|
||||
migration.
|
||||
- The composition root gains several focused views but does not gain another
|
||||
runtime service locator.
|
||||
- The legacy add-on identity and constructor-name lookup are removed.
|
||||
- The two contexts remain sizeable because each owns one cohesive persisted
|
||||
synchronisation model, but their dependency surfaces are explicit and do
|
||||
not include the complete core. Further extraction should follow a concrete
|
||||
behavioural boundary rather than create additional serviceFeatures for
|
||||
private operations.
|
||||
|
||||
## References
|
||||
|
||||
- [Feature maturity for 1.0](2026_07_feature_maturity_for_1_0.md)
|
||||
- [Service feature and legacy Module boundaries](../design_docs/service_feature_and_legacy_module_boundaries.md)
|
||||
- [Optional-file synchronisation architecture](../design_docs/optional_file_sync_architecture.md)
|
||||
- [Hidden File Sync guide](../tips/hidden-file-sync.md)
|
||||
- [Settings reference](../settings.md#6-customisation-sync-advanced)
|
||||
- [Development guide](../../devs.md#service-composition-and-legacy-modules)
|
||||
Reference in new issue
Block a user