Files
skills/docs/security-stack/security/vault-sync/src/patch/patch.md
2026-05-29 08:20:56 +01:00

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/diff
  • applyPatch(item, patch) — applies and re-validates against VaultItemSchema
  • 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_ms per 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 when before deeply equals after. Internally serialises both via JSON.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 through VaultItemSchema. Throws VaultSyncError(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/diff for 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.