first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
# patch
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.