@vrtmrz/livesync-commonlib 0.1.20 → 0.1.21

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.
@@ -68,9 +68,22 @@ export interface UserInitiatedReplicationAuthority {
68
68
  export type ReplicationInteraction = NoInteraction | UserInitiatedReplicationAuthority;
69
69
  /** Authority carried through a replication request and its failure path. */
70
70
  export type InteractionAuthority = ReplicationInteraction;
71
- /** A request made by an explicitly user-initiated one-shot command. */
71
+ /** Stable host presentation choices for routine OneShot progress. */
72
+ export declare const REPLICATION_PROGRESS_PRESENTATIONS: Readonly<{
73
+ readonly QUIET: "quiet";
74
+ readonly NOTICE: "notice";
75
+ }>;
76
+ export type ReplicationProgressPresentation = (typeof REPLICATION_PROGRESS_PRESENTATIONS)[keyof typeof REPLICATION_PROGRESS_PRESENTATIONS];
77
+ /**
78
+ * A request made by an explicitly user-initiated one-shot command.
79
+ *
80
+ * Progress presentation is independent of interaction authority: a quiet
81
+ * command may still open an authorised peer-selection or failure-recovery
82
+ * dialogue when the operation requires a decision.
83
+ */
72
84
  export interface UserInitiatedOneShotRequest {
73
85
  readonly trigger: "manual";
86
+ readonly progressPresentation: ReplicationProgressPresentation;
74
87
  readonly interaction: InteractionAuthority;
75
88
  }
76
89
  /** A request made by an automatic finite replication trigger. */
@@ -193,9 +206,9 @@ export interface ReplicationAttemptFailure {
193
206
  readonly setting: ObsidianLiveSyncSettings;
194
207
  readonly outcome: ReplicationFailed;
195
208
  }
196
- /** Immutable failure source plus the interaction authority of its original request. */
209
+ /** Immutable failure source plus presentation and interaction policy from its original request. */
197
210
  export interface ReplicationFailureRequest extends ReplicationAttemptFailure {
198
- readonly showMessage: boolean;
211
+ readonly progressPresentation: ReplicationProgressPresentation;
199
212
  readonly interaction: InteractionAuthority;
200
213
  }
201
214
  /** Return whether one detached setting still belongs to the admitted publication. */
@@ -16,6 +16,10 @@ const USER_INITIATED_REPLICATION_AUTHORITY = Object.freeze({
16
16
  failureRecovery: true
17
17
  })
18
18
  });
19
+ const REPLICATION_PROGRESS_PRESENTATIONS = Object.freeze({
20
+ QUIET: "quiet",
21
+ NOTICE: "notice"
22
+ });
19
23
  const REPLICATION_COMPLETED = Object.freeze({ status: "completed" });
20
24
  const REPLICATION_CANCELLED = Object.freeze({ status: "cancelled" });
21
25
  const CENTRAL_REMOTE_REPLICATION_READINESS = Object.freeze({
@@ -74,7 +78,7 @@ function supportedOpenReplicationOneShot() {
74
78
  const result = await replicator.openReplication(
75
79
  setting,
76
80
  false,
77
- request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery,
81
+ request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
78
82
  false
79
83
  );
80
84
  return outcomeFromFiniteOpenReplication(result);
@@ -129,6 +133,7 @@ export {
129
133
  PEER_REPLICATION_READINESS,
130
134
  REPLICATION_CANCELLED,
131
135
  REPLICATION_COMPLETED,
136
+ REPLICATION_PROGRESS_PRESENTATIONS,
132
137
  USER_INITIATED_REPLICATION_AUTHORITY,
133
138
  defineReplicatorProviderDefinitions,
134
139
  isActiveReplicatorContextBoundToSetting,
@@ -1,6 +1,6 @@
1
- export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
1
+ export { CAPABILITY_NOT_APPLICABLE, CAPABILITY_NOT_IMPLEMENTED, CENTRAL_REMOTE_REPLICATION_READINESS, NO_INTERACTION, PEER_REPLICATION_READINESS, REPLICATION_CANCELLED, REPLICATION_COMPLETED, REPLICATION_PROGRESS_PRESENTATIONS, USER_INITIATED_REPLICATION_AUTHORITY, defineReplicatorProviderDefinitions, isReplicationCompleted, outcomeFromContinuousOpenReplication, outcomeFromFiniteOpenReplication, replicationBlocked, replicationFailed, supportedOpenReplicationContinuous, supportedOpenReplicationOneShot, supportedOpenReplicationUnattended, supportedStopActiveTransfer, supportedCapability, } from "./ReplicatorProvider.js";
2
2
  export type { ReplicatorInstance } from "./ReplicatorInstance.js";
3
- export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
3
+ export type { ActiveReplicatorContext, CapabilitySupport, ContinuousReplicationRequest, ContinuousReplicationTrigger, ContinuousRunner, InteractionAuthority, InteractionPermissions, NotApplicableCapability, NotImplementedCapability, NoInteraction, PermittedInteractionPermissions, ReplicationBlockReason, ReplicationAttemptFailure, ReplicationCancelled, ReplicationCompleted, ReplicationFailed, ReplicationInteraction, ReplicationOutcome, ReplicationProgressPresentation, ReplicationReadinessRequirements, ReplicationPartial, ReplicationFailureRequest, ReplicatorConfigurationIdentity, ReplicatorProviderDefinition, ReplicatorProviderDefinitionMap, StopActiveTransferRunner, SupportedCapability, UnattendedOneShotRequest, UnattendedReplicationTrigger, UnattendedOneShotRunner, UserInitiatedOneShotRequest, UserInitiatedReplicationAuthority, UserInitiatedOneShotRunner, } from "./ReplicatorProvider.js";
4
4
  export { CAPABILITY_SUPPORT_KINDS, CAPABILITY_UNAVAILABLE_REASONS } from "./ProviderCapability.js";
5
5
  export { NO_REMOTE_RESOURCE_CAPABILITIES, REMOTE_RESOURCE_KINDS } from "./RemoteResource.js";
6
6
  export type { ConnectionProbeFactory, PreferredTweakProbe, PreferredTweakProbeFactory, RemoteConnectionProbe, RemoteConnectionProbeOptions, RemoteConnectionProbeResult, RemoteResourceCapabilities, RemoteResourceFactoryMap, RemoteResourceKind, RemoteResourceMap, SecuritySeedResource, SecuritySeedResourceFactory, SynchronisationInformationResource, SynchronisationInformationResourceFactory, } from "./RemoteResource.js";
@@ -6,6 +6,7 @@ import {
6
6
  PEER_REPLICATION_READINESS,
7
7
  REPLICATION_CANCELLED,
8
8
  REPLICATION_COMPLETED,
9
+ REPLICATION_PROGRESS_PRESENTATIONS,
9
10
  USER_INITIATED_REPLICATION_AUTHORITY,
10
11
  defineReplicatorProviderDefinitions,
11
12
  isReplicationCompleted,
@@ -50,6 +51,7 @@ export {
50
51
  REMOTE_RESOURCE_KINDS,
51
52
  REPLICATION_CANCELLED,
52
53
  REPLICATION_COMPLETED,
54
+ REPLICATION_PROGRESS_PRESENTATIONS,
53
55
  USER_INITIATED_REPLICATION_AUTHORITY,
54
56
  applyCentralRemoteAdministrationMutation,
55
57
  centralRemoteAdministrationVerificationFailed,
@@ -6,6 +6,7 @@ import {
6
6
  CAPABILITY_NOT_APPLICABLE,
7
7
  NO_REMOTE_RESOURCE_CAPABILITIES,
8
8
  PEER_REPLICATION_READINESS,
9
+ REPLICATION_PROGRESS_PRESENTATIONS,
9
10
  defineReplicatorProviderDefinitions,
10
11
  outcomeFromFiniteOpenReplication,
11
12
  replicationBlocked,
@@ -38,7 +39,7 @@ function useP2PReplicatorFeature(host, openReplicationUIFactory, openRebuildUIFa
38
39
  const result = await instance.openReplication(
39
40
  setting,
40
41
  false,
41
- request.interaction.permissions.failureRecovery,
42
+ request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
42
43
  false
43
44
  );
44
45
  return outcomeFromFiniteOpenReplication(result);
@@ -11,6 +11,7 @@ import {
11
11
  CAPABILITY_SUPPORT_KINDS,
12
12
  NO_INTERACTION,
13
13
  CENTRAL_REMOTE_REPLICATION_READINESS,
14
+ REPLICATION_PROGRESS_PRESENTATIONS,
14
15
  USER_INITIATED_REPLICATION_AUTHORITY,
15
16
  isActiveReplicatorContextBoundToSetting,
16
17
  isReplicationCompleted,
@@ -196,6 +197,7 @@ class ReplicationService extends ServiceBase {
196
197
  onReplicationFailed = handlers().bailFirstFailure("onReplicationFailed");
197
198
  async replicateUserInitiated(request = {
198
199
  trigger: "manual",
200
+ progressPresentation: REPLICATION_PROGRESS_PRESENTATIONS.NOTICE,
199
201
  interaction: USER_INITIATED_REPLICATION_AUTHORITY
200
202
  }) {
201
203
  return await this._typedReplication.runUserInitiated(request);
@@ -29,6 +29,7 @@ export interface TypedReplicationCoordinatorDependencies {
29
29
  */
30
30
  export declare class TypedReplicationCoordinator {
31
31
  private readonly dependencies;
32
+ private oneShotInProgress;
32
33
  constructor(dependencies: TypedReplicationCoordinatorDependencies);
33
34
  /**
34
35
  * Execute a user-initiated finite role with the request's interaction authority.
@@ -67,6 +68,15 @@ export declare class TypedReplicationCoordinator {
67
68
  * not a request to begin new remote work.
68
69
  */
69
70
  stopActiveTransfer(): Promise<ReplicationOutcome>;
71
+ /**
72
+ * Admit one complete typed OneShot attempt for this service instance.
73
+ *
74
+ * Ownership is recorded before the operation reaches its first await and is
75
+ * retained through readiness, provider work, activity settlement, and
76
+ * failure handling. Later requests are neither queued nor joined: they
77
+ * settle immediately so repeated UI actions cannot replay after the owner.
78
+ */
79
+ private runOneShot;
70
80
  private finishFiniteAttempt;
71
81
  private acquireContext;
72
82
  private acquireReadyContext;
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  CAPABILITY_SUPPORT_KINDS,
3
3
  NO_INTERACTION,
4
+ REPLICATION_PROGRESS_PRESENTATIONS,
4
5
  isActiveReplicatorContextBoundToSetting,
5
6
  replicationBlocked,
6
7
  replicationFailed
@@ -11,6 +12,7 @@ class TypedReplicationCoordinator {
11
12
  this.dependencies = dependencies;
12
13
  }
13
14
  dependencies;
15
+ oneShotInProgress = false;
14
16
  /**
15
17
  * Execute a user-initiated finite role with the request's interaction authority.
16
18
  *
@@ -20,23 +22,25 @@ class TypedReplicationCoordinator {
20
22
  * activity has settled.
21
23
  */
22
24
  async runUserInitiated(request) {
23
- const showMessage = request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery;
24
- const ready = await this.acquireReadyContext(showMessage);
25
- if ("status" in ready) return ready;
26
- const capability = ready.provider.userInitiatedOneShot;
27
- if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
28
- return this.finishFiniteAttempt(replicationBlocked(capability.reason));
29
- }
30
- const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
31
- return this.finishFiniteAttempt(
32
- await this.runAdmittedFiniteActivity(
33
- ready,
34
- (context) => capability.run(context.replicator, settings, request),
35
- settings,
36
- request.interaction,
37
- showMessage
38
- )
39
- );
25
+ return await this.runOneShot(async () => {
26
+ const showProgress = request.progressPresentation === REPLICATION_PROGRESS_PRESENTATIONS.NOTICE;
27
+ const ready = await this.acquireReadyContext(showProgress);
28
+ if ("status" in ready) return ready;
29
+ const capability = ready.provider.userInitiatedOneShot;
30
+ if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
31
+ return this.finishFiniteAttempt(replicationBlocked(capability.reason));
32
+ }
33
+ const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
34
+ return this.finishFiniteAttempt(
35
+ await this.runAdmittedFiniteActivity(
36
+ ready,
37
+ (context) => capability.run(context.replicator, settings, request),
38
+ settings,
39
+ request.interaction,
40
+ request.progressPresentation
41
+ )
42
+ );
43
+ });
40
44
  }
41
45
  /**
42
46
  * Execute an unattended finite role without granting interaction authority.
@@ -49,22 +53,24 @@ class TypedReplicationCoordinator {
49
53
  if (request.interaction.kind !== NO_INTERACTION.kind) {
50
54
  return replicationBlocked("interaction-required");
51
55
  }
52
- const ready = await this.acquireReadyContext(false);
53
- if ("status" in ready) return ready;
54
- const capability = ready.provider.unattendedOneShot;
55
- if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
56
- return this.finishFiniteAttempt(replicationBlocked(capability.reason));
57
- }
58
- const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
59
- return this.finishFiniteAttempt(
60
- await this.runAdmittedFiniteActivity(
61
- ready,
62
- (context) => capability.run(context.replicator, settings, request),
63
- settings,
64
- NO_INTERACTION,
65
- false
66
- )
67
- );
56
+ return await this.runOneShot(async () => {
57
+ const ready = await this.acquireReadyContext(false);
58
+ if ("status" in ready) return ready;
59
+ const capability = ready.provider.unattendedOneShot;
60
+ if (capability.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
61
+ return this.finishFiniteAttempt(replicationBlocked(capability.reason));
62
+ }
63
+ const settings = Object.freeze(asCopy(this.dependencies.currentSettings()));
64
+ return this.finishFiniteAttempt(
65
+ await this.runAdmittedFiniteActivity(
66
+ ready,
67
+ (context) => capability.run(context.replicator, settings, request),
68
+ settings,
69
+ NO_INTERACTION,
70
+ REPLICATION_PROGRESS_PRESENTATIONS.QUIET
71
+ )
72
+ );
73
+ });
68
74
  }
69
75
  /**
70
76
  * Start the provider's continuous role outside finite activity accounting.
@@ -130,6 +136,23 @@ class TypedReplicationCoordinator {
130
136
  );
131
137
  return admitted ?? replicationBlocked("no-active-replicator");
132
138
  }
139
+ /**
140
+ * Admit one complete typed OneShot attempt for this service instance.
141
+ *
142
+ * Ownership is recorded before the operation reaches its first await and is
143
+ * retained through readiness, provider work, activity settlement, and
144
+ * failure handling. Later requests are neither queued nor joined: they
145
+ * settle immediately so repeated UI actions cannot replay after the owner.
146
+ */
147
+ async runOneShot(run) {
148
+ if (this.oneShotInProgress) return replicationBlocked("replication-in-progress");
149
+ this.oneShotInProgress = true;
150
+ try {
151
+ return await run();
152
+ } finally {
153
+ this.oneShotInProgress = false;
154
+ }
155
+ }
133
156
  finishFiniteAttempt(outcome) {
134
157
  this.dependencies.recordFiniteAttempt();
135
158
  return outcome;
@@ -164,7 +187,7 @@ class TypedReplicationCoordinator {
164
187
  * is rejected instead of running work prepared for an older publication.
165
188
  * Failure recovery runs only after the reservation has been released.
166
189
  */
167
- async runAdmittedFiniteActivity(expectedContext, run, setting, interaction, showMessage) {
190
+ async runAdmittedFiniteActivity(expectedContext, run, setting, interaction, progressPresentation) {
168
191
  const admitted = await this.dependencies.replicatorService.runWithActiveReplicatorContext((context) => {
169
192
  if (context !== expectedContext || !isActiveReplicatorContextBoundToSetting(context, setting)) {
170
193
  return replicationBlocked("not-ready");
@@ -178,7 +201,7 @@ class TypedReplicationCoordinator {
178
201
  context: expectedContext,
179
202
  setting,
180
203
  outcome: result,
181
- showMessage,
204
+ progressPresentation,
182
205
  interaction
183
206
  })
184
207
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrtmrz/livesync-commonlib",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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",