Sync ​

Sync Engine's sync pipeline is extensible at three points: sync trigger options, decision-making, and conflict resolution. For the internal sync pipeline architecture, see deep dive: sync.

Sync Trigger ​

Every sync run is launched with a trigger name and a set of options. A TriggerEntry is registered under a trigger name and supplies the options factory; when the scheduler flushes a batch of sync requests, it selects the entry with the highest priority among the batch's trigger names and passes its options() to the run.

ts
type TriggerEntry = { priority: number; options?: () => SyncOptions };
ts
ctx.registerTrigger(key: string, entry: TriggerEntry): () => boolean;

SyncOptions customizes one run. Unset fields fall back to defaults: the selected conflict resolver, the configured strategy rules, move detection on, and no confirmations.

ts
type SyncOptions = {
  remoteLister?: RemoteLister;
  conflictResolver?: ConflictResolver;
  detectMoves?: boolean;
  needConfirmTasks?: boolean;
  needConfirmDeletion?: boolean;
  syncStrategy?: Array<GlobStrategy>;
};

GlobStrategy is { expr: string; strategy: string }, where strategy is a registered decider ID or none. Supplying syncStrategy replaces the configured rules for that run; it does not merge with them.

The plugin registers built-in entries: realtime (priority 1000), interval (2000), focus (3000), startup (4000), nonInteractiveManual (4990), and manual (5000). Registering under an existing name replaces the entry.

Remote Lister ​

SyncOptions.remoteLister supplies the remote file listing instead of a fresh traversal.

ts
type RemoteLister = (info: Infras & { reporter: ListReporter }) => MaybePromise<Array<Stat>>;

Infras is { localFs: Fs; remoteFs: Fs; record: RecordStore }. The reporter must be passed through to remoteFs.list() calls. For how trigger options integrate with the sync flow, see deep dive: sync.

Registering a Trigger Entry ​

See registration.

Decider ​

A Decider compares local stats, remote stats, and prior records to produce sync tasks for one strategy.

ts
type DeciderInput = {
  localStats: StatsMap;
  remoteStats: StatsMap;
  records: RecordStatsMap;
  taskFactory: TaskFactory;
};

type Decider = (input: DeciderInput) => Array<Task>;

Stats are bucketed by strategy name before planning; decideTasks invokes the registered decider once per bucket that has entries. Each registered decider ID that supplies a prettyName becomes a selectable strategy in Sync Strategy rules. Use taskFactory instead of constructing task classes directly since their constructors require internal sync infrastructure. Built-in deciders include bidirectional, mirrorLocal, and mirrorRemote; see deep dive: sync for their behavior.

Examples: bidirectional decider and mirror deciders.

Registering a Decider ​

See registration.

Conflict Resolver ​

A ConflictResolver handles files that conflict (both sides changed since last sync).

ts
type ConflictResolverPayload = {
  local: FileStat;
  remote: FileStat;
  key: string;
  localFs: Fs;
  remoteFs: Fs;
  record: RecordStore;
};

type ConflictResolver = (payload: ConflictResolverPayload) => MaybePromise<void>;
ts
import { pipe } from '@hesprs/sync-engine-sdk';

// Simple resolver that writes remote content to local
const resolver: ConflictResolver = async ({ key, localFs, remoteFs, remote }) => {
  await pipe({ from: remoteFs, to: LocalFs, key, fileStat: remote });
};

Registering a Conflict Resolver ​

See registration.

Transfer Utilities ​

Three helpers exported from @hesprs/sync-engine-sdk for copying file content between filesystems with automatic size-based streaming.

pipe ​

ts
import { pipe } from '@hesprs/sync-engine-sdk';

function pipe(options: { from: Fs; to: Fs; key: string; stat: FileStat }): Promise<void>;

Reads a file from from and writes it to to under the same key. Automatically selects buffered or streaming mode based on file size (device-adaptive). Silently succeeds if the source file does not exist (swallows TOCTOU 404 / ENOENT errors).

ts
await pipe({ from: remoteFs, to: localFs, key: 'folder/note.md', stat });

readWithSize ​

ts
function readWithSize(
  fs: Fs,
  key: string,
  stat: FileStat,
): MaybePromise<Binary | ReadableStream<Binary> | undefined>;

Reads a file, choosing readStream for files larger than 2.5 MiB and read otherwise. Returns undefined if the file does not exist. The stat parameter is forwarded to the chosen Fs method as required by the interface.

writeWithValue ​

ts
function writeWithValue(
  fs: Fs,
  key: string,
  value: Binary | ReadableStream<Binary>,
  stat: FileStat,
): MaybePromise<string>;

Writes a value to fs, choosing writeStream for ReadableStream inputs and write for Binary inputs. Returns the uid from the underlying write call. The stat parameter is forwarded as required by the interface.

Task Types ​

All task types extend BaseTask and are type-only exports. TaskFactory accepts task-specific options; every task requires key.

ts
type TaskNames =
  | 'addRecord'
  | 'removeRecord'
  | 'createLocalDir'
  | 'createRemoteDir'
  | 'download'
  | 'resolveConflict'
  | 'removeLocal'
  | 'removeRemote'
  | 'upload'
  | 'moveLocal'
  | 'moveRemote';

type TaskFactory = <N extends TaskNames>(
  name: N,
  options: TaskOptionsMap[N],
) => InstanceType<(typeof taskMap)[N]>;
TypeRequired options beyond keyOperation
AddRecordlocal: Stat, remote: StatCreates record for both sides. File records contain both UIDs; directory records contain isDir.
RemoveRecordNoneDeletes sync record without changing either filesystem.
Downloadremote: FileStatCopies remote file to local filesystem and records both UIDs.
Uploadlocal: FileStatCopies local file to remote filesystem and records both UIDs.
CreateLocalDirremote: FolderStatCreates local directory and its sync record.
CreateRemoteDirlocal: FolderStatCreates remote directory and its sync record.
RemoveLocallocal: StatDeletes local path and its sync record.
RemoveRemoteremote: StatDeletes remote path and its sync record.
MoveLocalnewKey: stringMoves local path from key to newKey and moves its record.
MoveRemotenewKey: stringMoves remote path from key to newKey and moves its record.
ResolveConflictlocal: FileStat, remote: FileStat, resolver: ConflictResolverInvokes resolver with both file states and sync infrastructure.

SyncTerminateReason ​

This is the return type of ctx.requestSync, ctx.executeSync, and the payload of syncTerminated event.

ts
type SyncTerminateReason =
  | { result: 'cancelled' }
  | { result: 'completed' }
  | { result: 'failed'; error: Error }
  | { result: 'noop' };

All content licensed under the CC BY 4.0 License.