Skip to content

Commit 741b7f4

Browse files
committed
feat(mutable): support mutable mode
1 parent b847c63 commit 741b7f4

File tree

2 files changed

+480
-29
lines changed

2 files changed

+480
-29
lines changed

src/index.ts

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ export type TravelsOptions<F extends boolean, A extends boolean> = {
2929
* Whether to automatically archive the current state, by default `true`
3030
*/
3131
autoArchive?: A;
32+
/**
33+
* Whether to mutate the state in place (for observable state like MobX, Vue, Pinia)
34+
* When true, apply patches directly to the existing state object
35+
* When false (default), create new immutable state objects
36+
* @default false
37+
*/
38+
mutable?: boolean;
3239
} & Omit<MutativeOptions<true, F>, 'enablePatches'>;
3340

3441
type InitialValue<I extends any> = I extends (...args: any) => infer R ? R : I;
@@ -112,6 +119,7 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
112119
private initialPosition: number;
113120
private initialPatches?: TravelPatches;
114121
private autoArchive: A;
122+
private mutable: boolean;
115123
private options: Omit<MutativeOptions<true, F>, 'enablePatches'>;
116124
private listeners: Set<Listener<S>> = new Set();
117125
private pendingState: S | null = null;
@@ -122,6 +130,7 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
122130
initialPatches,
123131
initialPosition = 0,
124132
autoArchive = true as A,
133+
mutable = false,
125134
...mutativeOptions
126135
} = options;
127136

@@ -158,12 +167,16 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
158167
}
159168

160169
this.state = initialState;
161-
this.initialState = initialState;
170+
// For mutable mode, deep clone initialState to prevent mutations
171+
this.initialState = mutable
172+
? JSON.parse(JSON.stringify(initialState))
173+
: initialState;
162174
this.position = initialPosition;
163175
this.initialPosition = initialPosition;
164176
this.maxHistory = maxHistory;
165177
this.initialPatches = initialPatches;
166178
this.autoArchive = autoArchive;
179+
this.mutable = mutable;
167180
this.options = mutativeOptions;
168181
this.allPatches = cloneTravelPatches(initialPatches);
169182
this.tempPatches = cloneTravelPatches();
@@ -200,20 +213,51 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
200213
* Update the state
201214
*/
202215
public setState(updater: Updater<S>): void {
203-
const [nextState, patches, inversePatches] = (
204-
typeof updater === 'function'
205-
? create(this.state, updater as any, {
206-
...this.options,
207-
enablePatches: true,
208-
})
209-
: create(this.state, () => updater, {
210-
...this.options,
211-
enablePatches: true,
212-
})
213-
) as [S, Patches<true>, Patches<true>];
214-
215-
this.state = nextState;
216-
this.pendingState = nextState;
216+
let patches: Patches<true>;
217+
let inversePatches: Patches<true>;
218+
219+
if (this.mutable) {
220+
// For observable state: generate patches then apply mutably
221+
const isFn = typeof updater === 'function';
222+
223+
[, patches, inversePatches] = create(
224+
this.state,
225+
isFn
226+
? (updater as any)
227+
: (draft: any) => {
228+
// For non-function updater, assign all properties to draft
229+
Object.assign(draft, updater);
230+
},
231+
{
232+
...this.options,
233+
enablePatches: true,
234+
}
235+
) as [S, Patches<true>, Patches<true>];
236+
237+
// Apply patches to mutate the existing state object
238+
apply(this.state as object, patches, { mutable: true });
239+
240+
// Keep the same reference
241+
this.pendingState = this.state;
242+
} else {
243+
// For immutable state: create new object
244+
const [nextState, p, ip] = (
245+
typeof updater === 'function'
246+
? create(this.state, updater as any, {
247+
...this.options,
248+
enablePatches: true,
249+
})
250+
: create(this.state, () => updater, {
251+
...this.options,
252+
enablePatches: true,
253+
})
254+
) as [S, Patches<true>, Patches<true>];
255+
256+
patches = p;
257+
inversePatches = ip;
258+
this.state = nextState;
259+
this.pendingState = nextState;
260+
}
217261

218262
// Reset pendingState asynchronously
219263
Promise.resolve().then(() => {
@@ -419,19 +463,24 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
419463
].reverse();
420464
}
421465

422-
this.state = apply(
423-
this.state as object,
424-
back
425-
? _allPatches.inversePatches
426-
.slice(-this.maxHistory)
427-
.slice(nextPosition)
428-
.flat()
429-
.reverse()
430-
: _allPatches.patches
431-
.slice(-this.maxHistory)
432-
.slice(this.position, nextPosition)
433-
.flat()
434-
) as S;
466+
const patchesToApply = back
467+
? _allPatches.inversePatches
468+
.slice(-this.maxHistory)
469+
.slice(nextPosition, this.position)
470+
.flat()
471+
.reverse()
472+
: _allPatches.patches
473+
.slice(-this.maxHistory)
474+
.slice(this.position, nextPosition)
475+
.flat();
476+
477+
if (this.mutable) {
478+
// For observable state: mutate in place
479+
apply(this.state as object, patchesToApply, { mutable: true });
480+
} else {
481+
// For immutable state: create new object
482+
this.state = apply(this.state as object, patchesToApply) as S;
483+
}
435484

436485
this.position = nextPosition;
437486
this.notify();
@@ -457,8 +506,30 @@ export class Travels<S, F extends boolean = false, A extends boolean = true> {
457506
public reset(): void {
458507
this.position = this.initialPosition;
459508
this.allPatches = cloneTravelPatches(this.initialPatches);
460-
this.state = this.initialState;
461509
this.tempPatches = cloneTravelPatches();
510+
511+
if (this.mutable) {
512+
// For observable state: mutate back to initial state
513+
// Directly mutate each property to match initial state
514+
const state = this.state as any;
515+
const initial = this.initialState as any;
516+
517+
// Remove properties that exist in current but not in initial
518+
for (const key of Object.keys(state)) {
519+
if (!(key in initial)) {
520+
delete state[key];
521+
}
522+
}
523+
524+
// Set/update all properties from initial state
525+
for (const key of Object.keys(initial)) {
526+
state[key] = initial[key];
527+
}
528+
} else {
529+
// For immutable state: reassign reference
530+
this.state = this.initialState;
531+
}
532+
462533
this.notify();
463534
}
464535

0 commit comments

Comments
 (0)