3.9 KiB
patch
(no submodules)
Purpose
patch.ts re-exports the RFC 6902 JSON Patch primitives provided by
@suaveplan/diff, narrowed to the VaultItem domain. It exists so the
sync engine and the offline queue can compute and apply small, structural
deltas instead of shipping whole items across the wire — saving bandwidth
on the most common case (a single field change) and making conflict
detection precise per-path. Every applied patch is re-validated against
VaultItemSchema so a malformed delta cannot poison local state.
Features
diffVaultItems(before, after)— minimal JSON Patch via@suaveplan/diffapplyPatch(item, patch)— applies and re-validates againstVaultItemSchema- Empty diff for deeply-equal inputs (no spurious operations)
- Structured
VaultSyncError(PATCH.INVALID)for bad ops and shape failures - OTel histogram
vault_sync.patch.apply.duration_msper apply call
Usage: Basic Example
import { applyPatch, diffVaultItems } from "@suaveplan/vault-sync";
const before = {
id: "i1",
title: "Old",
password: "p",
folderId: null,
history: [],
nodeId: "a",
updatedAt: 1,
clock: { a: 1 },
};
const after = { ...before, title: "New", updatedAt: 2 };
const patch = diffVaultItems(before, after);
console.log(patch);
// [{ op: "replace", path: ["title"], value: "New" },
// { op: "replace", path: ["updatedAt"], value: 2 }]
const applied = applyPatch(before, patch);
console.log(applied.title); // "New"
Usage: Advanced Example
import { applyPatch, diffVaultItems } from "@suaveplan/vault-sync";
import { VaultSyncError } from "@suaveplan/vault-sync";
import { VAULT_SYNC_CODES } from "@suaveplan/vault-sync/errors";
import type { VaultItem } from "@suaveplan/vault-sync/types";
interface Mutation {
itemId: string;
patch: ReturnType<typeof diffVaultItems>;
}
function recordMutation(before: VaultItem, after: VaultItem): Mutation {
return { itemId: before.id, patch: diffVaultItems(before, after) };
}
function safeApply(item: VaultItem, mutation: Mutation): VaultItem {
try {
return applyPatch(item, mutation.patch);
} catch (err) {
if (
err instanceof VaultSyncError &&
err.code === VAULT_SYNC_CODES["PATCH.INVALID"]
) {
// Skip this mutation; the queue will retry on the next pull.
return item;
}
throw err;
}
}
API Reference
diffVaultItems(before: VaultItem, after: VaultItem): JsonPatch— returns an RFC 6902 patch (Operation[]). Empty array whenbeforedeeply equalsafter. Internally serialises both viaJSON.parse(JSON.stringify(...))to coerce undefined-valued fields to canonical absent form.applyPatch(item: VaultItem, patch: JsonPatch): VaultItem— applies the patch and parses the result throughVaultItemSchema. ThrowsVaultSyncError(PATCH.INVALID)for: structurally-invalid patches (reason: "structural"), failures during apply (reason: "apply", e.g. missing path, out-of-bounds array index), and post-apply shape failures (reason: "shape").JsonPatch— re-exported from@suaveplan/difffor downstream consumers that want one import path for the whole patch surface.
Implementation Notes
The package leans on @suaveplan/diff rather than re-implementing patch
math. The thin wrapper here is responsible for: (a) erasing
@suaveplan/diff's richer error types into VaultSyncError, (b)
re-running VaultItemSchema after each apply, and (c) recording the
single OTel duration histogram. Re-validating after apply is paranoid but
cheap — the schema run is microseconds — and it is the only line of
defence between a malformed remote patch and corrupt local state. The
asJson helper round-trips through JSON to drop undefined values, which
is consistent with what the sync wire format does anyway.