+ FluidManipulationClient {
	tmpJSONFilename{
		^Platform.defaultTempDir++"tmp_fluid_data_"++
		Date.localtime.stamp++"_"++UniqueID.next++".json";
	}

	dump {|action|
		var filename = this.tmpJSONFilename;
		action ?? {action = postit};
			this.write(filename, {
				action.value(this.parseJSON(File.readAllString(filename)));
				File.delete(filename);
			});
	}

	load{|dict, action|
		var filename = this.tmpJSONFilename;
		File.use(filename, "wt", { |f| f.write(this.asJSON(dict));});
		this.read(filename, {
				action.notNil.if{ action.value; };
				File.delete(filename);
		});
	}

	toDict{|obj|
		var converted;
		if(obj.class === Event){
			converted = obj.as(Dictionary);
			converted.keysValuesChange{|k,v|this.toDict(v)}
			^converted;
		};
		if(obj.class === Array){
			converted = obj.collect{|v| this.toDict(v)};
			^converted;
		};
		^obj;
	}

	parseJSON{|jsonStr|
		var parsed = jsonStr;
		jsonStr.do({|char,pos|
			var inString = false;
			char.switch(
				$",{(jsonStr[pos-1]==$\ && inString).not.if({inString = inString.not})},
				${,{ if(inString.not){parsed[pos] = $(} },
				$},{ if(inString.not){parsed[pos] = $)} }
			)
		});
		^this.toDict(parsed.interpret);
	}

	asJSON{|d|
		if(d.isNumber){^d};
		if(d.isString){^d.asString.asCompileString};
		if(d.isKindOf(Symbol)){^this.asJSON(d.asString)};
		if(d.isKindOf(Dictionary))
		{
		  ^"{" ++ (
			d.keys.asList.collect{|k|
			k.asString.asCompileString ++ ":" + this.asJSON(d[k])
			}).join(", ") ++ "}"
		};
		if(d.isKindOf(SequenceableCollection))
		{
			^"[" ++ d.collect({|x|this.asJSON(x)}).join(", ")++ "]";
		};
	}
}
