mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 00:46:45 +00:00
refactor(sync-core): add ui and config port contracts
This commit is contained in:
parent
a59aa39931
commit
fd5a0b6945
4 changed files with 125 additions and 4 deletions
|
|
@ -156,8 +156,9 @@ Current extraction state and remaining immediate debt:
|
|||
- PR 4a is present with `packages/sync-core/src/ports.ts`. The package now
|
||||
exports minimal contracts for operation application, action dispatch,
|
||||
remote-apply windows, deferred local action flushing, archive side effects,
|
||||
and operation-store persistence. The existing Angular services satisfy those
|
||||
contracts app-side.
|
||||
operation-store persistence, conflict UI, and sync configuration. The existing
|
||||
Angular services satisfy the replay/storage contracts app-side; conflict UI
|
||||
and sync config are currently type-only contracts for future adapters.
|
||||
- PR 4b's current small helper set is present: remote-apply crash-safety
|
||||
ordering, upload last-server-sequence planning, full-state snapshot upload
|
||||
follow-up partitioning, download gap/full-state/encryption planning, and
|
||||
|
|
@ -669,8 +670,8 @@ Introduce orchestration ports without moving the orchestrators yet. This reduces
|
|||
the risk of the later service moves.
|
||||
|
||||
Status: implemented for the current branch slice. `@sp/sync-core` exports the
|
||||
first minimal port contracts, and these app services now explicitly satisfy
|
||||
them:
|
||||
first minimal replay/storage port contracts, and these app services now
|
||||
explicitly satisfy them:
|
||||
|
||||
- `OperationApplierService` implements `OperationApplyPort<Operation>` and uses
|
||||
`ActionDispatchPort<SyncActionLike>` for its NgRx dispatch seam.
|
||||
|
|
@ -680,6 +681,10 @@ them:
|
|||
- `OperationLogStoreService` implements
|
||||
`OperationStorePort<Operation, OperationLogEntry>`.
|
||||
|
||||
`ConflictUiPort` and `SyncConfigPort` are also exported as type-only package
|
||||
contracts. Runtime Angular adapters are intentionally still pending because no
|
||||
orchestration has moved behind them yet.
|
||||
|
||||
This is contract-only: NgRx dispatch, hydration windows, archive IndexedDB
|
||||
handling, and deferred local action processing remain app-side.
|
||||
|
||||
|
|
|
|||
|
|
@ -119,11 +119,18 @@ export type {
|
|||
export type {
|
||||
ActionDispatchPort,
|
||||
ArchiveSideEffectPort,
|
||||
ConflictUiDialogRequest,
|
||||
ConflictUiNotification,
|
||||
ConflictUiNotificationSeverity,
|
||||
ConflictUiPort,
|
||||
DeferredLocalActionsPort,
|
||||
OperationApplyPort,
|
||||
OperationStorePort,
|
||||
RemoteApplyWindowPort,
|
||||
SyncActionLike,
|
||||
SyncConfigPort,
|
||||
SyncConfigSnapshot,
|
||||
SyncPortMeta,
|
||||
} from './ports';
|
||||
|
||||
// Conflict-resolution helpers.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ApplyOperationsOptions, ApplyOperationsResult } from './apply.types';
|
||||
import type { Operation, OperationLogEntry } from './operation.types';
|
||||
|
||||
export type SyncPortMeta = Record<string, string | number | boolean | null | undefined>;
|
||||
|
||||
/**
|
||||
* Minimal action shape used at sync-core boundaries.
|
||||
*
|
||||
|
|
@ -70,3 +72,53 @@ export interface OperationStorePort<
|
|||
markSynced(seqs: number[]): Promise<void>;
|
||||
markRejected(opIds: string[]): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain-free sync configuration snapshot.
|
||||
*
|
||||
* Provider IDs stay plain strings at the package boundary. Host applications can
|
||||
* narrow them in their adapter layer.
|
||||
*/
|
||||
export interface SyncConfigSnapshot<TProviderId extends string = string> {
|
||||
isEnabled: boolean;
|
||||
syncProvider: TProviderId | null;
|
||||
isEncryptionEnabled?: boolean;
|
||||
isCompressionEnabled?: boolean;
|
||||
isManualSyncOnly?: boolean;
|
||||
syncInterval?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Port for reading host sync configuration without importing framework store
|
||||
* selectors or host provider enums.
|
||||
*/
|
||||
export interface SyncConfigPort<TProviderId extends string = string> {
|
||||
getSyncConfig(): Promise<SyncConfigSnapshot<TProviderId>>;
|
||||
}
|
||||
|
||||
export interface ConflictUiDialogRequest {
|
||||
conflictType: string;
|
||||
scenario?: string;
|
||||
reason?: string;
|
||||
counts?: Record<string, number>;
|
||||
timestamps?: Record<string, number>;
|
||||
meta?: SyncPortMeta;
|
||||
}
|
||||
|
||||
export type ConflictUiNotificationSeverity = 'info' | 'warning' | 'error';
|
||||
|
||||
export interface ConflictUiNotification {
|
||||
severity: ConflictUiNotificationSeverity;
|
||||
message: string;
|
||||
reason?: string;
|
||||
meta?: SyncPortMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Port for conflict dialogs/snacks. Resolutions are strings so the host owns
|
||||
* user-facing choices such as USE_LOCAL, USE_REMOTE, or CANCEL.
|
||||
*/
|
||||
export interface ConflictUiPort<TResolution extends string = string> {
|
||||
showConflictDialog(request: ConflictUiDialogRequest): Promise<TResolution>;
|
||||
notify?(notification: ConflictUiNotification): Promise<void> | void;
|
||||
}
|
||||
|
|
|
|||
57
packages/sync-core/tests/ports.spec.ts
Normal file
57
packages/sync-core/tests/ports.spec.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ConflictUiPort, SyncConfigPort, SyncConfigSnapshot } from '../src';
|
||||
|
||||
describe('sync-core ports', () => {
|
||||
it('keeps sync config provider IDs as host-owned strings', async () => {
|
||||
type ProviderId = 'super-sync' | 'file-sync';
|
||||
const config: SyncConfigSnapshot<ProviderId> = {
|
||||
isEnabled: true,
|
||||
syncProvider: 'super-sync',
|
||||
isEncryptionEnabled: true,
|
||||
isCompressionEnabled: false,
|
||||
isManualSyncOnly: false,
|
||||
syncInterval: 5,
|
||||
};
|
||||
const port: SyncConfigPort<ProviderId> = {
|
||||
getSyncConfig: vi.fn().mockResolvedValue(config),
|
||||
};
|
||||
|
||||
await expect(port.getSyncConfig()).resolves.toBe(config);
|
||||
expect(port.getSyncConfig).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('keeps conflict UI reasons and resolutions host-owned strings', async () => {
|
||||
type Resolution = 'USE_LOCAL' | 'USE_REMOTE' | 'CANCEL';
|
||||
const notify = vi.fn();
|
||||
const port: ConflictUiPort<Resolution> = {
|
||||
showConflictDialog: vi.fn().mockResolvedValue('USE_LOCAL'),
|
||||
notify,
|
||||
};
|
||||
|
||||
await expect(
|
||||
port.showConflictDialog({
|
||||
conflictType: 'sync-import',
|
||||
scenario: 'LOCAL_IMPORT_FILTERS_REMOTE',
|
||||
reason: 'BACKUP_RESTORE',
|
||||
counts: { filteredOps: 3 },
|
||||
timestamps: { localImport: 123 },
|
||||
meta: { providerId: 'super-sync' },
|
||||
}),
|
||||
).resolves.toBe('USE_LOCAL');
|
||||
|
||||
port.notify?.({
|
||||
severity: 'warning',
|
||||
message: 'sync-conflict',
|
||||
reason: 'BACKUP_RESTORE',
|
||||
meta: { filteredOps: 3 },
|
||||
});
|
||||
|
||||
expect(port.showConflictDialog).toHaveBeenCalledOnce();
|
||||
expect(notify).toHaveBeenCalledWith({
|
||||
severity: 'warning',
|
||||
message: 'sync-conflict',
|
||||
reason: 'BACKUP_RESTORE',
|
||||
meta: { filteredOps: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue