or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration-utilities.mdcore-production.mddraft-management.mdindex.mdpatches-system.md
tile.json

patches-system.mddocs/

0

# Patches System

1

2

Advanced patch tracking system for implementing undo/redo, debugging, and state synchronization features. The patches system allows you to track, serialize, and replay state changes with fine-grained detail.

3

4

## Capabilities

5

6

### enablePatches

7

8

Enables the patches plugin, which is required for all patch-related functionality.

9

10

```typescript { .api }

11

/**

12

* Enable the patches plugin for tracking changes as patches

13

* Must be called before using patch-related functions

14

*/

15

function enablePatches(): void;

16

```

17

18

This function must be called once before using `produceWithPatches`, `applyPatches`, or patch listeners with `produce`.

19

20

### applyPatches

21

22

Applies an array of Immer patches to the first argument, creating a new state with the patches applied.

23

24

```typescript { .api }

25

/**

26

* Apply an array of Immer patches to the first argument

27

* @param base - The object to apply patches to

28

* @param patches - Array of patch objects describing changes

29

* @returns New state with patches applied

30

*/

31

function applyPatches<T>(base: T, patches: readonly Patch[]): T;

32

```

33

34

**Usage Examples:**

35

36

```typescript

37

import { enablePatches, produceWithPatches, applyPatches } from "immer";

38

39

// Enable patches functionality

40

enablePatches();

41

42

const baseState = {

43

user: { name: "John", age: 30 },

44

todos: ["Learn Immer", "Use patches"]

45

};

46

47

// Generate patches

48

const [nextState, patches, inversePatches] = produceWithPatches(baseState, draft => {

49

draft.user.age = 31;

50

draft.todos.push("Master patches");

51

draft.user.email = "john@example.com";

52

});

53

54

console.log(patches);

55

// [

56

// { op: "replace", path: ["user", "age"], value: 31 },

57

// { op: "add", path: ["todos", 2], value: "Master patches" },

58

// { op: "add", path: ["user", "email"], value: "john@example.com" }

59

// ]

60

61

// Apply patches to recreate the same transformation

62

const recreatedState = applyPatches(baseState, patches);

63

console.log(JSON.stringify(recreatedState) === JSON.stringify(nextState)); // true

64

65

// Apply patches to different base state

66

const differentBase = {

67

user: { name: "Jane", age: 25 },

68

todos: ["Task 1"]

69

};

70

71

const transformedDifferent = applyPatches(differentBase, patches);

72

console.log(transformedDifferent);

73

// {

74

// user: { name: "Jane", age: 31, email: "john@example.com" },

75

// todos: ["Task 1", undefined, "Master patches"]

76

// }

77

78

// Apply inverse patches for undo functionality

79

const revertedState = applyPatches(nextState, inversePatches);

80

console.log(JSON.stringify(revertedState) === JSON.stringify(baseState)); // true

81

```

82

83

### Patch Listener Integration

84

85

Patch listeners can be used with `produce` and `finishDraft` to track changes without using `produceWithPatches`.

86

87

```typescript

88

import { produce, finishDraft, createDraft, enablePatches } from "immer";

89

90

enablePatches();

91

92

const state = { count: 0, items: [] as string[] };

93

94

// Using patch listener with produce

95

const updatedState = produce(

96

state,

97

draft => {

98

draft.count += 1;

99

draft.items.push("new item");

100

},

101

(patches, inversePatches) => {

102

console.log("Changes made:", patches);

103

console.log("To undo:", inversePatches);

104

}

105

);

106

107

// Using patch listener with finishDraft

108

const draft = createDraft(state);

109

draft.count = 10;

110

draft.items.push("manual item");

111

112

const result = finishDraft(draft, (patches, inversePatches) => {

113

// Log or store patches for later use

114

console.log("Manual draft changes:", patches);

115

});

116

```

117

118

## Patch Structure

119

120

```typescript { .api }

121

interface Patch {

122

/** The type of operation: add, remove, or replace */

123

op: "replace" | "remove" | "add";

124

/** Path to the changed value as array of keys/indices */

125

path: (string | number)[];

126

/** The new value (undefined for remove operations) */

127

value?: any;

128

}

129

130

type PatchListener = (patches: Patch[], inversePatches: Patch[]) => void;

131

```

132

133

### Patch Operation Types

134

135

**Add Operations:**

136

```typescript

137

// Adding array element

138

{ op: "add", path: ["items", 2], value: "new item" }

139

140

// Adding object property

141

{ op: "add", path: ["user", "email"], value: "user@example.com" }

142

143

// Adding nested property

144

{ op: "add", path: ["config", "settings", "theme"], value: "dark" }

145

```

146

147

**Replace Operations:**

148

```typescript

149

// Replacing primitive value

150

{ op: "replace", path: ["count"], value: 42 }

151

152

// Replacing array element

153

{ op: "replace", path: ["items", 0], value: "updated item" }

154

155

// Replacing nested object property

156

{ op: "replace", path: ["user", "profile", "name"], value: "New Name" }

157

```

158

159

**Remove Operations:**

160

```typescript

161

// Removing array element

162

{ op: "remove", path: ["items", 1] }

163

164

// Removing object property

165

{ op: "remove", path: ["user", "temporaryData"] }

166

167

// Removing nested property

168

{ op: "remove", path: ["config", "deprecated", "oldSetting"] }

169

```

170

171

## Advanced Patch Usage

172

173

### Undo/Redo System

174

175

```typescript

176

import { enablePatches, produce, applyPatches, Patch } from "immer";

177

178

enablePatches();

179

180

class UndoRedoStore<T> {

181

private history: T[] = [];

182

private patches: Patch[][] = [];

183

private inversePatches: Patch[][] = [];

184

private currentIndex = -1;

185

186

constructor(private initialState: T) {

187

this.history.push(initialState);

188

this.currentIndex = 0;

189

}

190

191

get current(): T {

192

return this.history[this.currentIndex];

193

}

194

195

update(updater: (draft: any) => void): T {

196

const [nextState, patches, inversePatches] = produceWithPatches(

197

this.current,

198

updater

199

);

200

201

// Remove any future history when making new changes

202

this.history = this.history.slice(0, this.currentIndex + 1);

203

this.patches = this.patches.slice(0, this.currentIndex);

204

this.inversePatches = this.inversePatches.slice(0, this.currentIndex);

205

206

// Add new state and patches

207

this.history.push(nextState);

208

this.patches.push(patches);

209

this.inversePatches.push(inversePatches);

210

this.currentIndex++;

211

212

return nextState;

213

}

214

215

undo(): T | null {

216

if (this.currentIndex <= 0) return null;

217

218

const patches = this.inversePatches[this.currentIndex - 1];

219

const prevState = applyPatches(this.current, patches);

220

this.currentIndex--;

221

222

return prevState;

223

}

224

225

redo(): T | null {

226

if (this.currentIndex >= this.history.length - 1) return null;

227

228

const patches = this.patches[this.currentIndex];

229

const nextState = applyPatches(this.current, patches);

230

this.currentIndex++;

231

232

return nextState;

233

}

234

235

canUndo(): boolean {

236

return this.currentIndex > 0;

237

}

238

239

canRedo(): boolean {

240

return this.currentIndex < this.history.length - 1;

241

}

242

}

243

244

// Usage

245

const store = new UndoRedoStore({ todos: [], count: 0 });

246

247

// Make changes

248

store.update(draft => {

249

draft.todos.push("Task 1");

250

draft.count = 1;

251

});

252

253

store.update(draft => {

254

draft.todos.push("Task 2");

255

draft.count = 2;

256

});

257

258

console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }

259

260

// Undo

261

store.undo();

262

console.log(store.current); // { todos: ["Task 1"], count: 1 }

263

264

// Redo

265

store.redo();

266

console.log(store.current); // { todos: ["Task 1", "Task 2"], count: 2 }

267

```

268

269

### State Synchronization

270

271

```typescript

272

import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";

273

274

enablePatches();

275

276

class StateSynchronizer<T> {

277

private listeners: Array<(patches: Patch[]) => void> = [];

278

279

constructor(private state: T) {}

280

281

// Subscribe to state changes

282

subscribe(listener: (patches: Patch[]) => void): () => void {

283

this.listeners.push(listener);

284

return () => {

285

const index = this.listeners.indexOf(listener);

286

if (index > -1) this.listeners.splice(index, 1);

287

};

288

}

289

290

// Update local state and notify listeners

291

update(updater: (draft: any) => void): T {

292

const [nextState, patches] = produceWithPatches(this.state, updater);

293

294

if (patches.length > 0) {

295

this.state = nextState;

296

this.listeners.forEach(listener => listener(patches));

297

}

298

299

return nextState;

300

}

301

302

// Apply patches from remote source

303

applyRemotePatches(patches: Patch[]): T {

304

this.state = applyPatches(this.state, patches);

305

return this.state;

306

}

307

308

getCurrentState(): T {

309

return this.state;

310

}

311

}

312

313

// Usage for real-time collaboration

314

const localSync = new StateSynchronizer({

315

document: { title: "Shared Doc", content: "" },

316

users: [] as string[]

317

});

318

319

// Send patches to remote when local changes occur

320

const unsubscribe = localSync.subscribe(patches => {

321

// In real app, send patches to server/other clients

322

console.log("Sending patches to remote:", patches);

323

// websocket.send(JSON.stringify({ type: 'patches', patches }));

324

});

325

326

// Apply changes locally

327

localSync.update(draft => {

328

draft.document.title = "Collaborative Document";

329

draft.users.push("Alice");

330

});

331

332

// Simulate receiving remote patches

333

const remotePatches: Patch[] = [

334

{ op: "replace", path: ["document", "content"], value: "Hello world!" },

335

{ op: "add", path: ["users", 1], value: "Bob" }

336

];

337

338

localSync.applyRemotePatches(remotePatches);

339

console.log(localSync.getCurrentState());

340

// {

341

// document: { title: "Collaborative Document", content: "Hello world!" },

342

// users: ["Alice", "Bob"]

343

// }

344

```

345

346

### Patch Serialization and Storage

347

348

```typescript

349

import { enablePatches, produceWithPatches, applyPatches, Patch } from "immer";

350

351

enablePatches();

352

353

class PatchLogger<T> {

354

private patchHistory: Array<{ timestamp: number; patches: Patch[] }> = [];

355

356

constructor(private initialState: T) {}

357

358

// Apply update and log patches

359

update(updater: (draft: any) => void): T {

360

const [nextState, patches] = produceWithPatches(this.initialState, updater);

361

362

if (patches.length > 0) {

363

// Store patches with timestamp

364

this.patchHistory.push({

365

timestamp: Date.now(),

366

patches: patches

367

});

368

369

this.initialState = nextState;

370

}

371

372

return nextState;

373

}

374

375

// Serialize patch history to JSON

376

exportHistory(): string {

377

return JSON.stringify({

378

initialState: this.initialState,

379

patches: this.patchHistory

380

});

381

}

382

383

// Restore from serialized patch history

384

static fromHistory<T>(serialized: string): { state: T; logger: PatchLogger<T> } {

385

const { initialState, patches } = JSON.parse(serialized);

386

387

// Replay all patches to reconstruct current state

388

let currentState = initialState;

389

for (const entry of patches) {

390

currentState = applyPatches(currentState, entry.patches);

391

}

392

393

const logger = new PatchLogger(currentState);

394

logger.patchHistory = patches;

395

396

return { state: currentState, logger };

397

}

398

399

// Get patches within time range

400

getPatchesBetween(startTime: number, endTime: number): Patch[] {

401

return this.patchHistory

402

.filter(entry => entry.timestamp >= startTime && entry.timestamp <= endTime)

403

.flatMap(entry => entry.patches);

404

}

405

}

406

407

// Usage

408

const logger = new PatchLogger({ data: [], version: 1 });

409

410

logger.update(draft => {

411

draft.data.push("item1");

412

});

413

414

logger.update(draft => {

415

draft.version = 2;

416

draft.data.push("item2");

417

});

418

419

// Export and restore

420

const serialized = logger.exportHistory();

421

const { state, logger: restoredLogger } = PatchLogger.fromHistory(serialized);

422

423

console.log(state); // { data: ["item1", "item2"], version: 2 }

424

```

425

426

The patches system provides powerful capabilities for change tracking, state synchronization, and building complex state management patterns with Immer.