Skip to content

Commit 5f230cb

Browse files
committed
Freeze core graph state boundaries
1 parent 3761adf commit 5f230cb

3 files changed

Lines changed: 62 additions & 3 deletions

File tree

packages/core/src/graph/graph-slice.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import produce from 'immer';
2+
import produce, { freeze } from 'immer';
33
import {
44
ActionCreatorWithPayload,
55
createAction,
@@ -121,9 +121,11 @@ export function graphSliceCreator<State, Event extends string>(
121121
),
122122
};
123123

124+
const frozenGraph = freeze(graph, true);
125+
124126
return {
125127
actions,
126-
getInitialState: () => graph,
128+
getInitialState: () => frozenGraph,
127129
reduce(g, action) {
128130
switch (action.type) {
129131
case actions.addMetadata.type:
@@ -171,7 +173,7 @@ export function graphSliceCreator<State, Event extends string>(
171173
draft.current = payload.id;
172174
});
173175
case actions.load.type:
174-
return action.payload;
176+
return freeze(action.payload, true);
175177
default:
176178
return g;
177179
}

packages/core/tests/graph.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,32 @@ describe('initializeProvenanceGraph', () => {
6767
graph.update(graph.changeCurrent(graph.root.id));
6868
expect(listener).toHaveBeenCalledTimes(1);
6969
});
70+
71+
it('freezes the initial state so external mutation does not change the root snapshot', () => {
72+
const initialState = {
73+
count: 0,
74+
nested: {
75+
value: 1,
76+
},
77+
};
78+
const graph = initializeProvenanceGraph<typeof initialState, 'increment'>(
79+
initialState
80+
);
81+
82+
expect(Object.isFrozen(initialState)).toBe(true);
83+
expect(Object.isFrozen(initialState.nested)).toBe(true);
84+
85+
expect(() => {
86+
initialState.nested.value = 2;
87+
}).toThrow(TypeError);
88+
89+
expect(
90+
(
91+
graph.root.state as {
92+
type: 'checkpoint';
93+
val: typeof initialState;
94+
}
95+
).val.nested.value
96+
).toBe(1);
97+
});
7098
});

packages/core/tests/import_export.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,33 @@ describe('Export', () => {
101101
expect(imported.current.id).toBe(exportObject.root);
102102
await expect(imported.to(orphan.id)).rejects.toThrow();
103103
});
104+
105+
it('freezes imported graph objects so later external mutation cannot rewrite the store', async () => {
106+
const { trrack, add } = setup();
107+
108+
await trrack.apply('Add', add(2));
109+
110+
const exportObject = trrack.exportObject();
111+
const rootNode = exportObject.nodes[exportObject.root] as {
112+
state: {
113+
type: 'checkpoint';
114+
val: {
115+
counter: number;
116+
};
117+
};
118+
};
119+
const { trrack: imported } = setup();
120+
imported.importObject(exportObject);
121+
122+
expect(Object.isFrozen(exportObject)).toBe(true);
123+
expect(Object.isFrozen(exportObject.nodes)).toBe(true);
124+
expect(Object.isFrozen(rootNode.state.val)).toBe(true);
125+
expect(imported.getState().counter).toBe(2);
126+
127+
expect(() => {
128+
rootNode.state.val.counter = 99;
129+
}).toThrow(TypeError);
130+
131+
expect(imported.getState().counter).toBe(2);
132+
});
104133
});

0 commit comments

Comments
 (0)