@vrtmrz/livesync-commonlib 0.1.18 → 0.1.19-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -186,8 +186,7 @@ class ReplicationService extends ServiceBase {
186
186
  }
187
187
  return activeReplicator;
188
188
  }
189
- async replicateAllToRemote(showingNotice = false) {
190
- if (!this.appLifecycleService.isReady()) return false;
189
+ async performReplicateAllToRemote(showingNotice) {
191
190
  if (!await this.onBeforeReplicate(showingNotice)) {
192
191
  this._log(this.context.translate("Replicator.Message.SomeModuleFailed"), LOG_LEVEL_NOTICE);
193
192
  return false;
@@ -204,8 +203,7 @@ class ReplicationService extends ServiceBase {
204
203
  return await activeReplicator.replicateAllToServer(currentSettings, showingNotice);
205
204
  return !checkResult;
206
205
  }
207
- async replicateAllFromRemote(showingNotice = false) {
208
- if (!this.appLifecycleService.isReady()) return false;
206
+ async performReplicateAllFromRemote(showingNotice) {
209
207
  const activeReplicator = this.getActiveReplicatorFor("fetching data from remote");
210
208
  if (!activeReplicator) {
211
209
  return false;
@@ -218,6 +216,40 @@ class ReplicationService extends ServiceBase {
218
216
  return await activeReplicator.replicateAllFromServer(currentSettings, showingNotice);
219
217
  return !checkResult;
220
218
  }
219
+ async replicateAllToRemote(showingNotice = false) {
220
+ if (!this.appLifecycleService.isReady()) return false;
221
+ return await this.performReplicateAllToRemote(showingNotice);
222
+ }
223
+ async replicateAllFromRemote(showingNotice = false) {
224
+ if (!this.appLifecycleService.isReady()) return false;
225
+ return await this.performReplicateAllFromRemote(showingNotice);
226
+ }
227
+ /**
228
+ * Perform a full upload owned by an active rebuild while the physical database is ready.
229
+ *
230
+ * This concrete maintenance entry point is intentionally absent from `IReplicationService`;
231
+ * it is not a general application-readiness bypass.
232
+ */
233
+ async replicateAllToRemoteForRebuild(showingNotice = false) {
234
+ if (!this.databaseService.isDatabaseReady()) {
235
+ this._log("The selected local database is not ready for the rebuild upload.", LOG_LEVEL_NOTICE);
236
+ return false;
237
+ }
238
+ return await this.performReplicateAllToRemote(showingNotice);
239
+ }
240
+ /**
241
+ * Perform a full download owned by an active rebuild while the physical database is ready.
242
+ *
243
+ * This concrete maintenance entry point is intentionally absent from `IReplicationService`;
244
+ * it is not a general application-readiness bypass.
245
+ */
246
+ async replicateAllFromRemoteForRebuild(showingNotice = false) {
247
+ if (!this.databaseService.isDatabaseReady()) {
248
+ this._log("The selected local database is not ready for the rebuild download.", LOG_LEVEL_NOTICE);
249
+ return false;
250
+ }
251
+ return await this.performReplicateAllFromRemote(showingNotice);
252
+ }
221
253
  _getReplicatorAndPerform(action, perform) {
222
254
  const activeReplicator = this.getActiveReplicatorFor(action);
223
255
  if (!activeReplicator) {
@@ -39,6 +39,7 @@ export declare abstract class ReplicatorService<T extends ServiceContext = Servi
39
39
  private suspendReplication;
40
40
  private reinitialiseReplicator;
41
41
  private disposeReplicator;
42
+ private discardFailedReplicator;
42
43
  private _initialiseReplicator;
43
44
  /**
44
45
  * Close the active replication if any.
@@ -15,7 +15,10 @@ class ReplicatorService extends ServiceBase {
15
15
  super(context);
16
16
  this.dependencies = dependencies;
17
17
  this.appLifecycleService = dependencies.appLifecycleService;
18
- this._unresolvedErrorManager = new UnresolvedErrorManager(dependencies.appLifecycleService, this.context.events);
18
+ this._unresolvedErrorManager = new UnresolvedErrorManager(
19
+ dependencies.appLifecycleService,
20
+ this.context.events
21
+ );
19
22
  this.settingService = dependencies.settingService;
20
23
  this.databaseEventService = dependencies.databaseEventService;
21
24
  if (dependencies.registerLifecycleHandlers ?? true) {
@@ -83,6 +86,18 @@ class ReplicatorService extends ServiceBase {
83
86
  this._replicatorType = void 0;
84
87
  return true;
85
88
  }
89
+ async discardFailedReplicator(replicator) {
90
+ if (this._activeReplicator === replicator) {
91
+ this._activeReplicator = void 0;
92
+ this._replicatorType = void 0;
93
+ }
94
+ try {
95
+ await Promise.resolve(replicator.closeReplication());
96
+ } catch (error) {
97
+ this._log("Failed to close a replicator after its initialisation failed.", LOG_LEVEL_VERBOSE);
98
+ this._log(error, LOG_LEVEL_VERBOSE);
99
+ }
100
+ }
86
101
  async _initialiseReplicator() {
87
102
  const message = this.context.translate("Replicator.Message.InitialiseFatalError");
88
103
  const setting = this.settingService.currentSettings();
@@ -123,19 +138,23 @@ class ReplicatorService extends ServiceBase {
123
138
  this._replicatorType = replicatorType;
124
139
  this.replicationStatics.value = { ...DEFAULT_REPLICATION_STATICS };
125
140
  await yieldMicrotask();
126
- if (!await newReplicator.initializeDatabaseForReplication()) {
127
- this._log("Failed to initialise the replicator's local node information.");
128
- this._activeReplicator = void 0;
129
- this._replicatorType = void 0;
141
+ try {
142
+ if (!await newReplicator.initializeDatabaseForReplication()) {
143
+ this._log("Failed to initialise the replicator's local node information.");
144
+ await this.discardFailedReplicator(newReplicator);
145
+ this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
146
+ return false;
147
+ }
148
+ if (!await this.onReplicatorInitialised()) {
149
+ this._log("Failed to initialise the replicator, onReplicatorInitialised reported some problems.");
150
+ await this.discardFailedReplicator(newReplicator);
151
+ this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
152
+ return false;
153
+ }
154
+ } catch (error) {
155
+ await this.discardFailedReplicator(newReplicator);
130
156
  this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
131
- return false;
132
- }
133
- if (!await this.onReplicatorInitialised()) {
134
- this._log("Failed to initialise the replicator, onReplicatorInitialised reported some problems.");
135
- this._activeReplicator = void 0;
136
- this._replicatorType = void 0;
137
- this._unresolvedErrorManager.showError(message, LOG_LEVEL_NOTICE);
138
- return false;
157
+ throw error;
139
158
  }
140
159
  const remoteTypeDisplay = replicatorType === RemoteTypes.REMOTE_COUCHDB ? "CouchDB" : replicatorType;
141
160
  this._log(`Replicator (${remoteTypeDisplay}) initialised and activated`, LOG_LEVEL_VERBOSE);
@@ -69,6 +69,8 @@ The one-shot CouchDB connectivity preflight is the first bounded consumer. It ap
69
69
 
70
70
  `openSimpleStore()` may create an inert namespace handle during composition, before the backing key-value database has opened. Maintained hosts must complete their sequential settings lifecycle before scans, watchers, or replication invoke the handle. Operations fail on a lifecycle violation rather than waiting for readiness; do not introduce an implicit wait which can hang after failed initialisation or become self-referential from an initialisation handler. Treat database reset as a transient unavailable boundary and reconstruct derived local state after reopening.
71
71
 
72
+ The active local database identity, settings-selected reset sequence, and lifecycle event phases are specified in [Local database lifecycle](database-lifecycle.md). Extend that document and the focused service and rebuild tests whenever database selection, reset ownership, event ordering, or failure propagation changes.
73
+
72
74
  ### P2P composition ownership
73
75
 
74
76
  `useP2PReplicatorFeature` is the sole owner of the active `LiveSyncTrysteroReplicator` and its lifecycle bindings. It creates or replaces the outer replicator when `ReplicatorService` requests one, closes the previous instance before replacement, shares an in-flight replacement between concurrent callers, and returns a stable result object whose `replicator` property resolves the current instance.
@@ -24,7 +24,9 @@ Commonlib's injected remote-service tests verify that an owning operation can ab
24
24
 
25
25
  Commonlib's remote-preference result contract distinguishes available synchronisation settings, a remote which has not stored those settings yet, a failed read, and a remote type which does not support central preferences. Focused CouchDB and Object Storage tests cover absent milestone data separately from transport failures, while Self-hosted LiveSync owns the initialisation choices, cancellation policy, and user-facing explanation.
26
26
 
27
- Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. Before pair construction, it withholds Metadata whose actual document ID disagrees with its recorded path, without reflection, deletion, or last-seen side effects. A consistently addressed entry for the same logical path continues through the established path-based flow; only a path without such an entry also withholds storage processing. The read-only actual-ID Inspector and separately invoked one-entry repair retain ambiguous states for review, revalidate an exact source revision, verify a target-first copy, and preserve the source on failure. Self-hosted LiveSync keeps the existing Fast Setup aggregate policy and owns confirmation and presentation in its file/database repair interface.
27
+ Commonlib's full offline scanner distinguishes completed, deliberately skipped, and failed storage/database pairs. Before pair construction, it withholds Metadata whose actual document ID disagrees with its recorded path, without reflection, deletion, or last-seen side effects. A consistently addressed entry for the same logical path continues through the established path-based flow; only a path without such an entry also withholds storage processing. The read-only actual-ID Inspector and separately invoked one-entry repair retain ambiguous states for review, revalidate an exact source revision, verify a target-first copy, and preserve the source on failure. Snapshot-restoration tests additionally verify that restored inclusions use current storage content, stale deletions are suppressed, rename halves are admitted from their current path states, and an inspection failure does not become evidence of absence. The following full scan remains the final reconciliation step. Self-hosted LiveSync keeps the existing Fast Setup aggregate policy and owns confirmation and presentation in its file/database repair interface.
28
+
29
+ Commonlib's local-database lifecycle tests distinguish the settings-selected database from the active wrapper. They verify replacement before a changed-suffix reset, fail-closed cleanup after rejected physical preparation, application readiness after the requested scan and completion hooks, and the prevention of rebuilt events and later remote mutation after a failed local reset. Self-hosted LiveSync owns settings persistence, setup flags, recovery dialogue, and real-Obsidian rebuild confirmation; those host results remain downstream evidence rather than Commonlib guarantees.
28
30
 
29
31
  Most replication and storage services used by the plug-in still enter through explicit `compat/*` paths. Those imports prove that the published compatibility boundary supports the current migration; they are not examples of a finished high-level client API.
30
32
 
package/docs/releasing.md CHANGED
@@ -92,3 +92,25 @@ gh workflow run publish-npm.yml \
92
92
  For a stable release, use `source_ref=main`. The workflow rejects a stable release selected from another branch, rejects a stable release without its trusted workflow commit, and requires the workflow-triggering SHA to equal the packaged `main` SHA. The workflow dispatch itself uses `--ref main` for every release, so an unmerged branch cannot change the trusted publication job or acquire access to the `npm` environment.
93
93
 
94
94
  The workflow always stages to `next`. Inspect the staged package name, version, access, dist-tag, provenance, checksum, files, selected source branch, and source commit before approving it through npm. Approval and later promotion to `latest` are separate user-authorised operations. Keep the Commonlib and Self-hosted LiveSync consumer pull requests in draft while validating the exact registry pre-release. If validation fails, leave that published version immutable and prepare a new pre-release. After successful consumer validation, merge Commonlib only through its separate maintainer gate. Validate a stable release in Self-hosted LiveSync before promoting it to `latest`.
95
+
96
+ ## Publishing a GitHub Release
97
+
98
+ Create a GitHub Release for each stable Commonlib release after the exact registry artefact has passed downstream validation and the version has been promoted to `latest`. Target the exact `main` commit used to build the published package.
99
+
100
+ Protect the GitHub `github-release` environment with a required reviewer and permit only `main`. The manually dispatched workflow first verifies the release with read-only repository access. Only its protected publication job receives `contents: write` access.
101
+
102
+ Set the exact version and release commit, then dispatch the workflow from `main`:
103
+
104
+ ```bash
105
+ version='<version>'
106
+ release_sha='<full-release-commit>'
107
+ gh workflow run publish-github-release.yml \
108
+ --ref main \
109
+ -f version="$version" \
110
+ -f expected_sha="$release_sha" \
111
+ -f confirmation="release @vrtmrz/livesync-commonlib@$version from $release_sha"
112
+ ```
113
+
114
+ The workflow requires the release commit to be an ancestor of its trusted `main` commit, reads the exact source version and corresponding release notes from that commit, confirms that npm `latest` points to the version, and rejects an existing version tag. Review those results before approving the `github-release` environment. The protected job rechecks npm `latest`, then creates the version tag and GitHub Release and verifies that the tag points to the selected commit.
115
+
116
+ Treat dispatching and approving the GitHub Release workflow as separate user-authorised remote operations. Verify the version, exact release commit, and release notes immediately before approval.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.18",
3
+ "version": "0.1.19-rc.0",
4
4
  "description": "Shared data, storage, and replication primitives for Self-hosted LiveSync clients.",
5
5
  "type": "module",
6
6
  "types": "./dist/index.d.ts",