JSON Patches Examples
Examples for working with JSON Patches (RFC 6902) in Craft.
Performance
Craft's patch application is 24x faster than immer!
Generating Patches
Basic Patches
typescript
import { craftWithPatches } from "@sylphx/craft";
const state = {
count: 0,
user: { name: "Alice" }
};
const [nextState, patches, inversePatches] = craftWithPatches(state, draft => {
draft.count = 5;
draft.user.name = "Bob";
});
console.log(patches);
// [
// { op: 'replace', path: ['count'], value: 5 },
// { op: 'replace', path: ['user', 'name'], value: 'Bob' }
// ]
console.log(inversePatches);
// [
// { op: 'replace', path: ['count'], value: 0 },
// { op: 'replace', path: ['user', 'name'], value: 'Alice' }
// ]Array Patches
typescript
const state = { items: [1, 2, 3] };
const [next, patches] = craftWithPatches(state, draft => {
draft.items.push(4);
draft.items[0] = 10;
});
console.log(patches);
// [
// { op: 'replace', path: ['items', 0], value: 10 },
// { op: 'add', path: ['items', 3], value: 4 }
// ]Nested Patches
typescript
const state = {
user: {
profile: {
name: "Alice",
age: 25
}
}
};
const [next, patches] = craftWithPatches(state, draft => {
draft.user.profile.age = 26;
});
console.log(patches);
// [
// { op: 'replace', path: ['user', 'profile', 'age'], value: 26 }
// ]Applying Patches
Basic Application
typescript
import { applyPatches } from "@sylphx/craft";
const state = { count: 0, name: "Alice" };
const patches = [
{ op: 'replace', path: ['count'], value: 5 },
{ op: 'replace', path: ['name'], value: 'Bob' }
];
const next = applyPatches(state, patches);
console.log(next); // { count: 5, name: "Bob" }Roundtrip
typescript
// Generate patches
const [next, patches, inversePatches] = craftWithPatches(state, draft => {
draft.count = 10;
});
// Recreate using patches
const recreated = applyPatches(state, patches);
console.log(recreated.count === next.count); // true
// Undo using inverse patches
const reverted = applyPatches(next, inversePatches);
console.log(reverted.count === state.count); // trueUndo/Redo System
Basic Undo/Redo
typescript
import { craftWithPatches, applyPatches, type Patch } from "@sylphx/craft";
class UndoRedo<T> {
private past: Patch[][] = [];
private future: Patch[][] = [];
constructor(private state: T) {}
getState(): T {
return this.state;
}
update(producer: (draft: any) => void): T {
const [next, _, inversePatches] = craftWithPatches(
this.state,
producer
);
this.past.push(inversePatches);
this.future = []; // Clear redo stack
this.state = next;
return next;
}
canUndo(): boolean {
return this.past.length > 0;
}
canRedo(): boolean {
return this.future.length > 0;
}
undo(): T {
const patches = this.past.pop();
if (!patches) return this.state;
const next = applyPatches(this.state, patches);
// Generate patches for redo
const [_, __, redoPatches] = craftWithPatches(
this.state,
() => next
);
this.future.push(redoPatches);
this.state = next;
return next;
}
redo(): T {
const patches = this.future.pop();
if (!patches) return this.state;
const next = applyPatches(this.state, patches);
// Generate patches for undo
const [_, __, undoPatches] = craftWithPatches(
this.state,
() => next
);
this.past.push(undoPatches);
this.state = next;
return next;
}
clear(): void {
this.past = [];
this.future = [];
}
}
// Usage
interface State {
count: number;
text: string;
}
const history = new UndoRedo<State>({ count: 0, text: "" });
history.update(draft => { draft.count = 1; });
history.update(draft => { draft.count = 2; });
history.update(draft => { draft.text = "hello"; });
console.log(history.getState()); // { count: 2, text: "hello" }
history.undo();
console.log(history.getState()); // { count: 2, text: "" }
history.undo();
console.log(history.getState()); // { count: 1, text: "" }
history.redo();
console.log(history.getState()); // { count: 2, text: "" }Advanced Undo/Redo with History Limits
typescript
class LimitedHistory<T> {
private past: Patch[][] = [];
private future: Patch[][] = [];
private maxHistorySize: number;
constructor(
private state: T,
maxHistorySize: number = 50
) {
this.maxHistorySize = maxHistorySize;
}
update(producer: (draft: any) => void): T {
const [next, _, inversePatches] = craftWithPatches(
this.state,
producer
);
this.past.push(inversePatches);
// Limit history size
if (this.past.length > this.maxHistorySize) {
this.past.shift();
}
this.future = [];
this.state = next;
return next;
}
undo(): T {
if (this.past.length === 0) return this.state;
const patches = this.past.pop()!;
const next = applyPatches(this.state, patches);
const [_, __, redoPatches] = craftWithPatches(
this.state,
() => next
);
this.future.push(redoPatches);
this.state = next;
return next;
}
redo(): T {
if (this.future.length === 0) return this.state;
const patches = this.future.pop()!;
const next = applyPatches(this.state, patches);
const [_, __, undoPatches] = craftWithPatches(
this.state,
() => next
);
this.past.push(undoPatches);
this.state = next;
return next;
}
getHistoryInfo() {
return {
canUndo: this.past.length > 0,
canRedo: this.future.length > 0,
undoCount: this.past.length,
redoCount: this.future.length
};
}
}State Synchronization
Client-Server Sync
typescript
interface Message {
type: 'update';
patches: Patch[];
}
class SyncedState<T> {
constructor(
private state: T,
private sendToServer: (msg: Message) => void
) {}
update(producer: (draft: any) => void): T {
const [next, patches] = craftWithPatches(this.state, producer);
// Send patches to server
this.sendToServer({
type: 'update',
patches
});
this.state = next;
return next;
}
applyServerUpdate(patches: Patch[]): T {
this.state = applyPatches(this.state, patches);
return this.state;
}
}
// Usage
const syncedState = new SyncedState(initialState, (msg) => {
websocket.send(JSON.stringify(msg));
});
// Client update
syncedState.update(draft => {
draft.count++;
});
// Server update received
websocket.onmessage = (event) => {
const msg = JSON.parse(event.data);
syncedState.applyServerUpdate(msg.patches);
};Collaborative Editing
typescript
class CollaborativeState<T> {
private localVersion = 0;
private serverVersion = 0;
constructor(
private state: T,
private clientId: string,
private sendPatch: (patch: any) => void
) {}
localUpdate(producer: (draft: any) => void): T {
const [next, patches] = craftWithPatches(this.state, producer);
this.localVersion++;
// Send to server with version info
this.sendPatch({
clientId: this.clientId,
version: this.localVersion,
patches
});
this.state = next;
return next;
}
applyRemoteUpdate(patches: Patch[], fromVersion: number): T {
if (fromVersion !== this.serverVersion) {
console.warn("Version mismatch - need conflict resolution");
return this.state;
}
this.state = applyPatches(this.state, patches);
this.serverVersion++;
return this.state;
}
}Audit Logging
Change Tracking
typescript
interface ChangeLog {
timestamp: number;
userId: string;
patches: Patch[];
}
class AuditableState<T> {
private changelog: ChangeLog[] = [];
constructor(private state: T) {}
update(producer: (draft: any) => void, userId: string): T {
const [next, patches] = craftWithPatches(this.state, producer);
// Log the change
this.changelog.push({
timestamp: Date.now(),
userId,
patches
});
this.state = next;
return next;
}
getChangelog(): ChangeLog[] {
return [...this.changelog];
}
replayHistory(): T[] {
const states: T[] = [this.state];
let current = this.state;
for (const log of this.changelog) {
current = applyPatches(current, log.patches);
states.push(current);
}
return states;
}
getChangesByUser(userId: string): ChangeLog[] {
return this.changelog.filter(log => log.userId === userId);
}
}Detailed Change Tracking
typescript
class ChangeTracker<T> {
private changes: Array<{
timestamp: number;
patches: Patch[];
inversePatches: Patch[];
metadata?: any;
}> = [];
constructor(private state: T) {}
update(
producer: (draft: any) => void,
metadata?: any
): T {
const [next, patches, inversePatches] = craftWithPatches(
this.state,
producer
);
this.changes.push({
timestamp: Date.now(),
patches,
inversePatches,
metadata
});
this.state = next;
return next;
}
getChangeSummary() {
return {
totalChanges: this.changes.length,
firstChange: this.changes[0]?.timestamp,
lastChange: this.changes[this.changes.length - 1]?.timestamp,
patches: this.changes.flatMap(c => c.patches)
};
}
exportChanges() {
return JSON.stringify(this.changes);
}
importChanges(json: string) {
const changes = JSON.parse(json);
for (const change of changes) {
this.state = applyPatches(this.state, change.patches);
this.changes.push(change);
}
}
}Optimistic Updates
With Rollback
typescript
class OptimisticState<T> {
private pendingPatches: Map<string, Patch[]> = new Map();
constructor(private state: T) {}
async optimisticUpdate(
producer: (draft: any) => void,
apiCall: () => Promise<void>
): Promise<T> {
const requestId = crypto.randomUUID();
// Apply optimistically
const [next, _, inversePatches] = craftWithPatches(
this.state,
producer
);
this.pendingPatches.set(requestId, inversePatches);
this.state = next;
try {
await apiCall();
this.pendingPatches.delete(requestId);
return this.state;
} catch (error) {
// Rollback on error
const patches = this.pendingPatches.get(requestId);
if (patches) {
this.state = applyPatches(this.state, patches);
this.pendingPatches.delete(requestId);
}
throw error;
}
}
}
// Usage
const state = new OptimisticState({ count: 0 });
await state.optimisticUpdate(
draft => { draft.count++; },
() => api.incrementCounter()
);Next Steps
- Async - Async operations
- Composition - Reusable updaters
- API Reference - Patch API docs