mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-09-21 18:17:05 +00:00
Adopt active Replicator ownership contracts
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
import {
|
||||
MILESTONE_DOCID,
|
||||
type EntryMilestoneInfo,
|
||||
type RemoteDBSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import type { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import {
|
||||
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS,
|
||||
CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
|
||||
applyCentralRemoteAdministrationMutation,
|
||||
milestoneSatisfiesCentralRemoteAdministration,
|
||||
centralRemoteAdministrationVerificationFailed,
|
||||
centralRemoteAdministrationVerified,
|
||||
supportedCapability,
|
||||
type MilestoneCentralRemoteAdministrationObservation,
|
||||
type CentralRemoteAdministrationFailureReason,
|
||||
type CentralRemoteAdministrationRequest,
|
||||
type CentralRemoteAdministrationReplicator,
|
||||
type CentralRemoteAdministrationResult,
|
||||
type CentralRemoteAdministrationRunner,
|
||||
type SupportedCapability,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const JOURNAL_MILESTONE_PATH = "_00000000-milestone.json";
|
||||
|
||||
type CentralMilestoneReadResult =
|
||||
| { readonly milestone: EntryMilestoneInfo | false | undefined }
|
||||
| { readonly failureReason: CentralRemoteAdministrationFailureReason; readonly detail?: unknown };
|
||||
|
||||
type PreparedCentralMilestoneReader = () => Promise<CentralMilestoneReadResult>;
|
||||
|
||||
type CentralMilestoneReaderPreparer = (
|
||||
replicator: CentralRemoteAdministrationReplicator,
|
||||
setting: RemoteDBSettings
|
||||
) => PreparedCentralMilestoneReader;
|
||||
|
||||
type CouchDBAdministrationReplicator = CentralRemoteAdministrationReplicator &
|
||||
Pick<LiveSyncCouchDBReplicator, "connectRemoteCouchDBWithSetting" | "isMobile">;
|
||||
|
||||
type JournalAdministrationClient = Pick<LiveSyncJournalReplicator["client"], "downloadJson">;
|
||||
|
||||
async function ensureLocalNodeIdentity(
|
||||
replicator: CentralRemoteAdministrationReplicator
|
||||
): Promise<CentralRemoteAdministrationResult | undefined> {
|
||||
if (replicator.nodeid) {
|
||||
return undefined;
|
||||
}
|
||||
if ((await replicator.initializeDatabaseForReplication()) && replicator.nodeid) {
|
||||
return undefined;
|
||||
}
|
||||
return centralRemoteAdministrationVerificationFailed(
|
||||
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.LOCAL_IDENTITY_UNAVAILABLE
|
||||
);
|
||||
}
|
||||
|
||||
function observeMilestone(
|
||||
replicator: CentralRemoteAdministrationReplicator,
|
||||
milestone: EntryMilestoneInfo
|
||||
): MilestoneCentralRemoteAdministrationObservation {
|
||||
return {
|
||||
kind: CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
locked: !!milestone.locked,
|
||||
accepted: !!milestone.accepted_nodes?.includes(replicator.nodeid),
|
||||
nodeId: replicator.nodeid,
|
||||
};
|
||||
}
|
||||
|
||||
function resultFromMilestone(
|
||||
replicator: CentralRemoteAdministrationReplicator,
|
||||
request: CentralRemoteAdministrationRequest,
|
||||
milestone: EntryMilestoneInfo | false | undefined
|
||||
): CentralRemoteAdministrationResult {
|
||||
if (!milestone) {
|
||||
return centralRemoteAdministrationVerificationFailed(
|
||||
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_NOT_FOUND
|
||||
);
|
||||
}
|
||||
const observation = observeMilestone(replicator, milestone);
|
||||
return milestoneSatisfiesCentralRemoteAdministration(request.action, observation)
|
||||
? centralRemoteAdministrationVerified(observation)
|
||||
: centralRemoteAdministrationVerificationFailed(
|
||||
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.POSTCONDITION_MISMATCH,
|
||||
{
|
||||
observation,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply and verify the central milestone protocol without selecting a provider.
|
||||
*
|
||||
* The provider definition has already selected the reader preparer. Preparing
|
||||
* it before mutation rejects incomplete composition before a remote write and
|
||||
* binds any provider-owned client which must be used for postcondition reading.
|
||||
*/
|
||||
async function runCentralRemoteAdministration(
|
||||
replicator: CentralRemoteAdministrationReplicator,
|
||||
setting: RemoteDBSettings,
|
||||
request: CentralRemoteAdministrationRequest,
|
||||
prepareMilestoneReader: CentralMilestoneReaderPreparer
|
||||
): Promise<CentralRemoteAdministrationResult> {
|
||||
const identityFailure = await ensureLocalNodeIdentity(replicator);
|
||||
if (identityFailure) return identityFailure;
|
||||
|
||||
const readMilestone = prepareMilestoneReader(replicator, setting);
|
||||
await applyCentralRemoteAdministrationMutation(replicator, setting, request.action);
|
||||
|
||||
const readResult = await readMilestone();
|
||||
if ("failureReason" in readResult) {
|
||||
return centralRemoteAdministrationVerificationFailed(readResult.failureReason, { detail: readResult.detail });
|
||||
}
|
||||
return resultFromMilestone(replicator, request, readResult.milestone);
|
||||
}
|
||||
|
||||
function requireCouchDBAdministrationOperations(
|
||||
replicator: CentralRemoteAdministrationReplicator
|
||||
): asserts replicator is CouchDBAdministrationReplicator {
|
||||
const candidate = replicator as Partial<CouchDBAdministrationReplicator>;
|
||||
if (typeof candidate.connectRemoteCouchDBWithSetting !== "function" || typeof candidate.isMobile !== "function") {
|
||||
throw new Error("The configured CouchDB administration adapter does not provide milestone access.");
|
||||
}
|
||||
}
|
||||
|
||||
function prepareCouchDBMilestoneReader(
|
||||
replicator: CentralRemoteAdministrationReplicator,
|
||||
setting: RemoteDBSettings
|
||||
): PreparedCentralMilestoneReader {
|
||||
requireCouchDBAdministrationOperations(replicator);
|
||||
|
||||
return async () => {
|
||||
let connection: Awaited<ReturnType<CouchDBAdministrationReplicator["connectRemoteCouchDBWithSetting"]>>;
|
||||
try {
|
||||
connection = await replicator.connectRemoteCouchDBWithSetting(setting, replicator.isMobile(), true);
|
||||
} catch (error) {
|
||||
return { failureReason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.CONNECTION_FAILED, detail: error };
|
||||
}
|
||||
if (typeof connection === "string") {
|
||||
return {
|
||||
failureReason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.CONNECTION_FAILED,
|
||||
detail: connection,
|
||||
};
|
||||
}
|
||||
|
||||
let milestone: EntryMilestoneInfo | undefined;
|
||||
let observationError: unknown;
|
||||
try {
|
||||
milestone = await connection.db.get<EntryMilestoneInfo>(MILESTONE_DOCID);
|
||||
} catch (error) {
|
||||
observationError = error;
|
||||
}
|
||||
try {
|
||||
await connection.close();
|
||||
} catch (error) {
|
||||
observationError ??= error;
|
||||
}
|
||||
if (observationError !== undefined) {
|
||||
return {
|
||||
failureReason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_READ_FAILED,
|
||||
detail: observationError,
|
||||
};
|
||||
}
|
||||
return { milestone };
|
||||
};
|
||||
}
|
||||
|
||||
function requireJournalAdministrationClient(
|
||||
replicator: CentralRemoteAdministrationReplicator
|
||||
): JournalAdministrationClient {
|
||||
const client = (replicator as { readonly client?: JournalAdministrationClient }).client;
|
||||
if (typeof client?.downloadJson !== "function") {
|
||||
throw new Error("The configured Object Storage administration adapter does not provide milestone access.");
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
function prepareObjectStorageMilestoneReader(
|
||||
replicator: CentralRemoteAdministrationReplicator
|
||||
): PreparedCentralMilestoneReader {
|
||||
const client = requireJournalAdministrationClient(replicator);
|
||||
|
||||
return async () => {
|
||||
try {
|
||||
return { milestone: await client.downloadJson<EntryMilestoneInfo>(JOURNAL_MILESTONE_PATH) };
|
||||
} catch (error) {
|
||||
return {
|
||||
failureReason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_READ_FAILED,
|
||||
detail: error,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const runCouchDBCentralRemoteAdministration: CentralRemoteAdministrationRunner = async (replicator, setting, request) =>
|
||||
await runCentralRemoteAdministration(replicator, setting, request, prepareCouchDBMilestoneReader);
|
||||
|
||||
const runObjectStorageCentralRemoteAdministration: CentralRemoteAdministrationRunner = async (
|
||||
replicator,
|
||||
setting,
|
||||
request
|
||||
) => await runCentralRemoteAdministration(replicator, setting, request, prepareObjectStorageMilestoneReader);
|
||||
|
||||
/** CouchDB mutation and milestone postcondition verification capability. */
|
||||
export const COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY: SupportedCapability<CentralRemoteAdministrationRunner> =
|
||||
supportedCapability(runCouchDBCentralRemoteAdministration);
|
||||
|
||||
/** Object Storage mutation and milestone postcondition verification capability. */
|
||||
export const OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY: SupportedCapability<CentralRemoteAdministrationRunner> =
|
||||
supportedCapability(runObjectStorageCentralRemoteAdministration);
|
||||
+67
-26
@@ -1,15 +1,15 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { DEFAULT_SETTINGS, REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
REMOTE_ADMINISTRATION_ACTIONS,
|
||||
REMOTE_ADMINISTRATION_FAILURE_REASONS,
|
||||
REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
|
||||
REMOTE_ADMINISTRATION_RESULT_STATUSES,
|
||||
CENTRAL_REMOTE_ADMINISTRATION_ACTIONS,
|
||||
CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS,
|
||||
CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
|
||||
CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
OBJECT_STORAGE_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
} from "./replicatorAdministration";
|
||||
COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
} from "./centralRemoteAdministration";
|
||||
|
||||
describe("central remote administration capabilities", () => {
|
||||
it("mutates CouchDB, verifies the requested postcondition, and closes only the owned connection", async () => {
|
||||
@@ -28,14 +28,14 @@ describe("central remote administration capabilities", () => {
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: database, close })),
|
||||
};
|
||||
const setting = { ...DEFAULT_SETTINGS, remoteType: REMOTE_COUCHDB };
|
||||
const capability = COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
const capability = COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
|
||||
await expect(
|
||||
capability.run(replicator as never, setting, { action: REMOTE_ADMINISTRATION_ACTIONS.LOCK })
|
||||
capability.run(replicator as never, setting, { action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.LOCK })
|
||||
).resolves.toEqual({
|
||||
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFIED,
|
||||
status: CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFIED,
|
||||
observation: {
|
||||
kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
kind: CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
locked: true,
|
||||
accepted: true,
|
||||
nodeId: "node-1",
|
||||
@@ -60,20 +60,20 @@ describe("central remote administration capabilities", () => {
|
||||
close,
|
||||
})),
|
||||
};
|
||||
const capability = COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
const capability = COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
|
||||
const result = await capability.run(
|
||||
replicator as never,
|
||||
{ ...DEFAULT_SETTINGS, remoteType: REMOTE_COUCHDB },
|
||||
{
|
||||
action: REMOTE_ADMINISTRATION_ACTIONS.LOCK,
|
||||
action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.LOCK,
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
|
||||
reason: REMOTE_ADMINISTRATION_FAILURE_REASONS.POSTCONDITION_MISMATCH,
|
||||
observation: { kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE, locked: false },
|
||||
status: CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
|
||||
reason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.POSTCONDITION_MISMATCH,
|
||||
observation: { kind: CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE, locked: false },
|
||||
});
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
});
|
||||
@@ -87,17 +87,17 @@ describe("central remote administration capabilities", () => {
|
||||
markRemoteResolved: vi.fn(async () => undefined),
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => "must not connect"),
|
||||
};
|
||||
const capability = COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
const capability = COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
|
||||
await expect(
|
||||
capability.run(
|
||||
replicator as never,
|
||||
{ ...DEFAULT_SETTINGS, remoteType: REMOTE_COUCHDB },
|
||||
{ action: REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED }
|
||||
{ action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED }
|
||||
)
|
||||
).resolves.toEqual({
|
||||
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
|
||||
reason: REMOTE_ADMINISTRATION_FAILURE_REASONS.LOCAL_IDENTITY_UNAVAILABLE,
|
||||
status: CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFICATION_FAILED,
|
||||
reason: CENTRAL_REMOTE_ADMINISTRATION_FAILURE_REASONS.LOCAL_IDENTITY_UNAVAILABLE,
|
||||
});
|
||||
expect(replicator.markRemoteResolved).not.toHaveBeenCalled();
|
||||
expect(replicator.connectRemoteCouchDBWithSetting).not.toHaveBeenCalled();
|
||||
@@ -115,14 +115,14 @@ describe("central remote administration capabilities", () => {
|
||||
markRemoteResolved: vi.fn(async () => undefined),
|
||||
connectRemoteCouchDBWithSetting: vi.fn(),
|
||||
};
|
||||
const capability = COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
const capability = COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
|
||||
await expect(
|
||||
capability.run(
|
||||
replicator as never,
|
||||
{ ...DEFAULT_SETTINGS, remoteType: REMOTE_COUCHDB },
|
||||
{
|
||||
action: REMOTE_ADMINISTRATION_ACTIONS.UNLOCK,
|
||||
action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.UNLOCK,
|
||||
}
|
||||
)
|
||||
).rejects.toBe(failure);
|
||||
@@ -139,14 +139,16 @@ describe("central remote administration capabilities", () => {
|
||||
client: { downloadJson },
|
||||
};
|
||||
const setting = { ...DEFAULT_SETTINGS, remoteType: REMOTE_MINIO };
|
||||
const capability = OBJECT_STORAGE_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
const capability = OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY;
|
||||
|
||||
await expect(
|
||||
capability.run(replicator as never, setting, { action: REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED })
|
||||
capability.run(replicator as never, setting, {
|
||||
action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED,
|
||||
})
|
||||
).resolves.toEqual({
|
||||
status: REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFIED,
|
||||
status: CENTRAL_REMOTE_ADMINISTRATION_RESULT_STATUSES.VERIFIED,
|
||||
observation: {
|
||||
kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
kind: CENTRAL_REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
locked: false,
|
||||
accepted: true,
|
||||
nodeId: "node-1",
|
||||
@@ -155,4 +157,43 @@ describe("central remote administration capabilities", () => {
|
||||
expect(replicator.markRemoteResolved).toHaveBeenCalledWith(setting);
|
||||
expect(downloadJson).toHaveBeenCalledWith("_00000000-milestone.json");
|
||||
});
|
||||
|
||||
it("rejects an incomplete CouchDB milestone adapter before mutation", async () => {
|
||||
const markRemoteLocked = vi.fn(async () => undefined);
|
||||
const replicator = {
|
||||
nodeid: "node-1",
|
||||
initializeDatabaseForReplication: vi.fn(async () => true),
|
||||
markRemoteLocked,
|
||||
markRemoteResolved: vi.fn(async () => undefined),
|
||||
};
|
||||
|
||||
await expect(
|
||||
COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY.run(
|
||||
replicator as never,
|
||||
{ ...DEFAULT_SETTINGS, remoteType: REMOTE_COUCHDB },
|
||||
{ action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.LOCK }
|
||||
)
|
||||
).rejects.toThrow("The configured CouchDB administration adapter does not provide milestone access.");
|
||||
expect(markRemoteLocked).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects an incomplete Object Storage milestone adapter before mutation", async () => {
|
||||
const markRemoteResolved = vi.fn(async () => undefined);
|
||||
const replicator = {
|
||||
nodeid: "node-1",
|
||||
initializeDatabaseForReplication: vi.fn(async () => true),
|
||||
markRemoteLocked: vi.fn(async () => undefined),
|
||||
markRemoteResolved,
|
||||
client: {},
|
||||
};
|
||||
|
||||
await expect(
|
||||
OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY.run(
|
||||
replicator as never,
|
||||
{ ...DEFAULT_SETTINGS, remoteType: REMOTE_MINIO },
|
||||
{ action: CENTRAL_REMOTE_ADMINISTRATION_ACTIONS.MARK_RESOLVED }
|
||||
)
|
||||
).rejects.toThrow("The configured Object Storage administration adapter does not provide milestone access.");
|
||||
expect(markRemoteResolved).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,143 +0,0 @@
|
||||
import {
|
||||
MILESTONE_DOCID,
|
||||
type EntryMilestoneInfo,
|
||||
type RemoteDBSettings,
|
||||
} from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { LiveSyncAbstractReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/LiveSyncAbstractReplicator";
|
||||
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import {
|
||||
REMOTE_ADMINISTRATION_FAILURE_REASONS,
|
||||
REMOTE_ADMINISTRATION_OBSERVATION_KINDS,
|
||||
applyRemoteAdministrationMutation,
|
||||
milestoneSatisfiesRemoteAdministration,
|
||||
remoteAdministrationVerificationFailed,
|
||||
remoteAdministrationVerified,
|
||||
supportedCapability,
|
||||
type MilestoneRemoteAdministrationObservation,
|
||||
type RemoteAdministrationRequest,
|
||||
type RemoteAdministrationResult,
|
||||
type SupportedCapability,
|
||||
type RemoteAdministrationRunner,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const JOURNAL_MILESTONE_PATH = "_00000000-milestone.json";
|
||||
|
||||
async function ensureLocalNodeIdentity(
|
||||
replicator: LiveSyncAbstractReplicator
|
||||
): Promise<RemoteAdministrationResult | undefined> {
|
||||
if (replicator.nodeid) {
|
||||
return undefined;
|
||||
}
|
||||
if ((await replicator.initializeDatabaseForReplication()) && replicator.nodeid) {
|
||||
return undefined;
|
||||
}
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.LOCAL_IDENTITY_UNAVAILABLE);
|
||||
}
|
||||
|
||||
function observeMilestone(
|
||||
replicator: LiveSyncAbstractReplicator,
|
||||
milestone: EntryMilestoneInfo
|
||||
): MilestoneRemoteAdministrationObservation {
|
||||
return {
|
||||
kind: REMOTE_ADMINISTRATION_OBSERVATION_KINDS.MILESTONE,
|
||||
locked: !!milestone.locked,
|
||||
accepted: !!milestone.accepted_nodes?.includes(replicator.nodeid),
|
||||
nodeId: replicator.nodeid,
|
||||
};
|
||||
}
|
||||
|
||||
function resultFromMilestone(
|
||||
replicator: LiveSyncAbstractReplicator,
|
||||
request: RemoteAdministrationRequest,
|
||||
milestone: EntryMilestoneInfo | false | undefined
|
||||
): RemoteAdministrationResult {
|
||||
if (!milestone) {
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_NOT_FOUND);
|
||||
}
|
||||
const observation = observeMilestone(replicator, milestone);
|
||||
return milestoneSatisfiesRemoteAdministration(request.action, observation)
|
||||
? remoteAdministrationVerified(observation)
|
||||
: remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.POSTCONDITION_MISMATCH, {
|
||||
observation,
|
||||
});
|
||||
}
|
||||
|
||||
async function runCouchDBRemoteAdministration(
|
||||
replicator: LiveSyncAbstractReplicator,
|
||||
setting: RemoteDBSettings,
|
||||
request: RemoteAdministrationRequest
|
||||
): Promise<RemoteAdministrationResult> {
|
||||
const identityFailure = await ensureLocalNodeIdentity(replicator);
|
||||
if (identityFailure) return identityFailure;
|
||||
|
||||
await applyRemoteAdministrationMutation(replicator, setting, request.action);
|
||||
|
||||
const couchDBReplicator = replicator as LiveSyncCouchDBReplicator;
|
||||
let connection;
|
||||
try {
|
||||
connection = await couchDBReplicator.connectRemoteCouchDBWithSetting(
|
||||
setting,
|
||||
couchDBReplicator.isMobile(),
|
||||
true
|
||||
);
|
||||
} catch (error) {
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.CONNECTION_FAILED, {
|
||||
detail: error,
|
||||
});
|
||||
}
|
||||
if (typeof connection === "string") {
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.CONNECTION_FAILED, {
|
||||
detail: connection,
|
||||
});
|
||||
}
|
||||
|
||||
let milestone: EntryMilestoneInfo | undefined;
|
||||
let observationError: unknown;
|
||||
try {
|
||||
milestone = await connection.db.get<EntryMilestoneInfo>(MILESTONE_DOCID);
|
||||
} catch (error) {
|
||||
observationError = error;
|
||||
}
|
||||
try {
|
||||
await connection.close();
|
||||
} catch (error) {
|
||||
observationError ??= error;
|
||||
}
|
||||
if (observationError !== undefined) {
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_READ_FAILED, {
|
||||
detail: observationError,
|
||||
});
|
||||
}
|
||||
return resultFromMilestone(replicator, request, milestone);
|
||||
}
|
||||
|
||||
async function runObjectStorageRemoteAdministration(
|
||||
replicator: LiveSyncAbstractReplicator,
|
||||
setting: RemoteDBSettings,
|
||||
request: RemoteAdministrationRequest
|
||||
): Promise<RemoteAdministrationResult> {
|
||||
const identityFailure = await ensureLocalNodeIdentity(replicator);
|
||||
if (identityFailure) return identityFailure;
|
||||
|
||||
await applyRemoteAdministrationMutation(replicator, setting, request.action);
|
||||
|
||||
const journalReplicator = replicator as LiveSyncJournalReplicator;
|
||||
let milestone: EntryMilestoneInfo | false | undefined;
|
||||
try {
|
||||
milestone = await journalReplicator.client.downloadJson<EntryMilestoneInfo>(JOURNAL_MILESTONE_PATH);
|
||||
} catch (error) {
|
||||
return remoteAdministrationVerificationFailed(REMOTE_ADMINISTRATION_FAILURE_REASONS.MILESTONE_READ_FAILED, {
|
||||
detail: error,
|
||||
});
|
||||
}
|
||||
return resultFromMilestone(replicator, request, milestone);
|
||||
}
|
||||
|
||||
/** CouchDB mutation and milestone postcondition verification capability. */
|
||||
export const COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY: SupportedCapability<RemoteAdministrationRunner> =
|
||||
supportedCapability(runCouchDBRemoteAdministration);
|
||||
|
||||
/** Object Storage mutation and milestone postcondition verification capability. */
|
||||
export const OBJECT_STORAGE_REMOTE_ADMINISTRATION_CAPABILITY: SupportedCapability<RemoteAdministrationRunner> =
|
||||
supportedCapability(runObjectStorageRemoteAdministration);
|
||||
@@ -1,16 +1,20 @@
|
||||
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import { REMOTE_COUCHDB, REMOTE_MINIO, type RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import {
|
||||
CAPABILITY_NOT_APPLICABLE,
|
||||
CENTRAL_REMOTE_REPLICATION_READINESS,
|
||||
NO_INTERACTION,
|
||||
REMOTE_RESOURCE_KINDS,
|
||||
REPLACE_SAME_KIND_REPLICATOR,
|
||||
defineReplicatorProviderDefinitions,
|
||||
supportedOpenReplicationContinuous,
|
||||
supportedOpenReplicationOneShot,
|
||||
supportedOpenReplicationUnattended,
|
||||
replicationBlocked,
|
||||
replicationFailed,
|
||||
supportedStopActiveTransfer,
|
||||
supportedCapability,
|
||||
type ReplicatorProviderDefinitionMap,
|
||||
type ReplicationOutcome,
|
||||
type ReplicatorInstance,
|
||||
type UserInitiatedOneShotRunner,
|
||||
type UnattendedOneShotRunner,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
LiveSyncCouchDBReplicator,
|
||||
@@ -32,12 +36,62 @@ import {
|
||||
createObjectStorageSecuritySeedResourceFactory,
|
||||
} from "./replicatorResources";
|
||||
import {
|
||||
COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
OBJECT_STORAGE_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
} from "./replicatorAdministration";
|
||||
COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
} from "./centralRemoteAdministration";
|
||||
|
||||
export type CentralReplicatorProviderHost = LiveSyncCouchDBReplicatorEnv & LiveSyncJournalReplicatorEnv;
|
||||
|
||||
/** Minimal operation required by both central one-shot adapters. */
|
||||
interface OneShotOutcomeReplicator extends ReplicatorInstance {
|
||||
openOneShotReplicationWithOutcome(setting: RemoteDBSettings, showResult: boolean): Promise<ReplicationOutcome>;
|
||||
}
|
||||
|
||||
function asOneShotOutcomeReplicator(instance: ReplicatorInstance): OneShotOutcomeReplicator | undefined {
|
||||
const candidate = instance as Partial<OneShotOutcomeReplicator>;
|
||||
return typeof candidate.openOneShotReplicationWithOutcome === "function"
|
||||
? (instance as OneShotOutcomeReplicator)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
async function runOneShotWithOutcome(
|
||||
instance: ReplicatorInstance,
|
||||
setting: RemoteDBSettings,
|
||||
showResult: boolean
|
||||
): Promise<ReplicationOutcome> {
|
||||
const replicator = asOneShotOutcomeReplicator(instance);
|
||||
if (!replicator) {
|
||||
return replicationFailed(new Error("The configured provider does not implement one-shot replication."));
|
||||
}
|
||||
return await replicator.openOneShotReplicationWithOutcome(setting, showResult);
|
||||
}
|
||||
|
||||
const couchDBUserInitiatedOneShot: UserInitiatedOneShotRunner = async (instance, setting, request) => {
|
||||
return await runOneShotWithOutcome(
|
||||
instance,
|
||||
setting,
|
||||
request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery
|
||||
);
|
||||
};
|
||||
|
||||
const couchDBUnattendedOneShot: UnattendedOneShotRunner = async (instance, setting, request) => {
|
||||
if (request.interaction.kind !== NO_INTERACTION.kind) return replicationBlocked("interaction-required");
|
||||
return await runOneShotWithOutcome(instance, setting, false);
|
||||
};
|
||||
|
||||
const objectStorageUserInitiatedOneShot: UserInitiatedOneShotRunner = async (instance, setting, request) => {
|
||||
return await runOneShotWithOutcome(
|
||||
instance,
|
||||
setting,
|
||||
request.interaction.kind === "permitted" && request.interaction.permissions.failureRecovery
|
||||
);
|
||||
};
|
||||
|
||||
const objectStorageUnattendedOneShot: UnattendedOneShotRunner = async (instance, setting, request) => {
|
||||
if (request.interaction.kind !== NO_INTERACTION.kind) return replicationBlocked("interaction-required");
|
||||
return await runOneShotWithOutcome(instance, setting, false);
|
||||
};
|
||||
|
||||
/** Build the complete central-remote provider policy for one LiveSync host. */
|
||||
export function createCentralReplicatorProviderDefinitions(
|
||||
host: CentralReplicatorProviderHost
|
||||
@@ -52,7 +106,6 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
!!settings.couchDB_URI?.trim() &&
|
||||
!!settings.couchDB_DBNAME?.trim(),
|
||||
configurationIdentity: getCouchDBReplicatorConfigurationIdentity,
|
||||
sameKindReconciliation: REPLACE_SAME_KIND_REPLICATOR,
|
||||
create: () => Promise.resolve(new LiveSyncCouchDBReplicator(host)),
|
||||
remoteResources: {
|
||||
[REMOTE_RESOURCE_KINDS.CONNECTION]: supportedCapability(createCouchDBConnectionProbeFactory(host)),
|
||||
@@ -66,9 +119,9 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
createCouchDBSynchronisationInformationResourceFactory(host)
|
||||
),
|
||||
},
|
||||
remoteAdministration: COUCHDB_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedOpenReplicationOneShot(),
|
||||
unattendedOneShot: supportedOpenReplicationUnattended(),
|
||||
centralRemoteAdministration: COUCHDB_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedCapability(couchDBUserInitiatedOneShot),
|
||||
unattendedOneShot: supportedCapability(couchDBUnattendedOneShot),
|
||||
continuous: supportedOpenReplicationContinuous(),
|
||||
stopActiveTransfer: supportedStopActiveTransfer(),
|
||||
},
|
||||
@@ -79,7 +132,6 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
isConfigured: (settings) =>
|
||||
settings.remoteType === REMOTE_MINIO && !!settings.endpoint?.trim() && !!settings.bucket?.trim(),
|
||||
configurationIdentity: getObjectStorageReplicatorConfigurationIdentity,
|
||||
sameKindReconciliation: REPLACE_SAME_KIND_REPLICATOR,
|
||||
create: () => Promise.resolve(new LiveSyncJournalReplicator(host)),
|
||||
remoteResources: {
|
||||
[REMOTE_RESOURCE_KINDS.CONNECTION]: supportedCapability(
|
||||
@@ -93,9 +145,9 @@ export function createCentralReplicatorProviderDefinitions(
|
||||
),
|
||||
[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION]: CAPABILITY_NOT_APPLICABLE,
|
||||
},
|
||||
remoteAdministration: OBJECT_STORAGE_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedOpenReplicationOneShot(),
|
||||
unattendedOneShot: supportedOpenReplicationUnattended(),
|
||||
centralRemoteAdministration: OBJECT_STORAGE_CENTRAL_REMOTE_ADMINISTRATION_CAPABILITY,
|
||||
userInitiatedOneShot: supportedCapability(objectStorageUserInitiatedOneShot),
|
||||
unattendedOneShot: supportedCapability(objectStorageUnattendedOneShot),
|
||||
continuous: CAPABILITY_NOT_APPLICABLE,
|
||||
stopActiveTransfer: supportedStopActiveTransfer(),
|
||||
},
|
||||
|
||||
@@ -3,13 +3,17 @@ import { REMOTE_COUCHDB, REMOTE_MINIO } from "@vrtmrz/livesync-commonlib/compat/
|
||||
import { createNewVaultSettings } from "@vrtmrz/livesync-commonlib/settings";
|
||||
import {
|
||||
CAPABILITY_SUPPORT_KINDS,
|
||||
NO_INTERACTION,
|
||||
REPLICATION_COMPLETED,
|
||||
REMOTE_RESOURCE_KINDS,
|
||||
REPLACE_SAME_KIND_REPLICATOR,
|
||||
USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
} from "@vrtmrz/livesync-commonlib/replication";
|
||||
|
||||
const constructorMocks = vi.hoisted(() => ({
|
||||
couchDB: vi.fn(),
|
||||
couchDBOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
|
||||
objectStorage: vi.fn(),
|
||||
objectStorageOneShot: vi.fn(async (..._args: unknown[]) => REPLICATION_COMPLETED),
|
||||
}));
|
||||
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||
@@ -17,6 +21,9 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicato
|
||||
constructor(host: unknown) {
|
||||
constructorMocks.couchDB(host);
|
||||
}
|
||||
openOneShotReplicationWithOutcome(...args: unknown[]) {
|
||||
return constructorMocks.couchDBOneShot(...args);
|
||||
}
|
||||
},
|
||||
}));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator", () => ({
|
||||
@@ -24,12 +31,21 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalRe
|
||||
constructor(host: unknown) {
|
||||
constructorMocks.objectStorage(host);
|
||||
}
|
||||
openOneShotReplicationWithOutcome(...args: unknown[]) {
|
||||
return constructorMocks.objectStorageOneShot(...args);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
import { createCentralReplicatorProviderDefinitions } from "./replicatorProviders";
|
||||
|
||||
describe("central Replicator provider definitions", () => {
|
||||
it("keeps the retained remote-resource catalogue bounded", () => {
|
||||
expect
|
||||
.soft(Object.values(REMOTE_RESOURCE_KINDS).sort())
|
||||
.toEqual(["connection", "preferred-tweak", "security-seed", "synchronisation-information"].sort());
|
||||
});
|
||||
|
||||
it("composes CouchDB and Object Storage policies outside LiveSyncBaseCore", async () => {
|
||||
const host = {} as Parameters<typeof createCentralReplicatorProviderDefinitions>[0];
|
||||
const definitions = createCentralReplicatorProviderDefinitions(host);
|
||||
@@ -37,8 +53,8 @@ describe("central Replicator provider definitions", () => {
|
||||
const objectStorage = definitions.get(REMOTE_MINIO)!;
|
||||
|
||||
expect([...definitions.keys()]).toEqual([REMOTE_COUCHDB, REMOTE_MINIO]);
|
||||
expect(couchDB.sameKindReconciliation).toBe(REPLACE_SAME_KIND_REPLICATOR);
|
||||
expect(objectStorage.sameKindReconciliation).toBe(REPLACE_SAME_KIND_REPLICATOR);
|
||||
expect("sameKindReconciliation" in couchDB).toBe(false);
|
||||
expect("sameKindReconciliation" in objectStorage).toBe(false);
|
||||
|
||||
expect(
|
||||
couchDB.isConfigured(
|
||||
@@ -76,10 +92,12 @@ describe("central Replicator provider definitions", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("declares an exhaustive resource and administration catalogue for both central providers", () => {
|
||||
it("declares the retained owned resources and cohesive optional administration", () => {
|
||||
const definitions = createCentralReplicatorProviderDefinitions({} as never);
|
||||
const couchResources = definitions.get(REMOTE_COUCHDB)?.remoteResources;
|
||||
const objectResources = definitions.get(REMOTE_MINIO)?.remoteResources;
|
||||
const couchAdministration = definitions.get(REMOTE_COUCHDB)?.centralRemoteAdministration;
|
||||
const objectAdministration = definitions.get(REMOTE_MINIO)?.centralRemoteAdministration;
|
||||
|
||||
expect(Object.keys(couchResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
|
||||
expect(Object.keys(objectResources ?? {}).sort()).toEqual(Object.values(REMOTE_RESOURCE_KINDS).sort());
|
||||
@@ -93,7 +111,100 @@ describe("central Replicator provider definitions", () => {
|
||||
expect(objectResources?.[REMOTE_RESOURCE_KINDS.SYNCHRONISATION_INFORMATION].kind).toBe(
|
||||
CAPABILITY_SUPPORT_KINDS.NOT_APPLICABLE
|
||||
);
|
||||
expect(definitions.get(REMOTE_COUCHDB)?.remoteAdministration.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(definitions.get(REMOTE_MINIO)?.remoteAdministration.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(couchAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect(objectAdministration?.kind).toBe(CAPABILITY_SUPPORT_KINDS.SUPPORTED);
|
||||
expect("activeRemoteReads" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
|
||||
expect("fullTransfers" in definitions.get(REMOTE_COUCHDB)!).toBe(false);
|
||||
});
|
||||
|
||||
it("dispatches central finite work through provider-local attempt results", async () => {
|
||||
const definitions = createCentralReplicatorProviderDefinitions({} as never);
|
||||
const couchDB = definitions.get(REMOTE_COUCHDB)!;
|
||||
const objectStorage = definitions.get(REMOTE_MINIO)!;
|
||||
const setting = createNewVaultSettings();
|
||||
const couchInstance = await couchDB.create(setting);
|
||||
const objectInstance = await objectStorage.create(setting);
|
||||
if (!couchInstance || !objectInstance) throw new Error("Provider construction failed");
|
||||
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
||||
throw new Error("CouchDB OneShot is unavailable");
|
||||
}
|
||||
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
||||
throw new Error("Object Storage OneShot is unavailable");
|
||||
}
|
||||
|
||||
await expect(
|
||||
couchDB.userInitiatedOneShot.run(couchInstance, setting, {
|
||||
trigger: "manual",
|
||||
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
})
|
||||
).resolves.toBe(REPLICATION_COMPLETED);
|
||||
await expect(
|
||||
objectStorage.unattendedOneShot.run(objectInstance, setting, {
|
||||
trigger: "resume",
|
||||
interaction: NO_INTERACTION,
|
||||
})
|
||||
).resolves.toBe(REPLICATION_COMPLETED);
|
||||
|
||||
expect(constructorMocks.couchDBOneShot).toHaveBeenCalledWith(setting, true);
|
||||
expect(constructorMocks.objectStorageOneShot).toHaveBeenCalledWith(setting, false);
|
||||
});
|
||||
|
||||
it("dispatches central finite work through the declared operation rather than constructor identity", async () => {
|
||||
const definitions = createCentralReplicatorProviderDefinitions({} as never);
|
||||
const couchDB = definitions.get(REMOTE_COUCHDB)!;
|
||||
const objectStorage = definitions.get(REMOTE_MINIO)!;
|
||||
const setting = createNewVaultSettings();
|
||||
const createStructuralOneShotReplicator = () => ({
|
||||
initializeDatabaseForReplication: vi.fn(async () => true),
|
||||
openReplication: vi.fn(async () => true),
|
||||
terminateSync: vi.fn(),
|
||||
closeReplication: vi.fn(),
|
||||
openOneShotReplicationWithOutcome: vi.fn(async () => REPLICATION_COMPLETED),
|
||||
});
|
||||
const couchInstance = createStructuralOneShotReplicator();
|
||||
const objectStorageInstance = createStructuralOneShotReplicator();
|
||||
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
||||
throw new Error("CouchDB OneShot is unavailable");
|
||||
}
|
||||
if (objectStorage.unattendedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
||||
throw new Error("Object Storage OneShot is unavailable");
|
||||
}
|
||||
|
||||
const couchOutcome = await couchDB.userInitiatedOneShot.run(couchInstance, setting, {
|
||||
trigger: "manual",
|
||||
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
});
|
||||
const objectStorageOutcome = await objectStorage.unattendedOneShot.run(objectStorageInstance, setting, {
|
||||
trigger: "resume",
|
||||
interaction: NO_INTERACTION,
|
||||
});
|
||||
|
||||
expect.soft(couchOutcome).toBe(REPLICATION_COMPLETED);
|
||||
expect.soft(objectStorageOutcome).toBe(REPLICATION_COMPLETED);
|
||||
expect(couchInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, true);
|
||||
expect(objectStorageInstance.openOneShotReplicationWithOutcome).toHaveBeenCalledWith(setting, false);
|
||||
});
|
||||
|
||||
it("rejects a one-shot adapter whose Replicator does not declare the required operation", async () => {
|
||||
const definitions = createCentralReplicatorProviderDefinitions({} as never);
|
||||
const couchDB = definitions.get(REMOTE_COUCHDB)!;
|
||||
const setting = createNewVaultSettings();
|
||||
const incompleteInstance = {
|
||||
initializeDatabaseForReplication: vi.fn(async () => true),
|
||||
openReplication: vi.fn(async () => true),
|
||||
terminateSync: vi.fn(),
|
||||
closeReplication: vi.fn(),
|
||||
};
|
||||
if (couchDB.userInitiatedOneShot.kind !== CAPABILITY_SUPPORT_KINDS.SUPPORTED) {
|
||||
throw new Error("CouchDB OneShot is unavailable");
|
||||
}
|
||||
|
||||
const outcome = await couchDB.userInitiatedOneShot.run(incompleteInstance, setting, {
|
||||
trigger: "manual",
|
||||
interaction: USER_INITIATED_REPLICATION_AUTHORITY,
|
||||
});
|
||||
|
||||
expect(outcome.status).toBe("failed");
|
||||
expect(incompleteInstance.openReplication).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { RemoteDBSettings } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||
import type { SecuritySeedResourceFactory } from "@vrtmrz/livesync-commonlib/replication";
|
||||
import {
|
||||
LiveSyncCouchDBReplicator,
|
||||
@@ -5,12 +6,17 @@ import {
|
||||
} from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||
import { LiveSyncJournalReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicator";
|
||||
import type { LiveSyncJournalReplicatorEnv } from "@vrtmrz/livesync-commonlib/compat/replication/journal/LiveSyncJournalReplicatorEnv";
|
||||
import { createReplicatorDisposer, snapshotRemoteSettings } from "./shared";
|
||||
import { createReplicatorDisposer, snapshotRemoteSettings, type ResourceReplicator } from "./shared";
|
||||
|
||||
export type SecuritySeedResourceHost = LiveSyncCouchDBReplicatorEnv & LiveSyncJournalReplicatorEnv;
|
||||
|
||||
/** Minimal private Replicator surface required by a Security Seed resource. */
|
||||
interface SecuritySeedReplicator extends ResourceReplicator {
|
||||
getReplicationPBKDF2Salt(setting: RemoteDBSettings, refresh?: boolean): Promise<Uint8Array<ArrayBuffer>>;
|
||||
}
|
||||
|
||||
function createSecuritySeedResourceFactory(
|
||||
createReplicator: () => LiveSyncCouchDBReplicator | LiveSyncJournalReplicator
|
||||
createReplicator: () => SecuritySeedReplicator
|
||||
): SecuritySeedResourceFactory {
|
||||
return (setting) => {
|
||||
const snapshot = snapshotRemoteSettings(setting);
|
||||
|
||||
Reference in New Issue
Block a user