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 decider and conflict resolver, the configured inclusion and exclusion rules, move detection on, and no confirmations.
ts
type SyncOptions = {
decider?: Decider;
remoteLister?: RemoteLister;
conflictResolver?: ConflictResolver;
detectMoves?: boolean;
needConfirmTasks?: boolean;
needConfirmDeletion?: boolean;
inclusionRules?: Array<GlobMatchRule>;
exclusionRules?: Array<GlobMatchRule>;
};The plugin registers built-in entries: realtime (priority 1000), interval (2000), startup (3000), migration (3980), nonInteractiveManual (3990), and manual (4000). 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.
ts
type DeciderInput = {
localStats: StatsMap;
remoteStats: StatsMap;
records: RecordStatsMap;
taskFactory: TaskFactory;
logger: (log: string) => void;
};
type Decider = (input: DeciderInput) => Array<BaseTask>;Use taskFactory instead of constructing task classes directly — their constructors require internal sync infrastructure. Built-in deciders include bidirectional, mirror-local, and mirror-remote; 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]>;| Type | Required options beyond key | Operation |
|---|---|---|
AddRecord | local: Stat, remote: Stat | Creates record for both sides. File records contain both UIDs; directory records contain isDir. |
RemoveRecord | None | Deletes sync record without changing either filesystem. |
Download | remote: FileStat | Copies remote file to local filesystem and records both UIDs. |
Upload | local: FileStat | Copies local file to remote filesystem and records both UIDs. |
CreateLocalDir | remote: FolderStat | Creates local directory and its sync record. |
CreateRemoteDir | local: FolderStat | Creates remote directory and its sync record. |
RemoveLocal | local: Stat | Deletes local path and its sync record. |
RemoveRemote | remote: Stat | Deletes remote path and its sync record. |
MoveLocal | oldKey: string, remote: Stat | Moves local path from oldKey to key and moves its record. |
MoveRemote | oldKey: string, local: Stat | Moves remote path from oldKey to key and moves its record. |
ResolveConflict | local: FileStat, remote: FileStat, resolver: ConflictResolver | Invokes 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: string }
| { result: 'noop' };